Teknisk artikel

Tekstmarkeringsannoteringer med PDFium QuadPoints i Delphi

PDFium Component opretter tekstmarkeringsannoteringer — det vil sige fremhævning (highlight), understregning (underline), gennemstregning (strikeout) og krøllet understregning (squiggly) — via TPdf.CreateAnnotation: Du indstiller HasAttachmentPoints := TrueTPdfAnnotation-posten (record) 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. Årsagen til denne artikel er dog, hvad der sker under overfladen, for den rå PDFium-kaldskæde har en fejltilstand, der producerer det mindst hjælpsomme symptom i værktøjskassen: FPDFAnnot_SetAttachmentPoints returnerer false på en nyligt oprettet annotering, hver eneste gang, uden nogen fejlkode og uden hint. Dette er den oprettelsesmæssige ledsager til vores artikel om læsning og gennemgang af eksisterende annoteringer, som går den anden vej gennem de samme strukturer

Fejlfindingsscenariet er altid det samme. Du opretter en fremhævningsannotering, du kalder attachment-points setteren med indeks 0, funktionen returnerer false, og du begynder at tvivle på dine koordinater. Du transponerer punkterne, vender Y-aksen, bytter sideplads ud med enhedsplads. Intet af det hjælper, for koordinaterne var aldrig problemet. Problemet er indekssemantikken i C API'en, og når først du ser 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 (quadrilaterals), og ISO 32000-1 §12.5.6.10 kræver det på enhver tekstmarkeringsannotering: Hver firkant markerer et ord eller en gruppe af sammenhængende ord, som fremhævningen, understregningen eller gennemstregningen gælder for. Annoteringens Rect-post eksisterer stadig, men for markeringstyper afgrænser den kun området; firkanterne er det, som gengiveren (renderer) rent faktisk tegner. En firkant frem for et rektangel anvendes, fordi tekst kan roteres eller skæres, så de fire hjørner gemmes som fire uafhængige punkter: x1 y1 x2 y2 x3 y3 x4 y4

Rækkefølgen af disse fire punkter er der, hvor specifikationen og den etablerede praksis skilles. Specifikationsteksten beskrives punkterne som tegnet 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, og derefter den nederste kant fra venstre mod højre. Fordi alle udviklere testede mod Acrobat, følger stort set alle gengivere — inklusive PDFium — Z-mønsteret, og filer, der følger specifikationens bogstavelige ordlyd, vises som kollapsede eller snoede fremhævninger i visse fremvisere. PDFiums FS_QUADPOINTSF-struktur koder præcis denne konvention: (x1,y1) is ø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 denne rækkefølge; gengivere er overbærende med mange ting, men en rodet firkant er ikke en af dem

Hvorfor returnerer FPDFAnnot_SetAttachmentPoints false?

FPDFAnnot_SetAttachmentPoints fejler på en ny annotering, fordi dens kontrakt er at erstatte firkanten på et givet indeks, og en nyligt oprettet annotering har nul firkanter at erstatte. Signaturen modtager et annoteringshåndtag, et quad_index og punkterne; indeks 0 betyder ikke "det første felt, opret det om nødvendigt", det betyder "den eksisterende firkant nummer 0", og når FPDFAnnot_CountAttachmentPoints rapporterer 0, findes der ikke en sådan firkant, og kaldet returnerer false. Funktionen, der opretter et felt, er FPDFAnnot_AppendAttachmentPoints. Enhver annotering, der oprettes via FPDFPage_CreateAnnot, starter med et antal på nul, så oprettelsesstien skal kalde Append først, og endast efterfølgende opdateringer må kalde Set

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

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 tilgængelige i PDFium.pas. Hver gang du har et FPDF_ANNOTATION-håndtag og ønsker at skrive firkanter, så spørg FPDFAnnot_CountAttachmentPoints først og forgrene derefter. Hvis du leder efter en løsning på, hvorfor "FPDFAnnot_SetAttachmentPoints returnerer false", er denne tælle-og-tilføje-forgrening næsten med sikkerhed svaret

Opret en fremhævning med TPdf.CreateAnnotation

Når komponenten håndterer forgreningen mellem Append og Set for dig, reduceres oprettelsen af en fremhævning til blot at udfylde en post (record). Eksemplet nedenfor opretter en A4-side og placerer en halvgennemsigtig gul fremhævning over et område på 200×20 punkter; bemærk, at firkanten følger Z-rækkefølgen beskrevet ovenfor, og at Rectangle er indstillet til at omslutte firkanten, hvilket sikrer, at fremvisere, der udfører hit-test mod Rect, fungerer fornuftigt

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;

At skifte undertype koster en enkelt linje. anUnderline, anStrikeout, og anSquiggly bruger den identiske post-form, firkanter og det hele, fordi ISO 32000-1 behandler alle fire som den samme annoteringsfamilie, der kun adskiller sig ved, hvordan firkantområdet er dekoreret. Undertyper, der ikke er tekstmarkeringer, såsom anSquare, anCircle, og anText, positionerer sig udelukkende ud fra Rectangle; lad HasAttachmentPoints forblive False for disse, hvorefter firkant-mekanikken aldrig afvikles

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 snyder enhver, hvis fingre som standard søger mod nul-baseret indeksering. Skriver du A.AttachmentPoints[0], vil Delphis dcc32 kompilere det uden klager, fordi grænsekontrol er slået fra som standard; under kørslen vil udtrykket stiltiende læse eller skrive i hukommelsen lige før arrayet, hvilket i en TPdfAnnotation-post er et tilstødende felt. Din fremhævning får et forkert hjørne, eller et nabofelt bliver beskadiget, uden at der udløses fejl. Free Pascal fangede netop denne fejl i vores egne demokilder under Lazarus-porteringen: fpc udfører grænsekontrol på kompileringstidspunktet for konstante indekser og afviste AttachmentPoints[0..3] fuldstændigt, hvilket var måden, hvorpå både off-by-one-fejlen og biblioteksfejlen med Set vs Append blev afsløret sammen

To vaner følger: Indekser firkanten fra 1 til 4, matchende hjørnerækkefølgen i koden ovenfor, og opbyg din annoteringskode mindst én gang med grænsekontrol aktiveret (enten {$R+} i Delphi eller en fpc-opbygning), før du stoler på den. At en standard dcc32-opbygning lykkes, er ikke bevis på, at indekserne er rigtige; det er kun bevis på, at intet brød ned på den hukommelse, der tilfældigvis lå der

Indhentning af firkant-koordinater fra reel tekst

Hårdtkodede rektangler er fine til en demo, men fremhævninger i produktion skal følge faktiske tegn, og koordinaterne bør stamme fra PDFiums tekstsidegeometri frem for gætteri. De rutiner, der dækkes i vores guide til tekstekstraktion med PDFium Component, giver dig omridsbokse (bounding boxes) pr. tegn i det samme sidekoordinatrum, som firkanterne bruger, så et søgeresultat konverteres direkte til hjørnepunkter: Venstre for det første tegn, højre for det sidste samt top og bund fra linjens udbredelse. Hvis du selv genererer teksten og har brug for at vide, hvor linjerne vil falde, før de eksisterer, dækker artiklen om tekstmåling og ordombrydning beregningen af disse udbredelser på herfor

En ærlig grænse: TPdfAnnotation-posten bærer et enkelt TQuadrilateralPoint, så ét CreateAnnotation-kald skriver én firkant. En markering, der strækker sig over tre linjer, kræver tre firkanter — én pr. linje i henhold til §12.5.6.10 — og du har to måder at opnå dette på. Den enkle måde er én annotering pr. linje, hvilket vises korrekt overalt og bevarer API'en på komponentniveau. Den kompakte måde — én annotering, der bærer tre firkanter — betyder, at du opretter annoteringen via komponenten og derefter selv kalder den eksporterede FPDFAnnot_AppendAttachmentPoints for den anden og tredje firkant, hvilket fungerer netop fordi Append opretter felter frem for at erstatte dem. Forsøg ikke at opnå flere firkanter via gentagne SetAttachmentPoints-kald; ethvert indeks ud over det aktuelle antal returnerer blot false af samme grund, som indeks 0 gjorde på den nye annotering

Efter skrivning skal du verificere i en rigtig fremviser frem for at stole på returkoderne: Åbn filen i Acrobat eller en hvilken som helst PDFium-baseret fremviser, og bekræft, at markeringen lander på teksten, har den tilsigtede uigennemsigtighed og overlever en gemme-og-genindlæse-rundtur. Annoteringstyperne, firkanthåndteringen og den antalsbevidste skriver, der vises her, er alle en del af standarden PDFium Component til Delphi, C++Builder og Lazarus; produktsiden indeholder hele annoterings-API-referencen sammen med resten af biblioteket