PDFium Component може да отвори PDF, живеещ вътре в по-голям буфер, директно от байтов диапазон. Претоварената версия LoadDocument(const Data: TBytes; Index, Count: Integer; Buffered: Boolean) адресира прозорец на място, така че не е нужно предварително Copy. В замяна ви иска да разберете едно правило: когато Buffered е False, подлежащият масив е зает, не копиран
Това е различен механизъм от подхода, задвижван от callback, описан в стрийминг на големи PDF файлове при поискване с PDFium VCL, който подава на PDFium четец FPDF_FILEACCESS и му позволява да изтегля блокове от диска, когато има нужда от тях. Онзи е за документи, твърде големи, за да се съберат в RAM. Този е за документи, вече в RAM, седящи на известен офсет вътре в нещо друго. Двата се допълват, а последният раздел обяснява коя ситуация принадлежи на кой
Копието от 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+} като мрежа за безопасност: билдовете за release рутинно се доставят с нея изключена, а дори когато е включена, сте превърнали грешка в безопасността на паметта в EIntOverflow, изтичащо от средата на рутина за валидация. PDFium Component третира аритметиката на недоверена дължина по същия начин, по който третира остатъка от границата, дисциплина, разгледана по-широко в закаляване на ABI-то на PDFium VCL и безопасност на паметта в Delphi
Защо прозорец с нулева дължина трябва да подава nil?
Защото @Data[Index] не е легален израз за всеки Index, който валидацията приема. Index = Length(Data) с Count = 0 е напълно добре оформен празен прозорец в опашката на буфера, а празен TBytes дава Index = 0 върху масив, който изобщо няма елемент нула. Вземането на адреса в който и да е от случаите индексира отвъд края, или разименува nil динамичен масив. Затова претоварената версия разклонява: Count = 0 дава nil указател, всеки друг брой дава @Data[Index]. Nil-ът после протича в претоварената версия с указател, чиято собствена защита приема nil указател, когато размерът е нула, а зареждането завършва в обичайната грешка "Cannot load PDF document", вместо в нарушение на достъп. Извикващ, изчислил прозорец от нула байта от деформиран контейнер, получава чист, улавяем EPdfError като всеки друг лош вход
Заето или копирано: какво решава Buffered
Buffered избира договора за собственост, и е единственият параметър тук с последствия отвъд самото извикване. С Buffered = True PDFium Component копира избрания прозорец, и само прозореца, във вътрешния си буфер преди зареждане. 40-мегабайтовият контейнер не се копира; 312-килобайтовият PDF се копира. Щом LoadDocument се върне, можете да освободите, преизползвате или презапишете контейнера незабавно, защото компонентът вече не го реферира. Това е стойността по подразбиране и правилният избор за почти целия код
Buffered = False подава @Data[Index] директно на FPDF_LoadMemDocument64, а PDFium пази този указател за живота на документа, вместо да копира байтовете. Това прави зареждането без разпределяне на памет, и прави целия подлежащ TBytes зает ресурс. Той трябва да остане жив и немодифициран, докато UnloadDocument не се изпълни или Active не стане False. Не прозореца, целия масив: динамичен масив се преброява по референции като единица, а пускането на последната референция където и да е във вашия код освобождава паметта, която PDFium все още чете. Задаването на Length върху него е също толкова фатално, защото преразпределяне може да премести блока. Заявете това в собствената си API документация, навсякъде където излагате такова зареждане, в същия дух като всяка друга граница на заемане срещу притежаване в Pascal код; режимът на провал е идентичен на опасностите от aliasing, описани в изтичането на 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 байтове, вече резидентни, на известен офсет
Ако оценявате това за визуализатор, панел за преглед или pipeline за пакетен прием, претоварената версия за байтов диапазон и поточният зареждащ модул са две от стратегиите за зареждане, които PDFium Component доставя заедно с зареждания от файл, поток и суров указател. Пълната повърхност на API-то, лицензирането и поддръжката на версии за Delphi и C++Builder са документирани на страницата на продукта PDFium Component