Articol tehnic

Adnotări de marcare a textului cu PDFium QuadPoints în Delphi

Componenta PDFium creează adnotări de marcare a textului — evidențiere, subliniere, tăiere (strikeout) și subliniere ondulată (squiggly) — prin intermediul TPdf.CreateAnnotation: setați HasAttachmentPoints := True pe înregistrarea TPdfAnnotation și completați patrulaterul său AttachmentPoints, iar componenta scrie intrarea QuadPoints definită în ISO 32000-1 §12.5.6.10. Aceasta reprezintă întreaga interfață API. Motivul apariției acestui articol constă în detaliile de sub această interfață, deoarece apelul brut PDFium are un mod de eșec care produce cel mai puțin util simptom: funcția FPDFAnnot_SetAttachmentPoints returnează false pentru o adnotare proaspăt creată, de fiecare dată, fără cod de eroare și fără indicii. Aceasta este componenta de creare asociată articolului nostru despre citirea și analizarea adnotărilor existente, care parcurge aceleași structuri în direcția opusă

Scenariul de depanare este întotdeauna același. Creați o adnotare de evidențiere, apelați funcția de setare a punctelor de atașare cu indexul 0, funcția returnează false și începeți să vă puneți la îndoială coordonatele. Transpuneți punctele, inversați axa Y, înlocuiți spațiul paginii cu cel al dispozitivului. Nimic din toate acestea nu ajută, deoarece coordonatele nu au fost niciodată problema. Problema este semantica indexului din API-ul C și, odată înțeleasă, remedierea constă în doar două linii

Ce înseamnă QuadPoints în standardul ISO 32000-1

Parametrul QuadPoints este un tablou de 8×n numere care descriu n patrulatere, iar standardul ISO 32000-1 §12.5.6.10 îl impune pentru fiecare adnotare de marcare a textului: fiecare patrulater marchează un cuvânt sau un grup de cuvinte învecinate cărora li se aplică evidențierea, sublinierea sau tăierea. Intrarea Rect a adnotării există în continuare, însă pentru adnotările de marcare ea limitează doar regiunea; patrulaterele sunt cele pe care le desenează efectiv motorul de randare. Se folosește un patrulater în loc de un dreptunghi deoarece textul poate fi rotit sau deformat, astfel încât cele patru colțuri sunt stocate ca patru puncte independente: x1 y1 x2 y2 x3 y3 x4 y4

Ordinea acestor patru puncte reprezintă aspectul în care specificația și implementările reale diferă. Textul specificației descrie punctele ca urmând patrulaterul în sens invers acelor de ceasornic, însă motorul de randare al Adobe le-a interpretat întotdeauna sub forma unui model în Z: mai întâi marginea de sus de la stânga la dreapta, apoi marginea de jos de la stânga la dreapta. Deoarece toți autorii au testat rezultatele în Acrobat, practic fiecare motor de randare, inclusiv PDFium, urmează modelul în Z, iar fișierele care respectă litera specificației sunt redate ca evidențieri distorsionate în unele vizualizatoare. Structura FS_QUADPOINTSF din PDFium codifică exact această convenție: (x1,y1) este colțul stânga-sus, (x2,y2) dreapta-sus, (x3,y3) stânga-jos, (x4,y4) dreapta-jos, în coordonate de pagină unde Y crește în sus. Respectați această ordine; motoarele de randare acceptă multe abateri, dar un patrulater greșit ordonat nu este una dintre ele

De ce returnează FPDFAnnot_SetAttachmentPoints valoarea false?

Funcția FPDFAnnot_SetAttachmentPoints eșuează pentru o adnotare nouă deoarece rolul său este să înlocuiască patrulaterul de la un index specificat, iar o adnotare proaspăt creată are zero patrulatere care pot fi înlocuite. Semnătura preia handlerul adnotării, un index quad_index și punctele; indexul 0 nu înseamnă „prima poziție, pe care o creează dacă este necesar”, ci înseamnă „patrulaterul existent cu numărul 0”, iar când FPDFAnnot_CountAttachmentPoints returnează valoarea 0, nu există un astfel de patrulater, iar apelul returnează false. Funcția care creează o poziție este FPDFAnnot_AppendAttachmentPoints. Fiecare adnotare creată prin FPDFPage_CreateAnnot începe cu un număr de zero, astfel încât procesul de creare trebuie să apeleze mai întâi Append, iar modificările ulterioare pot apela 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');

Această problemă a afectat și componenta PDFium în sine. Până la versiunea v1.79.0, rutina internă partajată de CreateAnnotation și SetAnnotation includea codificarea fixă FPDFAnnot_SetAttachmentPoints(Annotation, 0, ...), care era corectă pentru actualizarea unei adnotări existente, dar eșua garantat pentru una nouă, manifestându-se ca o eroare EPdfException cu mesajul „Cannot set attachment points”. Remedierea, livrată în v1.79.1, ramifică execuția în funcție de numărul de elemente

Crearea unei evidențieri cu TPdf.CreateAnnotation

Odată ce componenta gestionează alegerea între Append și Set în locul dvs., crearea unei evidențieri se reduce la completarea unei înregistrări. Exemplul de mai jos creează o pagină A4 și adaugă o evidențiere galbenă semitransparentă peste o regiune de 200×20 de puncte; rețineți că patrulaterul urmează ordinea în Z descrisă mai sus, iar Rectangle este setat pentru a închide patrulaterul, ceea ce asigură comportamentul corect al vizualizatoarelor care realizează teste de contact în raport cu 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;

Schimbarea subtipurilor necesită o singură linie. Opțiunile anUnderline, anStrikeout și anSquiggly utilizează aceeași structură de înregistrare, cu patrulatere și celelalte detalii, deoarece standardul ISO 32000-1 treats all four as the same annotation family distinguished only by how the quad region is decorated. Subtypes that are not text markup, such as anSquare, anCircle, and anText, position themselves from Rectangle alone; leave HasAttachmentPoints at False for those, and the quad machinery never runs

De ce se compilează AttachmentPoints[0] în Delphi, dar eșuează în FPC?

Parametrul TQuadrilateralPoint este declarat ca array [1..4] of TPdfPoint, un tablou cu indexarea de la 1, iar acest lucru îi pune în dificultate pe cei obișnuiți cu indexarea de la zero. Scrieți A.AttachmentPoints[0], iar dcc32 din Delphi va compila codul fără avertisment, deoarece verificarea limitelor intervalului (range checking) este dezactivată în mod implicit; la rulare, expresia citește sau scrie în mod silențios memoria aflată chiar înaintea tabloului, care în înregistrarea TPdfAnnotation reprezintă un câmp adiacent. Evidențierea va avea un colț incorect sau un câmp învecinat va fi corupt, fără a se declanșa nicio eroare. Free Pascal a detectat exact această eroare în codurile noastre demo în timpul portării în Lazarus: fpc realizează verificarea limitelor la compilare pentru indicii constanți și a respins direct AttachmentPoints[0..3], ceea ce a dus la descoperirea erorii de indexare și a problemei legate de Set-versus-Append

Din aceasta rezultă două reguli practice: indexați colțurile de la 1 la 4, respectând ordinea prezentată în codul de mai sus, și compilați codul adnotării cel puțin o dată cu verificarea limitelor activată, utilizând direct directiva {$R+} în Delphi sau orice build fpc, înainte de a-l considera funcțional. Faptul că un build implicit dcc32 este compilat cu succes nu reprezintă o dovadă că indicii sunt corecți, ci doar că sistemul nu s-a blocat accesând memoria disponibilă întâmplător

Preluarea coordonatelor patrulaterului din textul real

Dreptunghiurile cu valori fixe sunt potrivite pentru un demo, dar evidențierile de producție urmăresc glifele reale, iar coordonatele ar trebui preluate din geometria paginii de text a PDFium, nu prin aproximare. Rutinele prezentate în ghidul nostru pentru extragerea textului cu componenta PDFium oferă casete de delimitare (bounding boxes) pentru fiecare caracter în același spațiu de coordonate al paginii pe care îl folosesc patrulaterele, astfel încât o potrivire de căutare se convertește direct în colțuri: stânga primului caracter, dreapta ultimului caracter, iar marginile de sus și de jos din dimensiunile liniei. Dacă generați textul dvs. și trebuie să știți unde se vor încadra liniile înainte de a le crea, articolul despre măsurarea textului și încadrarea rândurilor explică modul de calcul al acestor dimensiuni

O limită ce trebuie menționată corect: înregistrarea TPdfAnnotation conține un singur element TQuadrilateralPoint, astfel încât un apel CreateAnnotation scrie un singur patrulater. O selecție care se întinde pe trei rânduri are nevoie de trei patrulatere, câte unul pentru fiecare rând, conform §12.5.6.10, existând două modalități de a realiza acest lucru: Calea simplă este o adnotare separată pentru fiecare rând, variantă redată corect în orice vizualizator care păstrează API-ul la nivel de componentă; Calea compactă — o singură adnotare care conține trei patrulatere — presupune crearea adnotării prin componentă, apoi apelarea manuală a funcției exportate FPDFAnnot_AppendAttachmentPoints pentru al doilea și al treilea patrulater, procedură ce funcționează deoarece Append creează poziții noi. Nu încercați să scrieți patrulatere multiple prin apeluri repetate SetAttachmentPoints; fiecare index care depășește numărul actual va returna false, din același motiv pentru care a eșuat indexul 0 pentru adnotarea nouă

După scriere, verificați rezultatul într-un vizualizator real în loc să vă bazați doar pe codurile returnate: deschideți fișierul în Acrobat sau în orice vizualizator bazat pe PDFium și confirmați că marcajul se încadrează corect pe text, are opacitatea dorită și se păstrează la un ciclu de salvare și reîncărcare. Tipurile de adnotări, gestionarea patrulaterelor și scrierea adaptată numărului de elemente prezentate aici fac parte din componenta standard PDFium Component pentru Delphi, C++Builder și Lazarus; pagina produsului conține documentația completă a interfeței API pentru adnotări, alături de restul bibliotecii