Teknisk artikel

Tekstmarkeringsannotationer med PDFium QuadPoints i Delphi

PDFium Component opretter tekstmarkeringsannotationer, altså fremhævning, understregning, gennemstregning og bølgestreg, gennem TPdf.CreateAnnotation: du sætter HasAttachmentPoints := True på TPdfAnnotation-recorden og udfylder dens AttachmentPoints-firkant, hvorefter komponenten skriver den QuadPoints-post, der er defineret i ISO 32000-1 §12.5.6.10. Det er hele API-fladen. Grunden til, at denne artikel findes, er det, der sker nedenunder, for det rå PDFium-kaldsforløb har en fejltilstand, der giver det mindst hjælpsomme symptom i hele værktøjskassen: FPDFAnnot_SetAttachmentPoints returnerer false på en nyoprettet annotation, hver eneste gang, uden fejlkode og uden fingerpeg. Dette er oprettelsessidens modstykke til vores artikel om at læse og gennemgå eksisterende annotationer, som går den anden vej gennem de samme strukturer

Fejlsøgningsscenen er altid den samme. Du opretter en fremhævningsannotation, du kalder setteren til tilknytningspunkter med indeks 0, funktionen returnerer false, og du begynder at tvivle på dine koordinater. Du ombytter punkterne, vender Y-aksen, bytter siderum ud med enhedsrum. Intet af det hjælper, for koordinaterne var aldrig problemet. Problemet er indekssemantikken i C-API'et, og når du først har set den, er rettelsen på to linjer

Hvad QuadPoints betyder i ISO 32000-1

QuadPoints er et array af 8×n tal, der beskriver n firkanter, og ISO 32000-1 §12.5.6.10 kræver det på enhver tekstmarkeringsannotation: hver firkant markerer et ord eller en gruppe af sammenhængende ord, som fremhævningen, understregningen eller gennemstregningen gælder for. Annotationens Rect-post findes stadig, men for markeringsundertyper afgrænser den kun området; firkanterne er det, gengiveren rent faktisk maler. En firkant frem for et rektangel, fordi tekst kan være roteret eller forskudt, så de fire hjørner gemmes som fire uafhængige punkter: x1 y1 x2 y2 x3 y3 x4 y4

Rækkefølgen af de fire punkter er der, hvor specifikationen og den installerede base skilles ad. Spec-teksten beskriver punkterne som en optegning af firkanten mod uret, men Adobes egen gengiver har altid fortolket dem i et Z-mønster i stedet: først den øverste kant fra venstre mod højre, derefter den nederste kant fra venstre mod højre. Fordi enhver forfatter testede mod Acrobat, følger stort set enhver gengiver, PDFium inklusive, Z-mønsteret, og filer, der følger specifikationens bogstavelige ordlyd, gengives som sammenklappede eller forvredne fremhævninger i visse fremvisere. PDFiums FS_QUADPOINTSF-struct koder præcis denne konvention: (x1,y1) er øverste venstre hjørne, (x2,y2) øverste højre, (x3,y3) nederste venstre, (x4,y4) nederste højre, i sidekoordinater hvor Y vokser opad. Følg den rækkefølge, så er du færdig; gengivere er overbærende med mange ting, men en sammenblandet firkant er ikke en af dem

Geometridiagram over QuadPoints i PDF-sidekoordinater, der viser Z-ordens hjørnenummerering TL, TR, BL, BR brugt af PDFium-tekstmarkeringsannotationer
PDFium forventer QuadPoints i Z-orden, øverst til venstre til øverst til højre og derefter nederst til venstre til nederst til højre, i sidekoordinater hvor Y vokser opad

Hvorfor returnerer FPDFAnnot_SetAttachmentPoints false?

FPDFAnnot_SetAttachmentPoints fejler på en ny annotation, fordi dens kontrakt er at erstatte firkanten på et givet indeks, og en nyoprettet annotation har nul firkanter at erstatte. Signaturen tager et annotationshandle, et quad_index og punkterne; indeks 0 betyder ikke "den første plads, oprettet efter behov", det betyder "den eksisterende firkant nummer 0", og når FPDFAnnot_CountAttachmentPoints melder 0, findes den firkant ikke, og kaldet returnerer false. Funktionen, der opretter en plads, er FPDFAnnot_AppendAttachmentPoints. Enhver annotation oprettet gennem FPDFPage_CreateAnnot starter med tælleren på nul, så oprettelsesstien skal kalde Append først, og kun efterfølgende opdateringer må kalde Set

Det bed PDFium Component selv. Til og med v1.79.0 hardkodede den interne rutine, som CreateAnnotation og SetAnnotation deler, FPDFAnnot_SetAttachmentPoints(Annotation, 0, ...), hvilket var korrekt ved opdatering af en eksisterende markeringsannotation og garanteret ville fejle for en ny, og viste sig som en EPdfException med beskeden 'Cannot set attachment points'. Rettelsen, der udkom i v1.79.1, forgrener sig på tælleren

// Inde i komponentens annotationsskriver (v1.79.1+):
// en ny annotation har endnu ingen firkantpladser, så Append opretter
// den første; Set erstatter kun en plads, der allerede findes
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');

Det samme mønster gælder, hvis du kalder de eksporterede C-funktioner direkte, hvilket komponenten lader dig gøre, da alle FPDFAnnot_*-indgangspunkter er eksponeret i PDFium.pas. Hver gang du holder et FPDF_ANNOTATION-handle og vil skrive firkanter, så spørg FPDFAnnot_CountAttachmentPoints først og dirigér derefter. Søger du efter "FPDFAnnot_SetAttachmentPoints returns false", er denne tæl-så-append-forgrening næsten helt sikkert dit svar

Sådan opretter du en fremhævning med TPdf.CreateAnnotation

Når komponenten klarer Append-versus-Set-dirigeringen for dig, reduceres det at oprette en fremhævning til at udfylde en record. Eksemplet nedenfor opretter en A4-side og lægger en halvgennemsigtig gul fremhævning over et område på 200×20 punkter; bemærk, at firkanten følger den Z-orden, der er beskrevet ovenfor, og at Rectangle er sat til at omslutte firkanten, hvilket holder fremvisere, der hit-tester mod Rect, i fornuftig opførsel

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 % opacitet
    A.HasAttachmentPoints := True;
    A.AttachmentPoints[1].X := 50;  A.AttachmentPoints[1].Y := 700; // øverste venstre
    A.AttachmentPoints[2].X := 250; A.AttachmentPoints[2].Y := 700; // øverste højre
    A.AttachmentPoints[3].X := 50;  A.AttachmentPoints[3].Y := 680; // nederste venstre
    A.AttachmentPoints[4].X := 250; A.AttachmentPoints[4].Y := 680; // nederste højre
    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;

At skifte undertype koster én linje. anUnderline, anStrikeout og anSquiggly tager den identiske recordform, firkanter og det hele, fordi ISO 32000-1 behandler alle fire som den samme annotationsfamilie, kun adskilt af, hvordan firkantområdet dekoreres. Undertyper, der ikke er tekstmarkering, såsom anSquare, anCircle og anText, placerer sig selv ud fra Rectangle alene; lad HasAttachmentPoints stå på False for dem, og firkantmaskineriet kører aldrig

Hvorfor kompilerer AttachmentPoints[0] i Delphi, men fejler i FPC?

TQuadrilateralPoint er erklæret som array [1..4] of TPdfPoint, et 1-baseret array, og det snubler enhver, hvis fingre som standard indekserer fra nul. Skriv A.AttachmentPoints[0], og Delphis dcc32 kompilerer det uden at kny, fordi områdekontrol er slået fra som standard; ved kørsel læser eller skriver udtrykket lydløst hukommelsen lige før arrayet, hvilket i en TPdfAnnotation-record er et tilstødende felt. Din fremhævning får ét skraldehjørne, eller et nabofelt bliver ødelagt, og intet rejser en fejl. Free Pascal fangede præcis denne fejl i vores egne demokilder under Lazarus-porteringen: fpc udfører områdekontrol på oversættelsestidspunktet for konstante indekser og afviste AttachmentPoints[0..3] blankt, og det er sådan, off-by-one-fejlen og Set-versus-Append-fejlen i biblioteket blev gravet frem sammen

To vaner følger heraf. Indeksér firkanten fra 1 til 4, svarende til hjørnerækkefølgen i koden ovenfor, og byg din annotationskode mindst én gang med områdekontrol slået til, enten {$R+} i Delphi eller en vilkårlig fpc-byggeproces, før du stoler på den. At en standard dcc32-byggeproces går igennem, er ikke bevis for, at indekserne er rigtige; det er kun bevis for, at intet gik ned på den hukommelse, der tilfældigvis lå der

Sådan får du firkantkoordinater fra rigtig tekst

Hardkodede rektangler er fine til en demo, men fremhævninger i produktion optegner faktiske glyffer, og koordinaterne bør komme fra PDFiums tekstsidegeometri frem for gætteri. Rutinerne, der er dækket i vores guide til tekstudtræk med PDFium Component, giver dig afgrænsningsbokse pr. tegn i det samme sidekoordinatrum, som firkanterne bruger, så et søgetræf omsættes direkte til hjørnepunkter: venstre for det første tegn, højre for det sidste, top og bund fra linjens udstrækning. Genererer du selv teksten og har brug for at vide, hvor linjerne falder, før de findes, dækker artiklen om tekstmåling og linjeombrydning beregningen af den udstrækning på forhånd

Én ærlig grænse: TPdfAnnotation-recorden bærer et enkelt TQuadrilateralPoint, så ét CreateAnnotation-kald skriver én firkant. En markering, der spænder over tre linjer, har brug for tre firkanter, én pr. linje, jf. §12.5.6.10, og du har to veje dertil. Den simple vej er én annotation pr. linje, som gengives korrekt overalt og holder sig til API'et på komponentniveau. Den kompakte vej, én annotation der bærer tre firkanter, betyder, at du opretter annotationen gennem komponenten og derefter selv kalder den eksporterede FPDFAnnot_AppendAttachmentPoints for den anden og tredje firkant, hvilket virker netop fordi Append opretter pladser i stedet for at erstatte dem. Prøv ikke at nå flere firkanter gennem gentagne SetAttachmentPoints-kald; ethvert indeks ud over den aktuelle tæller returnerer bare false, af samme grund som indeks 0 gjorde på den friske annotation

Flerlinjet fremhævning bygget af tre firkanter tilføjet én for én til en enkelt PDFium-annotation oprettet i Delphi
En markering, der spænder over tre linjer, bliver til én annotation med tre firkanter, bygget med AppendAttachmentPoints-kald, der får antallet af pladser til at vokse

Efter skrivningen skal du verificere i en rigtig fremviser frem for at stole på returkoderne: åbn filen i Acrobat eller en vilkårlig PDFium-baseret fremviser og bekræft, at markeringen lander på teksten, læses ved den tilsigtede opacitet og overlever en gem-og-genindlæs-rundtur. Annotationstyperne, firkanthåndteringen og den tællebevidste skriver, der er vist her, er alle en del af standardudgaven af PDFium Component til Delphi, C++Builder og Lazarus; produktsiden rummer den fulde annotations-API-reference sammen med resten af biblioteket