PDFium Component умеет открывать PDF, живущий внутри большего буфера, прямо из диапазона байт. Перегрузка LoadDocument(const Data: TBytes; Index, Count: Integer; Buffered: Boolean) адресует окно на месте, так что предварительный Copy не нужен. Взамен она просит понять одно правило: когда Buffered равен False, лежащий в основе массив заимствуется, а не копируется
Это другой механизм по сравнению с подходом на основе обратных вызовов, описанным в статье потоковая загрузка больших PDF по требованию с PDFium VCL, которая передаёт PDFium читатель FPDF_FILEACCESS и позволяет ему вытягивать блоки с диска по мере необходимости. Тот вариант — для документов, слишком больших, чтобы держать их в ОЗУ. Этот вариант — для документов, уже находящихся в ОЗУ, сидящих по известному смещению внутри чего-то другого. Оба дополняют друг друга, и последний раздел объясняет, какая ситуация к какому случаю относится
Копия на 40 МБ, которую никто не заказывал
Этот сценарий встречается везде, где PDF путешествуют внутри других форматов. Почтовое хранилище держит тела сообщений и вложения в одной записи. Контейнер архива соединяет манифест, несколько изображений и PDF. Собственный протокол передачи данных кадрирует документ за заголовком с префиксом длины. В каждом случае вы в итоге держите один большой TBytes и знаете, что PDF начинается с байта 1 182 336 и занимает 312 килобайт
До появления перегрузки для диапазона байт идиоматичным ответом был Copy(Data, Index, Count), который выделяет второй массив и копирует окно в него через memcpy. Затем вы передаёте этот срез в LoadDocument с Buffered = True, что копирует его снова в приватный буфер компонента. Две копии одних и тех же байт, одна из которых — чистая формальность, и на большом сканировании почтового ящика это повторяется для каждого сообщения. Перегрузка для диапазона байт убирает первую копию безусловно, а вторую — опционально
Что на самом деле делает перегрузка для диапазона байт
Перегрузка намеренно тонкая: она проверяет, вычисляет один указатель и делегирует форме LoadDocument, принимающей указатель, через которую уже проходит всё семейство. Index отсчитывается от нуля, Count — это длина в байтах, а Buffered по умолчанию True, точно так же, как в других перегрузках. Однопараметрический LoadDocument(const Data: TBytes; Buffered: Boolean) теперь сам по себе просто вызов этой перегрузки с Index = 0 и Count = Length(Data), так что путь проверки один, а не два
Вызов выглядит как код, который вы уже писали, минус срез
var
Frame: TBytes; // whole container record, tens of megabytes
Offset, Size: Integer;
begin
Frame := LoadContainerRecord('mailbox.dat');
LocateEmbeddedPdf(Frame, Offset, Size); // your container parser
// No Copy(Frame, Offset, Size) here - the window is addressed in place
Pdf.LoadDocument(Frame, Offset, Size, True);
try
RenderPreview(Pdf);
finally
Pdf.UnloadDocument;
end;
end;
Почему Index плюс Count переполняет проверку границ?
Потому что Index и Count оба типа Integer, а сумма двух больших положительных значений Integer не обязательно является большим положительным Integer. Это техническое ядро перегрузки, и это как раз то место, где естественно выглядящая проверка становится дырой в безопасности памяти. Очевидная формулировка неверна
// WRONG: Index + Count is evaluated in Integer and can wrap negative
if Index + Count <= Length(Data) then
DataPtr := @Data[Index];
// RIGHT: reject signs first, then bound each term separately,
// with the only arithmetic done as a subtraction that cannot wrap
Check(Index >= 0, 'PDF byte range index cannot be negative');
Check(Count >= 0, 'PDF byte range count cannot be negative');
Check(Index <= Length(Data), 'PDF byte range index exceeds data length');
Check(Count <= Length(Data) - Index, 'PDF byte range exceeds data length');
Разберите падающий случай пошагово. Возьмите Index = 2000000000 и Count = 2000000000. Их истинная сумма — четыре миллиарда, но в 32-битной знаковой арифметике результат переполняется ровно в минус 294 967 296. Это значение спокойно меньше, чем Length(Data), так что неверная проверка проходит, @Data[Index] берётся далеко за пределами массива, и PDFium получает дикий указатель плюс длину в два гигабайта. Дальше следует нарушение доступа в хороший день и молчаливый разбор не относящейся к делу памяти процесса в плохой
Правильный порядок исправляет это, вообще не складывая. Отрицательные значения отклоняются до того, как что-либо индексируется, так что @Data[Index] никогда не может быть взят ниже массива. Затем Index ограничивается сам по себе относительно Length(Data), что гарантирует, что Length(Data) - Index — неотрицательный Integer. И только затем Count сравнивается с этим остатком. Каждое промежуточное значение остаётся внутри представимого диапазона, так что никакая конфигурация сборки не может изменить результат. Не поддавайтесь искушению полагаться и на проверку переполнения {$Q+} как на страховку: релизные сборки регулярно поставляются с ней отключённой, а даже когда она включена, вы всего лишь превратили дыру в безопасности памяти в исключение EIntOverflow, вырывающееся из середины процедуры проверки. PDFium Component обходится с арифметикой недоверенной длины так же, как и с остальной границей, — эта дисциплина шире разобрана в статье об укреплении ABI PDFium VCL и безопасности памяти в Delphi
Почему окно нулевой длины должно передавать nil?
Потому что @Data[Index] — не легальное выражение для каждого Index, который принимает проверка. Index = Length(Data) с Count = 0 — это совершенно корректно сформированное пустое окно в хвосте буфера, а пустой TBytes даёт Index = 0 на массиве, у которого вообще нет элемента с индексом ноль. Взятие адреса в любом из этих случаев индексирует за конец либо разыменовывает нулевой динамический массив. Поэтому перегрузка ветвится: Count = 0 даёт нулевой указатель, любой другой count даёт @Data[Index]. Затем nil попадает в перегрузку с указателем, чья собственная защита принимает нулевой указатель, когда размер равен нулю, и загрузка заканчивается обычной ошибкой «Cannot load PDF document» вместо нарушения доступа. Вызывающий код, вычисливший окно нулевой длины из некорректного контейнера, получает чистое, перехватываемое исключение EPdfError, как и на любом другом плохом вводе
Заимствовано или скопировано: что решает Buffered
Buffered выбирает контракт владения, и это единственный параметр здесь, чьи последствия выходят за пределы вызова. При Buffered = True PDFium Component копирует выбранное окно, и только окно, в свой внутренний буфер перед загрузкой. 40-мегабайтный контейнер не копируется; PDF в 312 КБ копируется. Как только LoadDocument возвращает управление, вы можете сразу же освободить, переиспользовать или перезаписать контейнер, потому что компонент больше на него не ссылается. Это значение по умолчанию, и это правильный выбор почти для всего кода
Buffered = False передаёт @Data[Index] напрямую в FPDF_LoadMemDocument64, и PDFium хранит этот указатель на протяжении жизни документа вместо копирования байт. Это делает загрузку свободной от выделений памяти и делает весь лежащий в основе TBytes заимствованным ресурсом. Он должен оставаться живым и неизменённым до тех пор, пока не выполнится UnloadDocument или Active не станет False. Не окно, а весь массив: динамический массив подсчитывает ссылки как единое целое, и если последняя ссылка уйдёт где угодно в вашем коде, это освободит память, которую PDFium всё ещё читает. Установка Length на нём столь же фатальна, потому что перевыделение может переместить блок. Указывайте это в собственной документации API везде, где вы предоставляете подобную загрузку, в том же духе, что и любая другая граница заимствования-против-владения в коде на Pascal; режим сбоя идентичен опасностям алиасинга, описанным в статье о утечке FillChar и строки результата в Delphi, где буфер выглядит как принадлежащий, а на самом деле нет
type
TFrameSession = class
private
FFrame: TBytes; // owns the backing storage for as long as FPdf is loaded
FPdf: TPdf;
public
procedure OpenEmbedded(Offset, Size: Integer);
destructor Destroy; override;
end;
procedure TFrameSession.OpenEmbedded(Offset, Size: Integer);
begin
// Buffered = False: FFrame must outlive the loaded document
FPdf.LoadDocument(FFrame, Offset, Size, False);
end;
destructor TFrameSession.Destroy;
begin
FPdf.UnloadDocument; // release the borrow first
FFrame := nil; // only now may the storage go
inherited;
end;
Когда окно диапазона байт — неверный инструмент
Будьте честны насчёт границы. Перегрузка для диапазона байт предполагает, что контейнер уже полностью в памяти, а Count — это Integer, так что одно окно не может превышать два гигабайта. Если контейнер — это архив на 6 ГБ на диске или приходит через сокет, который вы не можете перемотать назад, эта перегрузка не поможет вам, а чтение всего этого в TBytes лишь для того, чтобы адресовать окно внутри него, сводит на нет весь смысл. Именно там место пути FPDF_FILEACCESS, и статья о потоковой загрузке по требованию показывает, как представить смещённое представление файла как собственный источник документа. Точно так же, если встроенные байты нуждаются в преобразовании до того, как их увидит PDFium — распаковке, расшифровке, шаге снятия обёртки, — настоящая копия неизбежна, и Buffered = True на преобразованном массиве — честный ответ. Окно диапазона байт окупается ровно в одной форме: непрерывные, неизменённые байты PDF, уже резидентные, по известному смещению
Если вы оцениваете это для просмотрщика, панели предпросмотра или конвейера пакетного приёма, перегрузка для диапазона байт и потоковый загрузчик — две из стратегий загрузки, которые PDFium Component поставляет наряду с загрузкой из файла, потока и сырого указателя. Полная поверхность API, лицензирование и поддержка версий Delphi и C++Builder документированы на странице продукта PDFium Component