Technisch artikel

PDFium Page-Object-Handles Verouderd na Transform in Delphi

Wanneer FPDFPage_TransFormWithClip een pagina herschrijft, beschrijft elke FPDF_PAGEOBJECT-handle die je al hebt, nog steeds de parse van vóór de transform. Het PDFium Component voor Delphi en C++Builder lost dit op binnen TransformPageContent, dat de tekstpagina ontlaadt, content regenereert, en dan de pagina herlaadt zodat latere queries de nieuwe coördinaten zien

Het symptoom is stil. Je past een schaal van 0,9 toe om een afdrukmarge toe te voegen, leest dan PageObjectInfo en krijgt exact dezelfde getallen als vóór de aanroep. Geen exception, geen foutcode, niets in een log. Dit is een ander falen dan de gecachete tekstpagina beschreven in het artikel over verouderde tekstpagina's na een bewerking: daar is de cache één enkele FPDF_TEXTPAGE-handle die je kunt weggooien en herbouwen, hier is het probleem elke page-object-handle in je eigen variabelen, plus een klasse van getters die falen rapporteren via een returncode die de meeste aanroepers weggooien

Waarom raken de grenzen van een page object verouderd zonder fout?

Omdat een page-object-handle een pointer is naar een geparseerde representatie van één specifieke contentstream, en een pagina-brede transform die contentstream vervangt door een nieuwe. PDFium loopt niet door je call stack op zoek naar handles om te patchen. Het bouwt een verse objectgraaf en laat de oude precies zoals hij was, dus een lees tegen de oude handle is een volkomen geldige lees van een structuur die niet langer overeenkomt met wat het bestand zegt

ISO 32000-1 §7.8.2 definieert de contentstream als de sequentie operators die een pagina tekent, en §8.3.3 definieert hoe de huidige transformatiematrix gebruikersruimte op deviceruimte afbeeldt. Een pagina-niveau-transform wordt uitgedrukt door die operators te omwikkelen en te herschrijven, niet door coördinaten per object ter plekke te bewerken. De coördinaten die de objecten dragen veranderen dus mogelijk helemaal niet; wat verandert is de matrix die van kracht is wanneer ze getekend worden. Elke handle die geparseerd werd onder de oude matrix beantwoordt geometrievragen onder de oude matrix, en beantwoordt ze zonder klacht

Wat FPDFPage_TransFormWithClip daadwerkelijk herschrijft

Het herschrijft de pagina, niet je momentopnames. FPDFPage_TransFormWithClip neemt een FS_MATRIX en een FS_RECTF-cliprechthoek en past beide toe op de hele paginacontent. Het is de juiste aanroep voor marges, imposition-schaling, en het normaliseren van een vreemd gedimensioneerde pagina tegen een doelbox. Het is de verkeerde aanroep om naar te grijpen als je verwacht dat bestaande handles meegaan, en het is ook het waard om te onthouden dat het alleen paginacontent raakt: annotaties zijn een aparte laag en hebben TransformPageAnnotations nodig, wat dezelfde zes matrixcoëfficiënten doorstuurt naar 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;

De verversvolgorde die TransformPageContent gebruikt

Vier stappen, in deze volgorde: tekstpagina ontladen, transformeren, content genereren, pagina herladen. TPdf.TransformPageContent draait precies die sequentie. Het roept CheckPageActive aan, kopieert de matrix en de clip naar hun native recordvormen, roept UnloadTextPage aan, dan FPDFPage_TransFormWithClip, dan UpdatePage, wat de wrapper rond FPDFPage_GenerateContent is, en tot slot ReloadPage

Elke stap verdient zijn plek. UnloadTextPage gaat eerst omdat de gecachete FPDF_TEXTPAGE tekenboxen bevat berekend onder de oude matrix, en het laat ook de afgeleide weblinklijst en elke lopende find-sessie vallen die daaruit gebouwd waren. FPDFPage_GenerateContent moet vóór de herlaad draaien, omdat de transform in de in-memory pagina leeft totdat hij teruggeserialiseerd wordt naar de contentstream, en een herlaad anders de ongewijzigde stream opnieuw zou parsen. ReloadPage sluit af met FPDF_LoadPage tegen de huidige pagina-index, wat het enige is dat je daadwerkelijk een verse objectgraaf geeft

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

Eén detail in ReloadPage is het waard om over te nemen als je deze sequentie ooit zelf schrijft. Het laadt eerst de nieuwe pagina en committeert die pas daarna aan het veld, zodat een mislukte paginaload de huidige native pagina en al zijn afgeleide caches intact laat in plaats van je in een halfafgebroken staat te laten vallen. Herladen is niet gratis — je betaalt voor een volledige herparse van de pagina — maar het wordt eenmaal per transform betaald, niet eenmaal per query, en er is geen goedkoper correct alternatief

Draag geen handles over de herlaad heen

Na de herlaad zijn de oude handles niet slechts verouderd, ze zijn bungelend. De vorige FPDF_PAGE is gesloten, en de FPDF_PAGEOBJECT-waarden die ertoe behoorden, zijn pointers naar vrijgegeven geheugen. TPdfPageObjectInfo stelt de native handle bloot in zijn veld Handle, wat echt nuttig is om een object rechtstreeks door te geven aan een low-level-aanroep, en net zo echt gevaarlijk om te bewaren in een formuliersveld of een lijst over een bewerking heen die de pagina herlaadt. Behandel een snapshotrecord als alleen geldig totdat de volgende aanroep die content regenereert, in dezelfde geest als de eigendomsregels besproken in de notities over ABI en geheugenveiligheid aan de PDFium-grens

Kan een getter falen en toch geldige data lijken?

Ja, en dit is de tweede helft van hetzelfde probleem. FPDFPageObj_GetRotatedBounds en FPDFPageObj_GetIsActive zijn out-parameter-getters: ze retourneren een int-succesvlag en schrijven het echte antwoord naar een referentie-argument. Beide kunnen FALSE retourneren voor een object dat aangemaakt is maar waarvan de pagina nog niet opnieuw geparseerd is. Wanneer dat gebeurt, blijft de out-parameter onaangeroerd, en een Pascal-record geïnitialiseerd met Default(TPdfPageObjectInfo) is allemaal nullen, dus de aanroeper ziet een vierhoek met vier punten op de oorsprong en een Active-vlag van False. Een mislukte aanroep is stilzwijgend gepromoveerd tot plausibel ogende data

TPdfPageObjectInfo beantwoordt dit met expliciete sentinels. HasRotatedBounds draagt het resultaat van de aanroep FPDFPageObj_GetRotatedBounds, HasActiveState draagt het resultaat van FPDFPageObj_GetIsActive, en de geometrie- en staatsvelden worden alleen geschreven wanneer de corresponderende sentinel True is. Hetzelfde patroon herhaalt zich door het record voor de andere out-parameter-getters, dus HasMatrix, HasFillColor, HasStrokeColor, en HasStrokeWidth betekenen allemaal hetzelfde: de native aanroep slaagde en het aangrenzende veld is betekenisvol

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

Het patroon generaliseert naar elke PDFium-getter die de conventie returncode-plus-out-parameter volgt, en daar zijn er veel van. Als een wrapper die conventie laat inklappen tot een gewoon functieresultaat, heeft hij het enige signaal weggegooid dat "het antwoord is nul" onderscheidt van "er is geen antwoord". Eén extra boolean per veld meedragen kost een byte en verwijdert een hele categorie bugs waarbij een record met standaardwaarden aangezien wordt voor een meting

Waar dit nog steeds bijt

Drie eerlijke grenzen. Ten eerste is de verversing per pagina: transformeer je pagina twee, dan blijven handles die je vasthoudt voor pagina één ongewijzigd, maar dan heb je nu twee pagina's op verschillende tijdstippen geparseerd en is het aan jou om te onthouden welke momentopnames van welke kwamen. Ten tweede is indexstabiliteit niet gegarandeerd over een contentregeneratie heen — na de herlaad is index 3 wat index 3 ook is in de nieuwe parse, dus identificeer objecten opnieuw op hun type en geometrie in plaats van aan te nemen dat posities standhielden. Ten derde wordt de cliprechthoek in FPDFPage_TransFormWithClip toegepast op paginacontent en verandert die geen enkele paginabox van formaat; als je content omlaag schaalt om een marge te creëren, is de MediaBox nog steeds de grootte die hij altijd was, en een viewer zal het oorspronkelijke vel tonen met de tekening erin verkleind. Niets hiervan is exotisch — het is het gewone gevolg van een C-API die pointers uitdeelt naar geparseerde staat en levensduur aan de aanroeper overlaat. De oplossing is degene die overal elders werkt: definieer precies wanneer een snapshot verloopt, ververs op die grens, en laat een mislukte aanroep nooit doorgaan voor een waarde

Werk je meer in het algemeen aan matrixgedrag, dan wordt de vermenigvuldigingsvolgorde die bepaalt waar een transform landt behandeld in het artikel over prepend, append en pivot met matrices. De hier beschreven transform- en page-object-API's worden geleverd met het PDFium Component voor Delphi en C++Builder, waarvan de productpagina de volledige referentie draagt voor het page-object-snapshotrecord en zijn sentinel-velden