Tehnički članak

Označavanje teksta sa PDFium QuadPoints u Delphiju

PDFium Component stvara zabeleške za označavanje teksta, što znači isticanje (highlight), podcrtavanje (underline), precrtavanje (strikeout) i valovito podcrtavanje (squiggly), putem metode TPdf.CreateAnnotation: postavite HasAttachmentPoints := True na zapis TPdfAnnotation i popunite njegov četvorougao AttachmentPoints, a komponenta zapisuje unos QuadPoints definisan u ISO 32000-1 §12.5.6.10. To je cela površina API-ja. Razlog zašto ovaj članak postoji je ono što se događa ispod toga, jer sirovi PDFium lanac poziva ima način kvara koji proizvodi najmanje koristan simptom u celom alatu: FPDFAnnot_SetAttachmentPoints vraća false na sveže stvorenoj zabelešci, svaki put, bez koda greške i bez ikakvog nagoveštaja. Ovo je prateći članak za stranu stvaranja našem članku o čitanju i pregledanju postojećih zabeleški, koji prolazi u drugom smeru kroz iste strukture

Scena uklanjanja pogrešaka uvek je ista. Stvorite zabelešku isticanja, pozovete postavljač spojnih tačaka (attachment-points setter) sa indeksom 0, funkcija vrati false, i počnete da sumnjate u svoje koordinate. Transponujete tačke, okrenete osu Y, zamenite prostor stranice za prostor uređaja. Ništa od toga ne pomaže jer koordinate nikada nisu bile problem. Problem je u indeksnoj semantici C API-ja, a kada je jednom uočite, ispravka je u dva reda

Šta QuadPoints znače u ISO 32000-1

QuadPoints je niz od 8×n brojeva koji opisuju n četvorouglova, a ISO 32000-1 §12.5.6.10 zahteva ga na svakoj zabelešci za označavanje teksta: svaki četvorougao označava reč ili skupinu susednih reči na koje se isticanje, podcrtavanje ili precrtavanje odnosi. Unos Rect za zabelešku i dalje postoji, ali za podtipove označavanja on samo omeđuje regiju; četvorouglovi (quads) su ono što renderer zapravo iscrtava. Četvorougao se koristi umesto pravougaonika jer tekst može biti rotiran ili nagnut, pa se četiri ugla čuvaju kao četiri nezavisne tačke: x1 y1 x2 y2 x3 y3 x4 y4

Redosled tih četiriju tačaka je mesto gde se specifikacija i instalirana baza korisnika razilaze. Tekst specifikacije opisuje tačke kao praćenje četvorougla u smeru suprotnom od kazaljke na satu, ali Adobeov sopstveni renderer uvek ih je tumačio u Z uzorku: prvo gornja ivica sleva nadesno, a zatim donja ivica sleva nadesno. Budući da su svi autori testirali u odnosu na Acrobat, praktički svaki renderer, uključujući i PDFium, prati Z uzorak, a datoteke koje prate doslovni tekst specifikacije iscrtavaju se kao urušena ili uvrnuta isticanja u nekim preglednicima. PDFiumova struktura FS_QUADPOINTSF kodira upravo tu konvenciju: (x1,y1) je gornji levi ugao, (x2,y2) gornji desni, (x3,y3) donji levi, (x4,y4) donji desni, u koordinatama stranice gde Y raste prema gore. Pratite taj redosled i gotovi ste; rendereri su popustljivi prema mnogim stvarima, ali zbrkani četvorougao nije jedna od njih

Zašto FPDFAnnot_SetAttachmentPoints vraća false?

FPDFAnnot_SetAttachmentPoints ne uspeva na novoj zabelešci jer je njegov ugovor zamena četvorougla na zadatom indeksu, a sveže stvorena zabeleška ima nula četvorouglova za zamenu. Potpis funkcije prima ručku zabeleške, quad_index i tačke; indeks 0 ne znači 'prvi utor, stvori ga ako je potrebno', nego znači 'postojeći četvorougao broj 0', a kada FPDFAnnot_CountAttachmentPoints javi 0, takav četvorougao ne postoji i poziv vraća false. Funkcija koja stvara utor je FPDFAnnot_AppendAttachmentPoints. Svaka zabeleška stvorena putem FPDFPage_CreateAnnot započinje sa brojem nula, pa staza stvaranja mora najpre pozvati Append, a tek naknadna ažuriranja mogu pozvati Set

To je pogodilo i samu komponentu PDFium Component. Do verzije v1.79.0 interna rutina koju dele CreateAnnotation i SetAnnotation imala je čvrsto kodirano FPDFAnnot_SetAttachmentPoints(Annotation, 0, ...), što je bilo tačno za ažuriranje postojeće zabeleške označavanja i zagarantovano neuspešno za novu, očitujući se kao EPdfException sa porukom 'Cannot set attachment points'. Ispravak, isporučen u verziji v1.79.1, grana se na osnovu broja

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

Isti uzorak vredi i ako direktno pozivate izvezene C funkcije, što vam komponenta dopušta jer su sve ulazne tačke FPDFAnnot_* izložene u datoteci PDFium.pas. Kad god držite ručku FPDF_ANNOTATION i želite pisati četvorouglove, najpre pitajte FPDFAnnot_CountAttachmentPoints i usmerite poziv u skladu sa tim. Ako tražite zašto 'FPDFAnnot_SetAttachmentPoints vraća false', ovo grananje sa brojenjem i appendanjem je gotovo sigurno vaš odgovor

Stvaranje isticanja pomoću TPdf.CreateAnnotation

Budući da komponenta umesto vas obavlja usmeravanje Append-naspram-Set, stvaranje isticanja svodi se na popunjavanje zapisa. Primer u nastavku stvara A4 stranicu i postavlja poluprozirno žuto isticanje preko regije od 200×20 tačaka; imajte na umu da četvorougao prati gore opisani Z redosled i da je Rectangle postavljen tako da obuhvati četvorougao, što održava razumno ponašanje u preglednicima koji testiraju pogotke u odnosu na Rect

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;

Promena podtipova košta jedan redak. anUnderline, anStrikeout i anSquiggly uzimaju identičan oblik zapisa, četvorouglove i sve ostalo, jer ISO 32000-1 tretira sva tri kao istu porodicu zabeleški koja se razlikuje samo po načinu ukrašavanja regije četvorougla. Podtipovi koji nisu označavanje teksta, kao što su anSquare, anCircle i anText, pozicioniraju se isključivo na osnovu Rectangle; za njih ostavite HasAttachmentPoints na False i mehanizam za četvorouglove se nikada neće pokrenuti

Zašto se AttachmentPoints[0] kompajlira u Delphiju, ali ne uspeva u FPC-u?

TQuadrilateralPoint je deklarisan kao array [1..4] of TPdfPoint, što je 1-bazirani niz, i to zbunjuje svakoga čiji prsti automatski kreću prema nultom indeksiranju. Napišite A.AttachmentPoints[0] i Delphijev dcc32 će to kompajlirati bez pritužbi, jer je provera raspona (range checking) prema zadanim postavkama isključena; u vreme izvršavanja (runtime) izraz tiho čita ili piše u memoriju neposredno pre niza, što je u zapisu TPdfAnnotation susedno polje. Vaše isticanje dobiva jedan pogrešan ugao ili se susedno polje ošteti, a ništa ne javlja grešku. Free Pascal je uhvatio upravo ovu grešku u našim sopstvenim demo izvorima tokom porta za Lazarus: fpc izvodi proveru raspona u vreme kompajliranja na konstantnim indeksima i odbio je AttachmentPoints[0..3] direktno, što je ujedno otkrilo i problem sa indeksom i grešku biblioteke Set-naspram-Append

Slede dve navike. Indeksirajte četvorougao od 1 do 4, podudarajući se sa redosledom uglova u gornjem kodu, i izgradite svoj kod za zabeleške barem jednom sa uključenom proverom raspona, bilo pomoću {$R+} u Delphiju ili u bilo kojoj fpc izvedbi, pre nego što mu poverujete. Prolazak zadane dcc32 kompilacije nije dokaz da su indeksi tačni; to je samo dokaz da se ništa nije srušilo na memoriji koja se slučajno tamo zatekla

Dohvatanje koordinata četvorougla iz stvarnog teksta

Rutine pokrivene u našem vodiču za ekstrakciju teksta sa PDFium komponentom daju vam okvire (bounding boxes) po znaku u istom koordinatnom prostoru stranice koji koriste četvorouglovi, tako da se pogodak pretrage direktno pretvara u ugaone tačke: levo od prvog znaka, desno od zadnjeg, gornja i donja strana iz raspona reda. Ako sami generišete tekst i trebate znati gde će redovi pasti pre nego što postoje, članak o merenju teksta i prelomu reči pokriva izračun tih raspona unapred

Jedna poštena granica: zapis TPdfAnnotation nose jedan TQuadrilateralPoint, pa jedan poziv CreateAnnotation zapisuje jedan četvorougao. Odabir koji se proteže kroz tri reda treba tri četvorougla, po jedan za svaki redak, prema §12.5.6.10, a imate dva načina da to postignete. Jednostavan način je jedna zabeleška po redu, što se svugdje ispravno prikazuje i zadržava API na nivou komponente. Kompaktan način, jedna zabeleška koja nosi tri četvorougla, znači stvaranje zabeleške kroz komponentu, a zatim samostalno pozivanje izvezene funkcije FPDFAnnot_AppendAttachmentPoints za drugi i treći četvorougao, što radi upravo zato što Append stvara utore umesto da ih zamenjuje. Ne pokušavajte postići višestruki četvorougao kroz ponovljene pozive SetAttachmentPoints; svaki indeks iznad trenutnog broja jednostavno vraća false, iz istog razloga iz kojeg je indeks 0 to učinio na sveže stvorenoj zabelešci

Nakon pisanja, proverite u stvarnom pregledniku umesto da verujete povratnim kodovima: otvorite datoteku u Acrobatu ili bilo kom pregledniku temeljenom na PDFium-u i potvrdite da označavanje seda na tekst, čita se u željenoj prozirnosti i preživljava krug spremanja i ponovnog učitavanja. Vrste zabeleški, rukovanje četvorouglovima i pisac svestan broja prikazani ovde deo su standardne komponente PDFium Component za Delphi, C++Builder i Lazarus; stranica proizvoda nosi kompletnu referencu API-ja za zabeleške uz ostatak biblioteke