Teknisk artikkel

Tekstmarkeringsannotasjoner med PDFium QuadPoints i Delphi

PDFium Component oppretter tekstmarkeringsannotasjoner, altså utheving, understreking, gjennomstreking og bølgestrek, gjennom TPdf.CreateAnnotation: du setter HasAttachmentPoints := True på TPdfAnnotation-recorden og fyller dens AttachmentPoints-firkant, og komponenten skriver QuadPoints-oppføringen definert i ISO 32000-1 §12.5.6.10. Det er hele API-flaten. Grunnen til at denne artikkelen finnes, er det som skjer under den, for den rå PDFium-kallkjeden har en feilmodus som gir det minst hjelpsomme symptomet i hele verktøykassen: FPDFAnnot_SetAttachmentPoints returnerer usann på en nyopprettet annotasjon, hver eneste gang, uten feilkode og uten hint. Dette er opprettelsessidens følgesvenn til artikkelen vår om å lese og gjennomgå eksisterende annotasjoner, som går den andre veien gjennom de samme strukturene

Feilsøkingsscenen er alltid den samme. Du oppretter en uthevingsannotasjon, du kaller setteren for festepunkter med indeks 0, funksjonen returnerer usann, og du begynner å tvile på koordinatene dine. Du bytter om punktene, snur Y-aksen, bytter sideområde mot enhetsområde. Ingenting hjelper, for koordinatene var aldri problemet. Problemet er indekssemantikken i C-API-et, og når du først ser den, er fiksen to linjer

Hva QuadPoints betyr i ISO 32000-1

QuadPoints er et array med 8×n tall som beskriver n firkanter, og ISO 32000-1 §12.5.6.10 krever det på hver eneste tekstmarkeringsannotasjon: hver firkant markerer et ord eller en gruppe sammenhengende ord som uthevingen, understrekingen eller gjennomstrekingen gjelder for. Annotasjonens Rect-oppføring finnes fortsatt, men for markeringsundertyper avgrenser den bare området; det er firkantene gjengiveren faktisk maler. En firkant i stedet for et rektangel fordi tekst kan være rotert eller skjevstilt, så de fire hjørnene lagres som fire uavhengige punkter: x1 y1 x2 y2 x3 y3 x4 y4

Rekkefølgen på de fire punktene er der spesifikasjonen og den installerte massen skiller lag. Spesifikasjonsteksten beskriver punktene som at de tegner firkanten mot klokken, men Adobes egen gjengiver har alltid tolket dem i et Z-mønster i stedet: først overkanten fra venstre mot høyre, så underkanten fra venstre mot høyre. Fordi hver forfatter testet mot Acrobat, følger i praksis hver gjengiver, PDFium inkludert, Z-mønsteret, og filer som følger spesifikasjonens bokstavelige ordlyd gjengis som sammenklappede eller vridde uthevinger i enkelte visere. PDFiums FS_QUADPOINTSF-struktur koder nøyaktig denne konvensjonen: (x1,y1) er øverste venstre hjørne, (x2,y2) øverste høyre, (x3,y3) nederste venstre, (x4,y4) nederste høyre, i sidekoordinater der Y vokser oppover. Følg den rekkefølgen og bli ferdig; gjengivere er romslige på mange punkter, men en rotet firkant er ikke ett av dem

Geometridiagram over QuadPoints i PDF-sidekoordinater som viser hjørnenummereringen i Z-rekkefølge, TL, TR, BL, BR, som PDFium-tekstmarkeringsannotasjoner bruker
PDFium forventer QuadPoints i Z-rekkefølge, øverst til venstre til øverst til høyre og så nederst til venstre til nederst til høyre, i sidekoordinater der Y vokser oppover

Hvorfor returnerer FPDFAnnot_SetAttachmentPoints usann?

FPDFAnnot_SetAttachmentPoints feiler på en ny annotasjon fordi kontrakten dens er å erstatte firkanten på en gitt indeks, og en nyopprettet annotasjon har null firkanter å erstatte. Signaturen tar et annotasjonshåndtak, en quad_index og punktene; indeks 0 betyr ikke «den første plassen, opprett den om nødvendig», det betyr «den eksisterende firkanten nummer 0», og når FPDFAnnot_CountAttachmentPoints melder 0, finnes ingen slik firkant, og kallet returnerer usann. Funksjonen som oppretter en plass, er FPDFAnnot_AppendAttachmentPoints. Hver annotasjon opprettet gjennom FPDFPage_CreateAnnot starter med antallet null, så opprettelsesstien må kalle Append først, og bare påfølgende oppdateringer får kalle Set

Dette bet PDFium Component selv. Til og med v1.79.0 hardkodet den interne rutinen som CreateAnnotation og SetAnnotation deler, FPDFAnnot_SetAttachmentPoints(Annotation, 0, ...), som var riktig for å oppdatere en eksisterende markeringsannotasjon og garantert ville feile for en ny, og viste seg som en EPdfException med meldingen 'Cannot set attachment points'. Fiksen, levert i v1.79.1, forgrener seg på antallet

// Inne i komponentens annotasjonsskriver (v1.79.1+):
// en ny annotasjon har ingen firkantplasser ennå, så Append oppretter
// den første; Set erstatter bare en plass som allerede finnes
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ønsteret gjelder hvis du kaller de eksporterte C-funksjonene direkte, noe komponenten lar deg gjøre siden alle FPDFAnnot_*-inngangspunkter er eksponert i PDFium.pas. Hver gang du holder et FPDF_ANNOTATION-håndtak og vil skrive firkanter, spør FPDFAnnot_CountAttachmentPoints først og rut deretter. Leter du etter «FPDFAnnot_SetAttachmentPoints returnerer usann», er denne tell-så-føy-til-forgreningen nesten helt sikkert svaret ditt

Å lage en utheving med TPdf.CreateAnnotation

Når komponenten gjør rutingen mellom Append og Set for deg, reduseres det å lage en utheving til å fylle en record. Eksempelet nedenfor oppretter en A4-side og slipper en halvgjennomsiktig gul utheving over et område på 200×20 punkter; merk at firkanten følger Z-rekkefølgen beskrevet ovenfor, og at Rectangle er satt til å omslutte firkanten, noe som holder visere som treffprøver mot Rect fornuftige

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 % dekkevne
    A.HasAttachmentPoints := True;
    A.AttachmentPoints[1].X := 50;  A.AttachmentPoints[1].Y := 700; // øverst til venstre
    A.AttachmentPoints[2].X := 250; A.AttachmentPoints[2].Y := 700; // øverst til høyre
    A.AttachmentPoints[3].X := 50;  A.AttachmentPoints[3].Y := 680; // nederst til venstre
    A.AttachmentPoints[4].X := 250; A.AttachmentPoints[4].Y := 680; // nederst til høyre
    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;

Å bytte undertype koster én linje. anUnderline, anStrikeout og anSquiggly tar den identiske recordformen, firkanter og alt, fordi ISO 32000-1 behandler alle fire som den samme annotasjonsfamilien, bare skilt av hvordan firkantområdet dekoreres. Undertyper som ikke er tekstmarkering, som anSquare, anCircle og anText, plasserer seg ut fra Rectangle alene; la HasAttachmentPoints stå på False for dem, så kjører firkantmaskineriet aldri

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

TQuadrilateralPoint er erklært som array [1..4] of TPdfPoint, et 1-basert array, og det snubler alle som har fingre som faller tilbake på nullbasert indeksering. Skriv A.AttachmentPoints[0], og Delphis dcc32 kompilerer det uten å klage, fordi områdekontroll er av som standard; ved kjøretid leser eller skriver uttrykket stille til minnet like før arrayet, som i en TPdfAnnotation-record er et tilstøtende felt. Uthevingen din får ett søppelhjørne, eller et nabofelt blir ødelagt, og ingenting reiser et unntak. Free Pascal fanget nøyaktig denne feilen i våre egne demokilder under Lazarus-porteringen: fpc utfører områdekontroll ved kompileringstid på konstante indekser og avviste AttachmentPoints[0..3] rett ut, som er slik av-med-én-feilen og bibliotekfeilen med Set mot Append ble gravd frem sammen

To vaner følger av dette. Indeksér firkanten 1 til 4, i tråd med hjørnerekkefølgen i koden ovenfor, og bygg annotasjonskoden din minst én gang med områdekontroll slått på, enten {$R+} i Delphi eller en hvilken som helst fpc-bygging, før du stoler på den. At en standard dcc32-bygging går gjennom, er ikke bevis på at indeksene er riktige; det er bare bevis på at ingenting krasjet på det minnet som tilfeldigvis lå der

Å hente firkantkoordinater fra virkelig tekst

Hardkodede rektangler er greit for en demo, men uthevinger i produksjon følger faktiske glyffer, og koordinatene bør komme fra PDFiums tekstsidegeometri i stedet for fra gjetting. Rutinene som dekkes i guiden vår til tekstuttrekk med PDFium Component gir deg avgrensningsbokser per tegn i det samme sidekoordinatrommet firkantene bruker, så et søketreff konverteres direkte til hjørnepunkter: venstre for det første tegnet, høyre for det siste, topp og bunn fra linjens utstrekning. Genererer du teksten selv og trenger å vite hvor linjene vil falle før de finnes, dekker artikkelen om tekstmåling og orddeling hvordan du beregner den utstrekningen på forhånd

Én ærlig grense: TPdfAnnotation-recorden bærer én enkelt TQuadrilateralPoint, så ett CreateAnnotation-kall skriver én firkant. Et utvalg som spenner over tre linjer trenger tre firkanter, én per linje, jf. §12.5.6.10, og du har to veier dit. Den enkle veien er én annotasjon per linje, som gjengis riktig overalt og holder deg på API-et på komponentnivå. Den kompakte veien, én annotasjon som bærer tre firkanter, innebærer å opprette annotasjonen gjennom komponenten og så kalle den eksporterte FPDFAnnot_AppendAttachmentPoints selv for den andre og tredje firkanten, noe som virker nettopp fordi Append oppretter plasser i stedet for å erstatte dem. Ikke prøv å nå flere firkanter gjennom gjentatte SetAttachmentPoints-kall; hver indeks forbi gjeldende antall returnerer bare usann, av samme grunn som indeks 0 gjorde på den ferske annotasjonen

Utheving over flere linjer bygget av tre firkanter føyd til én for én på én enkelt PDFium-annotasjon opprettet i Delphi
Et utvalg som spenner over tre linjer blir én annotasjon som bærer tre firkanter, bygget med AppendAttachmentPoints-kall som øker antallet plasser

Etter skrivingen bør du verifisere i en ekte viser i stedet for å stole på returkodene: åpne filen i Acrobat eller en hvilken som helst PDFium-basert viser og bekreft at markeringen lander på teksten, leses med den tiltenkte dekkevnen og overlever en rundtur med lagring og ny innlasting. Annotasjonstypene, firkanthåndteringen og den antallsbevisste skriveren som er vist her, er alle en del av standard PDFium Component for Delphi, C++Builder og Lazarus; produktsiden bærer den fullstendige annotasjons-API-referansen sammen med resten av biblioteket