Когато FPDFPage_TransFormWithClip презаписва страница, всеки манипулатор FPDF_PAGEOBJECT, който вече държите, все още описва разбора отпреди трансформацията. PDFium Component за Delphi и C++Builder решава това вътре в TransformPageContent, който освобождава текстовата страница, регенерира съдържанието, после презарежда страницата, така че по-късни заявки да виждат новите координати
Симптомът е тих. Прилагате скала 0.9, за да добавите поле за печат, после четете PageObjectInfo и получавате точно числата, които сте получили преди извикването. Никакво изключение, никакъв код за грешка, нищо в лог. Това е различен провал от кешираната текстова страница, описана в статията за остарели текстови страници след редакция: там кешът е единствен манипулатор FPDF_TEXTPAGE, който можете да отхвърлите и преизградите, тук проблемът е всеки манипулатор на page object в собствените ви променливи, плюс клас getter-и, отчитащи провал чрез код за връщане, който повечето извикващи изхвърлят
Защо границите на page object остаряват без грешка?
Защото манипулатор на page object е указател в разбрано представяне на един конкретен поток със съдържание, а трансформация на цяла страница заменя този поток със съдържание с нов. PDFium не обхожда вашия стек на извиквания, търсейки манипулатори за закърпване. Той изгражда свеж граф от обекти и оставя стария точно какъвто е бил, така че четене спрямо стария манипулатор е напълно валидно четене на структура, която вече не съответства на това, което файлът казва
ISO 32000-1 §7.8.2 дефинира потока със съдържание като последователността от оператори, рисуваща страница, а §8.3.3 дефинира как текущата трансформационна матрица картографира потребителското пространство върху пространството на устройството. Трансформация на ниво страница се изразява чрез обвиване и презаписване на тези оператори, не чрез редактиране на координати за отделен обект на място. Така координатите, които обектите носят, може изобщо да не се променят; това, което се променя, е матрицата, действаща в сила, когато те се рисуват. Всеки манипулатор, разбран под старата матрица, отговаря на въпроси за геометрия под старата матрица, и отговаря без оплакване
Какво всъщност презаписва FPDFPage_TransFormWithClip
Презаписва страницата, не вашите снимки. FPDFPage_TransFormWithClip взема FS_MATRIX и правоъгълник за изрязване FS_RECTF и прилага и двете върху цялото съдържание на страницата. Това е правилното извикване за полета, скалиране на импозиция и нормализиране на странно оразмерена страница спрямо целева кутия. Грешно е извикване, към което да посегнете, ако очаквате съществуващи манипулатори да следват, и си струва също да се помни, че докосва само съдържанието на страницата: анотациите са отделен слой и се нуждаят от TransformPageAnnotations, който препраща същите шест коефициента на матрицата на FPDFPage_TransformAnnots
var
Info: TPdfPageObjectInfo;
Scale: FS_MATRIX;
Clip: TPdfRectangle;
begin
Pdf.PageNumber:= 1;
Info:= Pdf.PageObjectInfo(0); // snapshot taken before the transform
Scale.a:= 0.9; Scale.b:= 0.0;
Scale.c:= 0.0; Scale.d:= 0.9;
Scale.e:= 29.7; Scale.f:= 42.0; // 5% margin, A4 in points
Clip:= Pdf.GetPageBox(pbMedia);
Pdf.TransformPageContent(Scale, Clip);
// Info.Bounds still holds pre-transform geometry, and Info.Handle now
// points into a page that TransformPageContent has already replaced
end;
Редът на опресняване, който TransformPageContent използва
Четири стъпки, в този ред: освобождаване на текстовата страница, трансформиране, генериране на съдържание, презареждане на страницата. TPdf.TransformPageContent изпълнява точно тази последователност. Извиква CheckPageActive, копира матрицата и изрязването в нативните им форми на запис, извиква UnloadTextPage, после FPDFPage_TransFormWithClip, после UpdatePage, което е обвивката около FPDFPage_GenerateContent, и накрая ReloadPage
Всяка стъпка заслужава мястото си. UnloadTextPage върви първа, защото кешираният FPDF_TEXTPAGE държи кутии на символи, изчислени под старата матрица, а също отхвърля и извлечения списък с уеб връзки, и всяка текуща сесия за търсене, изградена от него. FPDFPage_GenerateContent трябва да се изпълни преди презареждането, защото трансформацията живее в страницата в паметта, докато не бъде сериализирана обратно в потока със съдържание, а презареждане иначе би разбрало отново немодифицирания поток. ReloadPage завършва с FPDF_LoadPage спрямо текущия индекс на страница, което е единственото нещо, което всъщност ви дава свеж граф от обекти
// After the transform, re-enumerate. Do not reuse anything captured earlier.
var
I: Integer;
Info: TPdfPageObjectInfo;
begin
Pdf.TransformPageContent(Scale, Clip); // unload text page, transform,
// generate content, reload page
for I:= 0 to Pdf.ObjectCount- 1 do
begin
Info:= Pdf.PageObjectInfo(I); // handle and bounds from the new parse
if Info.Bounds.Right> PageWidth then
Log('object '+ IntToStr(I)+ ' still overflows after scaling');
end;
end;
Една подробност в ReloadPage си струва да се копира, ако някога напишете тази последователност сами. Тя зарежда новата страница първо и я ангажира в полето едва след това, така че зареждане на страница, което се провали, оставя текущата нативна страница и всички производни от нея кешове непокътнати, вместо да ви пусне в наполовина съборено състояние. Презареждането не е безплатно — плащате за пълен повторен разбор на страницата — но се плаща веднъж на трансформация, не веднъж на заявка, и няма по-евтина правилна алтернатива
Не пренасяйте манипулатори през презареждането
След презареждането старите манипулатори не са просто остарели, те висят без опора. Предишният FPDF_PAGE е бил затворен, а стойностите на FPDF_PAGEOBJECT, принадлежали му, са указатели в освободена памет. TPdfPageObjectInfo излага нативния манипулатор в полето си Handle, което е наистина полезно за подаване на обект директно в извикване на по-ниско ниво, и еднакво наистина опасно за пазене в поле на форма или списък през операция, презареждаща страницата. Третирайте запис-снимка като валиден само до следващото извикване, регенериращо съдържание, в същия дух като правилата за собственост, обсъдени в бележките за ABI и безопасност на паметта на границата на PDFium
Може ли getter да се провали и все пак да изглежда като валидни данни?
Да, и това е втората половина на същия проблем. FPDFPageObj_GetRotatedBounds и FPDFPageObj_GetIsActive са getter-и с изходен параметър: те връщат int флаг за успех и записват реалния отговор в референтен аргумент. И двете могат да върнат FALSE за обект, който е бил създаден, но чиято страница все още не е разбрана наново. Когато това се случи, изходният параметър остава недокоснат, а Pascal запис, инициализиран с Default(TPdfPageObjectInfo), е изцяло нули, така че извикващият вижда четириъгълник с четири точки в началото и флаг Active на False. Провалено извикване тихо е повишено до правдоподобно изглеждащи данни
TPdfPageObjectInfo отговаря на това с изрични сентинели. HasRotatedBounds носи резултата от извикването на FPDFPageObj_GetRotatedBounds, HasActiveState носи резултата от FPDFPageObj_GetIsActive, а полетата за геометрия и състояние се записват само когато съответният сентинел е True. Същата форма се повтаря в записа за другите getter-и с изходен параметър, така че HasMatrix, HasFillColor, HasStrokeColor, и HasStrokeWidth всички означават едно и също: нативното извикване е успяло и съседното поле е смислено
Info:= Pdf.PageObjectInfo(I);
if Info.HasRotatedBounds then
// RotatedBounds is array [1..4] of TPdfPoint, in draw order
UseQuad(Info.RotatedBounds[1], Info.RotatedBounds[2],
Info.RotatedBounds[3], Info.RotatedBounds[4])
else
// the native call failed; fall back to the axis-aligned rectangle
UseRect(Info.Bounds);
if Info.HasActiveState and (not Info.Active) then
SkipObject(I); // genuinely inactive
// if HasActiveState is False, the object state is unknown, not inactive
Моделът се обобщава към всеки getter на PDFium, следващ конвенцията код-за-връщане-плюс-изходен-параметър, а те са много. Ако обвивка сгъне тази конвенция в обикновен резултат на функция, е изхвърлила единствения сигнал, различаващ „отговорът е нула“ от „няма отговор“. Пренасянето на един допълнителен булев на поле струва байт и премахва цяла категория бъгове, при които запис по подразбиране бива сбъркан с измерване
Къде това все още хапе
Три честни граници. Първо, опресняването е на страница: трансформирате страница две и всякакви манипулатори, които държите за страница едно, остават незасегнати, но вече имате две страници, разбрани в различни моменти, и е ваша грижа да помните кои снимки от кои идват. Второ, стабилността на индекса не е гарантирана през регенерация на съдържание — след презареждането индекс 3 е каквото и да е индекс 3 в новия разбор, така че преидентифицирайте обектите по типа и геометрията им, вместо да предполагате, че позициите са се задържали. Трето, правоъгълникът за изрязване в FPDFPage_TransFormWithClip се прилага върху съдържанието на страницата и не преоразмерява никоя от кутиите на страницата; ако смалите съдържание, за да създадете поле, MediaBox все още е с размера, който винаги е бил, а визуализатор ще покаже оригиналния лист с рисунката смалена вътре в него. Нищо от това не е екзотично — то е обикновеното следствие от C API, раздаващо указатели в разбрано състояние и оставящо времето на живот на извикващия. Поправката е тази, която работи навсякъде другаде: дефинирайте точно кога снимка изтича, опреснявайте на тази граница, и никога не позволявайте провалено извикване да се маскира като стойност
Ако работите с поведението на матрици по-общо, редът на умножение, който решава къде трансформация каца, е разгледан в статията за prepend, append и pivot с матрици. API-тата за трансформация и page object, описани тук, се доставят с PDFium Component за Delphi и C++Builder, чиято страница на продукта носи пълната справка за записа-снимка на page object и сентинелните му полета