Technisch artikel

Tekstmarkeringsannotaties met PDFium QuadPoints in Delphi

Het PDFium-component maakt tekstmarkeringsannotaties, dat wil zeggen markeringen (highlights), onderstrepingen (underlines), doorhalingen (strikeouts) en golvende lijnen (squigglies), via TPdf.CreateAnnotation: u stelt HasAttachmentPoints := True in op het record TPdfAnnotation en vult de vierhoek AttachmentPoints in, waarna het component de QuadPoints-invoer schrijft die is gedefinieerd in ISO 32000-1 §12.5.6.10. Dat is het volledige API-oppervlak. De reden waarom dit artikel bestaat, is wat daaronder gebeurt, omdat de onbewerkte PDFium-aanroepketen een faalmodus heeft die het minst behulpzame symptoom in de hele toolkit oplevert: FPDFAnnot_SetAttachmentPoints retourneert elke keer false op een zojuist gemaakte annotatie, zonder foutcode en zonder aanwijzing. Dit is de tegenhanger aan de creatiezijde van ons artikel over het lezen en controleren van bestaande annotaties, dat de andere kant op loopt door dezelfde structuren

Het scenario voor foutopsporing is altijd hetzelfde. U maakt een markeringsannotatie, roept de attachment-points setter aan met index 0, de functie retourneert false, en u begint te twijfelen aan uw coördinaten. U transponeert de punten, draait de Y-as om, wisselt paginaruimte om voor apparaatruimte. Niets daarvan helpt, want de coördinaten waren nooit het probleem. Het probleem ligt bij de index-semantiek van de C-API, en als u die eenmaal doorheeft, is de oplossing slechts twee regels code

Wat QuadPoints betekenen in ISO 32000-1

QuadPoints is een array van 8×n getallen die n vierhoeken beschrijven, en ISO 32000-1 §12.5.6.10 vereist dit op elke tekstmarkeringsannotatie: elke vierhoek markeert een woord of een groep aaneengesloten woorden waarop de markering, onderstreping of doorhaling van toepassing is. De invoer Rect van de annotatie bestaat nog steeds, maar voor markerings-subtypes begrenst deze alleen het gebied; de 'quads' (vierhoeken) zijn wat de renderer daadwerkelijk tekent. Een vierhoek in plaats van een rechthoek, omdat tekst gedraaid of schuin getrokken kan zijn, dus de vier hoeken worden opgeslagen als vier onafhankelijke punten: x1 y1 x2 y2 x3 y3 x4 y4

De volgorde van die vier punten is waar de specificatie en de praktijk uiteenlopen. De specificatietekst beschrijft de punten als het tegen de klok in doorlopen van de vierhoek, maar Adobe's eigen renderer heeft ze altijd geïnterpreteerd in een Z-patroon: eerst de bovenrand van links naar rechts, en dan de onderrand van links naar rechts. Omdat elke auteur testte tegen Acrobat, volgt vrijwel elke renderer, inclusief PDFium, het Z-patroon. Bestanden die de letterlijke bewoording van de specificatie volgen, worden in sommige viewers weergegeven als ingeklapte of gedraaide markeringen. PDFium's struct FS_QUADPOINTSF codeert exact deze conventie: (x1,y1) is de hoek linksboven, (x2,y2) rechtsboven, (x3,y3) linksonder, en (x4,y4) rechtsonder, in paginacoördinaten waarbij Y naar boven toe groeit. Volg die volgorde; renderers zijn coulant met veel dingen, maar een verminkte vierhoek hoort daar niet bij

Waarom retourneert FPDFAnnot_SetAttachmentPoints false?

FPDFAnnot_SetAttachmentPoints faalt bij een nieuwe annotatie omdat het contract ervan is om de vierhoek op een gegeven index te vervangen, en een zojuist gemaakte annotatie heeft nul vierhoeken om te vervangen. De signatuur vereist een annotatie-handle, een quad_index en de punten; index 0 betekent niet "het eerste slot, maak het indien nodig aan", het betekent "de bestaande quad nummer 0". Wanneer FPDFAnnot_CountAttachmentPoints 0 rapporteert, is er niet zo'n quad en retourneert de aanroep false. De functie dat een slot aanmaakt is FPDFAnnot_AppendAttachmentPoints. Elke annotatie die via FPDFPage_CreateAnnot wordt gemaakt begint met een teller van nul, dus het creatie-pad moet eerst Append aanroepen, en alleen daaropvolgende updates mogen Set aanroepen

Dit zat ook in het PDFium-component zelf. Tot en met v1.79.0 was de interne routine die gedeeld werd door CreateAnnotation en SetAnnotation hardgecodeerd met FPDFAnnot_SetAttachmentPoints(Annotation, 0, ...), wat correct was voor het bijwerken van een bestaande markeringsannotatie, maar gegarandeerd faalde bij een nieuwe, wat naar boven kwam als een EPdfException met de melding 'Cannot set attachment points'. De oplossing, geleverd in v1.79.1, splitsen af op basis van de telling (count)

// Inside the component's annotation writer (v1.79.1+):
// a new annotation has no quad slots yet, so Append creates
// the first one; Set only replaces a slot that already exists
if FPDFAnnot_CountAttachmentPoints(Annotation) = 0 then
  Check(FPDFAnnot_AppendAttachmentPoints(Annotation, QuadPoints) <> 0,
    'Cannot set attachment points')
else
  Check(FPDFAnnot_SetAttachmentPoints(Annotation, 0, QuadPoints) <> 0,
    'Cannot set attachment points');

Hetzelfde patroon is van toepassing als u de geëxporteerde C-functies rechtstreeks aanroept, wat met het component mogelijk is omdat alle FPDFAnnot_*-toegangspunten beschikbaar zijn gemaakt in PDFium.pas. Telkens wanneer u een FPDF_ANNOTATION-handle vasthoudt en quads wilt schrijven, vraag dan eerst FPDFAnnot_CountAttachmentPoints en routeer dienovereenkomstig. Als u zoekt op "FPDFAnnot_SetAttachmentPoints returns false", is deze splitsing op basis van count-then-append vrijwel zeker het antwoord

Een markering maken met TPdf.CreateAnnotation

Nu het component de routering tussen Append en Set voor u regelt, is het maken van een markering (highlight) gereduceerd tot het invullen van een record. Het onderstaande voorbeeld maakt een A4-pagina en plaatst een halftransparante gele markering over een gebied van 200×20 punten; merk op dat de quad de hierboven beschreven Z-volgorde volgt, en dat Rectangle is ingesteld om de quad te omsluiten. Dit zorgt ervoor dat viewers die een hit-test uitvoeren tegen Rect zich verstandig gedragen

var
  Pdf: TPdf;
  A: TPdfAnnotation;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.CreateDocument;
    Pdf.AddPage(0, 595, 842);

    FillChar(A, SizeOf(A), 0);
    A.Subtype := anHighlight;
    A.HasColor := True;
    A.Color := clYellow;
    A.ColorAlpha := $80;                     // 50% opacity
    A.HasAttachmentPoints := True;
    A.AttachmentPoints[1].X := 50;  A.AttachmentPoints[1].Y := 700; // top-left
    A.AttachmentPoints[2].X := 250; A.AttachmentPoints[2].Y := 700; // top-right
    A.AttachmentPoints[3].X := 50;  A.AttachmentPoints[3].Y := 680; // bottom-left
    A.AttachmentPoints[4].X := 250; A.AttachmentPoints[4].Y := 680; // bottom-right
    A.Rectangle.Left := 50;  A.Rectangle.Top := 700;
    A.Rectangle.Right := 250; A.Rectangle.Bottom := 680;
    A.ContentsText := 'Highlighted region';
    Pdf.CreateAnnotation(A);

    Pdf.SaveAs('highlighted.pdf');
  finally
    Pdf.Free;
  end;
end;

Het wisselen van subtype kost één regel. anUnderline, anStrikeout en anSquiggly nemen exact dezelfde recordvorm aan, inclusief quads, omdat ISO 32000-1 alle drie behandelt als dezelfde annotatie-familie die alleen verschilt in hoe het quad-gebied is gedecoreerd. Subtypes die geen tekstmarkeringen zijn, zoals anSquare, anCircle en anText, positioneren zichzelf uitsluitend op basis van Rectangle; laat HasAttachmentPoints op False staan voor deze subtypes, en het quad-mechanisme wordt nooit uitgevoerd

Waarom compileert AttachmentPoints[0] wel in Delphi maar faalt het in FPC?

TQuadrilateralPoint is gedeclareerd als array [1..4] of TPdfPoint, een 1-gebaseerde array, en dat brengt iedereen in verwarring wiens vingers automatisch uitgaan van zero-based indexering. Als u A.AttachmentPoints[0] schrijft, compileert Delphi's dcc32 dit zonder klagen omdat range checking standaard is uitgeschakeld; tijdens runtime leest of schrijft de expressie stilletjes het geheugen net vóór de array, wat in een TPdfAnnotation-record een aangrenzend veld is. Uw markering krijgt een ongeldige hoek, of een naburig veld raakt gecorrumpeerd, zonder dat er een foutmelding optreedt. Free Pascal ving deze fout op in onze eigen demo-bronnen tijdens de Lazarus-port: fpc voert compile-time range checking uit op constante indices en wees AttachmentPoints[0..3] direct af, wat de reden is dat de off-by-one fout en de Set-versus-Append-bibliotheekfout tegelijkertijd aan het licht kwamen

Hieruit volgen twee gewoonten. Indexeer de quad met 1 tot en met 4, passend bij de hoekvolgorde in de code hierboven, en bouw uw annotatiecode ten minste eenmaal met range checking ingeschakeld ({$R+} in Delphi of een willekeurige fpc-build) voordat u deze vertrouwt. Het slagen van een standaard dcc32-build is geen bewijs dat de indices juist zijn; het bewijst alleen dat er niets is gecrasht op het geheugen dat daar toevallig aanwezig was

Quad-coördinaten ophalen uit echte tekst

Hardgecodeerde rechthoeken zijn prima voor een demo, maar markeringen in productie moeten echte glyphs volgen, en de coördinaten moeten afkomstig zijn van PDFium's tekstpaginageometrie in plaats van giswerk. De routines die worden behandeld in onze gids voor tekstextractie met de PDFium Component geven u bounding boxes per karakter in dezelfde paginacoördinatenruimte die de quads gebruiken, zodat een zoekresultaat direct wordt omgezet in hoekpunten: links van het eerste karakter, rechts van het laatste, en de boven- en onderkant uit de uitersten van de regel. Als u zelf de tekst genereert en moet weten waar regels zullen vallen voordat ze bestaan, behandelt het artikel over tekstmeting en tekstomloop het vooraf berekenen van die uitersten

Een eerlijke grens: het record TPdfAnnotation bevat een enkele TQuadrilateralPoint, dus één CreateAnnotation-aanroep schrijft één vierhoek. A selectie die zich over drie regels uitstrekt, heeft drie quads nodig (één per regel conform §12.5.6.10), en er zijn twee manieren om dat te bereiken. De eenvoudige manier is één annotatie per regel, wat overal correct rendert en de API op componentniveau behoudt. De compacte manier — één annotatie met drie quads — houdt in dat u de annotatie via het component maakt en vervolgens zelf het geëxporteerde FPDFAnnot_AppendAttachmentPoints aanroept voor de tweede en third quad. Dit werkt juist omdat Append slots aanmaakt in plaats van ze te vervangen. Probeer niet om multi-quad te bereiken via herhaalde SetAttachmentPoints-aanroepen; elke index groter dan de huidige telling retourneert gewoon false, om dezelfde reden als index 0 dat deed bij de nieuwe annotatie

Controleer na het schrijven in een echte viewer in plaats van te vertrouwen op de retourcodes: open het bestand in Acrobat of een willekeurige op PDFium gebaseerde viewer en controleer of de markering op de tekst valt, leesbaar is op de beoogde transparantie en een opslag-en-herlaadcyclus overleeft. De annotatietypen, de quad-afhandeling en de count-bewuste schrijver die hier worden getoond, maken allemaal deel uit van de standaard PDFium Component voor Delphi, C++Builder en Lazarus; de productpagina bevat de volledige API-referentie voor annotaties naast de rest van de bibliotheek