Techninis straipsnis

PDFium puslapio objekto rankenos pasenusios po transformacijos Delphi

Kai FPDFPage_TransFormWithClip perrašo puslapį, kiekviena FPDF_PAGEOBJECT rankena, kurią jau turite, vis dar aprašo analizę iš prieš transformaciją. PDFium Component Delphi ir C++Builder platformoms tai sprendžia TransformPageContent viduje, kuri paleidžia teksto puslapį, atnaujina turinį, tada iš naujo įkelia puslapį, kad vėlesnės užklausos matytų naujas koordinates

Simptomas — tylus. Pritaikote 0.9 mastelį, kad pridėtumėte spausdinimo paraštę, tada skaitote PageObjectInfo ir gaunate lygiai tuos pačius skaičius, kuriuos gavote prieš iškvietimą. Jokios išimties, jokio klaidos kodo, nieko žurnale. Tai — kitoks gedimas nei podėlyje esantis teksto puslapis, aprašytas straipsnyje apie pasenusius teksto puslapius po redagavimo: ten podėlis — viena FPDF_TEXTPAGE rankena, kurią galite paleisti ir atkurti, čia problema — kiekviena puslapio objekto rankena jūsų pačių kintamuosiuose, plius getter'ių klasė, pranešanti apie nesėkmę per grąžinimo kodą, kurį dauguma iškviečiančiųjų išmeta

Kodėl puslapio objekto ribos pasensta be klaidos?

Todėl, kad puslapio objekto rankena — rodyklė į išanalizuotą vieno konkretaus turinio srauto vaizdą, o visas puslapio transformacija pakeičia tą turinio srautą nauju. PDFium neina per jūsų iškvietimų steką, ieškodama rankenų taisyti. Ji sukuria šviežią objektų grafą ir palieka senąjį lygiai toks, koks buvo, todėl skaitymas prieš seną rankeną — visiškai galiojantis struktūros, kuri nebeatitinka to, ką sako failas, skaitymas

ISO 32000-1 §7.8.2 apibrėžia turinio srautą kaip operatorių seką, piešiančią puslapį, o §8.3.3 apibrėžia, kaip dabartinė transformacijos matrica atvaizduoja vartotojo erdvę į įrenginio erdvę. Puslapio lygio transformacija išreiškiama apgaubiant ir perrašant tuos operatorius, ne redaguojant kiekvieno objekto koordinates vietoje. Todėl objektų nešamos koordinatės gali visai nepasikeisti; tai, kas keičiasi, — matrica, galiojanti piešimo metu. Bet kokia rankena, kuri buvo išanalizuota prie senos matricos, atsako į geometrijos klausimus prie senos matricos, ir atsako be jokio nusiskundimo

Ką iš tikrųjų perrašo FPDFPage_TransFormWithClip

Ji perrašo puslapį, ne jūsų nuotraukas. FPDFPage_TransFormWithClip ima FS_MATRIX ir FS_RECTF apkarpymo stačiakampį ir taiko abu visam puslapio turiniui. Tai — teisingas iškvietimas paraštėms, sudėties mastelio keitimui ir keisto dydžio puslapio normalizavimui pagal tikslinę dėžę. Tai — neteisingas iškvietimas, jei tikitės, kad esamos rankenos seks kartu, ir taip pat verta prisiminti, kad ji liečia tik puslapio turinį: anotacijos — atskiras sluoksnis, joms reikia TransformPageAnnotations, kuri persiunčia tuos pačius šešis matricos koeficientus 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;

Atnaujinimo tvarka, kurią naudoja TransformPageContent

Keturi žingsniai, šia tvarka: paleisti teksto puslapį, transformuoti, generuoti turinį, iš naujo įkelti puslapį. TPdf.TransformPageContent vykdo lygiai šią seką. Ji iškviečia CheckPageActive, kopijuoja matricą ir apkarpymą į savo natyvias įrašų formas, iškviečia UnloadTextPage, tada FPDFPage_TransFormWithClip, tada UpdatePage, kuri yra apvalkalas aplink FPDFPage_GenerateContent, ir galiausiai ReloadPage

Kiekvienas žingsnis pelnytai užima savo vietą. UnloadTextPage eina pirmas, nes podėlyje esantis FPDF_TEXTPAGE laiko simbolių dėžes, apskaičiuotas pagal seną matricą, ir jis taip pat paleidžia kildintą žiniatinklio nuorodų sąrašą ir bet kokį vykstantį paieškos seansą, sukurtą iš jo. FPDFPage_GenerateContent turi vykti prieš pakartotinį įkėlimą, nes transformacija gyvena atmintyje esančiame puslapyje, kol ji serializuojama atgal į turinio srautą, o pakartotinis įkėlimas kitaip iš naujo analizuotų nepakeistą srautą. ReloadPage baigiasi FPDF_LoadPage pagal dabartinį puslapio indeksą, kas — vienintelis dalykas, iš tikrųjų duodantis jums šviežią objektų grafą

// 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;

Viena detalė ReloadPage verta pasiskolinti, jei kada nors rašytumėte šią seką patys. Ji pirma įkelia naują puslapį ir tik po to jį patvirtina laukui, todėl nepavykęs puslapio įkėlimas palieka dabartinį natyvų puslapį ir visus jo kildintus podėlius nepaliestus, o ne numeta jus į pusiau sugriautą būseną. Pakartotinis įkėlimas — ne nemokamas, jūs mokate už pilną puslapio analizę iš naujo, bet apmokama vieną kartą transformacijai, ne kiekvienai užklausai, ir nėra pigesnės teisingos alternatyvos

Nenešiokite rankenų per pakartotinį įkėlimą

Po pakartotinio įkėlimo senosios rankenos ne tik pasenusios, jos — kabančios. Ankstesnis FPDF_PAGE uždarytas, o FPDF_PAGEOBJECT reikšmės, priklausiusios jam, — rodyklės į paleistą atmintį. TPdfPageObjectInfo atveria natyvią rankeną savo Handle lauke, kuris iš tikrųjų naudingas perduodant objektą tiesiai į žemesnio lygio iškvietimą, ir lygiai taip pat iš tikrųjų pavojingas laikyti formos lauke ar sąraše per operaciją, iš naujo įkeliančią puslapį. Traktuokite nuotraukos įrašą kaip galiojantį tik iki kito iškvietimo, kuris iš naujo generuoja turinį, ta pačia dvasia kaip nuosavybės taisyklės, aptartos straipsnyje apie ABI ir atminties saugumą prie PDFium ribos

Ar getter'is gali nepavykti ir vis tiek atrodyti kaip galiojantys duomenys?

Taip, ir tai — antra tos pačios problemos pusė. FPDFPageObj_GetRotatedBounds ir FPDFPageObj_GetIsActive — iš-parametro getter'iai: jie grąžina int sėkmės vėliavėlę ir įrašo tikrą atsakymą į nuorodos argumentą. Abu gali grąžinti FALSE objektui, kuris buvo sukurtas, bet kurio puslapis dar nebuvo iš naujo išanalizuotas. Kai tai nutinka, iš-parametras paliekamas nepaliestas, o Pascal įrašas, inicializuotas su Default(TPdfPageObjectInfo), yra visi nuliai, todėl iškviečiantysis mato keturkampį su keturiais taškais pradžios taške ir Active vėliavėlę, lygią False. Nepavykęs iškvietimas tyliai paverstas tikėtinai atrodančiais duomenimis

TPdfPageObjectInfo į tai atsako aiškiais sargybiniais. HasRotatedBounds neša FPDFPageObj_GetRotatedBounds iškvietimo rezultatą, HasActiveState neša FPDFPageObj_GetIsActive rezultatą, o geometrijos ir būsenos laukai rašomi tik tada, kai atitinkamas sargybinis — True. Ta pati forma kartojasi visame įraše kitiems iš-parametro getter'iams, todėl HasMatrix, HasFillColor, HasStrokeColor ir HasStrokeWidth visi reiškia tą patį: natyvus iškvietimas pavyko, o šalia esantis laukas — prasmingas

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

Šis raštas apibendrinamas kiekvienam PDFium getter'iui, sekančiam grąžinimo-kodo-plius-iš-parametro konvenciją, o tokių — daug. Jei apvalkalas suplokština tą konvenciją į paprastą funkcijos rezultatą, jis prarado vienintelį signalą, atskiriantį „atsakymas — nulis“ nuo „atsakymo nėra“. Vieno papildomo loginio kintamojo laikymas kiekvienam laukui kainuoja baitą ir pašalina visą klaidų kategoriją, kurioje numatytas įrašas suklaidinamas kaip matavimas

Kur tai vis dar kanda

Trys sąžiningos ribos. Pirma, atnaujinimas — vieno puslapio: transformuokite antrą puslapį, ir bet kokios rankenos, kurias laikote pirmam puslapiui, nepaliečiamos, bet dabar turite du puslapius, išanalizuotus skirtingu metu, ir jūsų reikalas — prisiminti, kurios nuotraukos iš kurio kilo. Antra, indekso stabilumas negarantuojamas per turinio regeneraciją — po pakartotinio įkėlimo indeksas 3 yra tai, kas yra indeksas 3 naujoje analizėje, todėl iš naujo identifikuokite objektus pagal jų tipą ir geometriją, o ne manydami, kad pozicijos išsilaikė. Trečia, apkarpymo stačiakampis FPDFPage_TransFormWithClip taikomas puslapio turiniui ir nepakeičia dydžio jokiai puslapio dėžei; jei sumažinate turinį, kad sukurtumėte paraštę, MediaBox vis tiek tokio dydžio, koks visada buvo, o žiūryklė parodys originalų lapą su sutrauktu brėžiniu jo viduje. Nieko iš to nėra egzotiška — tai įprasta pasekmė C API, dalinančio rodykles į išanalizuotą būseną ir paliekančio gyvavimo trukmę iškviečiančiajam. Pataisymas — toks, kuris veikia visur kitur: apibrėžkite tiksliai, kada nuotrauka nebegalioja, atnaujinkite prie tos ribos ir niekada neleiskite nepavykusiam iškvietimui apsimesti reikšme

Jei nagrinėjate matricos elgesį plačiau, daugybos tvarka, nusprendžianti, kur atsiduria transformacija, aprašyta straipsnyje apie prepend, append ir pivot su matricomis. Transformacijos ir puslapio objekto API, aprašytos čia, dalyvauja PDFium Component Delphi ir C++Builder platformoms, kurio produkto puslapyje pateikta pilna puslapio objekto nuotraukos įrašo ir jo sargybinių laukų dokumentacija