Technický článek

Anotace s označením textu pomocí PDFium QuadPoints v Delphi

Komponenta PDFium Component vytváří anotace s označením textu — tedy zvýraznění, podtržení, přeškrtnutí a vlnovku — prostřednictvím metody TPdf.CreateAnnotation: v záznamu TPdfAnnotation nastavíte vlastnost HasAttachmentPoints := True a vyplníte čtyřúhelník AttachmentPoints a komponenta zapíše položku QuadPoints definovanou v normě ISO 32000-1 §12.5.6.10. To je celá plocha rozhraní API. Důvodem vzniku tohoto článku je to, co se děje pod ním, protože surový řetězec volání PDFium obsahuje chybový stav, který produkuje ten nejméně užitečný příznak z celé sady nástrojů: funkce FPDFAnnot_SetAttachmentPoints pokaždé vrátí hodnotu false u čerstvě vytvořené anotace, bez chybového kódu a bez jakéhokoli vysvětlení. Jedná se o doprovodný článek na straně vytváření k našemu článku o čtení a revizi stávajících anotací, který prochází stejnými strukturami opačným směrem

Scéna ladění je vždy stejná. Vytvoříte anotaci zvýraznění, zavoláte setter bodů připojení s indexem 0, funkce vrátí false a vy začnete pochybovat o svých souřadnicích. Transponujete body, otočíte osu Y, zaměníte prostor stránky za prostor zařízení. Nic z toho nepomáhá, protože souřadnice nikdy nebyly problémem. Problém spočívá v sémantice indexů v C API, a jakmile ji pochopíte, oprava zabere pouhé dva řádky

Co znamenají QuadPoints v normě ISO 32000-1

QuadPoints je pole 8×n čísel popisujících n čtyřúhelníků a norma ISO 32000-1 §12.5.6.10 jej vyžaduje u každé anotace označující text: každý čtyřúhelník označuje slovo nebo skupinu sousedících slov, na které se zvýraznění, podtržení nebo přeškrtnutí vztahuje. Položka anotace Rect sice stále existuje, ale pro podtypy označení pouze omezuje danou oblast; to, co vykreslovací jádro (renderer) skutečně maluje, jsou čtyřúhelníky. Používá se čtyřúhelník namísto obdélníku, protože text může být otočený nebo zkosený, takže se čtyři rohy ukládají jako čtyři nezávislé body: x1 y1 x2 y2 x3 y3 x4 y4

Pořadí těchto čtyř bodů je místem, kde se specifikace a instalovaná základna rozcházejí. Text specifikace popisuje body tak, že opisují čtyřúhelník proti směru hodinových ručiček, ale vlastní vykreslovací jádro společnosti Adobe je vždy interpretovalo v uspořádání do písmene Z: nejprve horní hrana zleva doprava, poté dolní hrana zleva doprava. Vzhledem k tomu, že každý autor testoval proti programu Acrobat, v podstatě každé vykreslovací jádro, včetně PDFium, následuje vzor Z, a soubory, které se drží doslovného znění specifikace, se v některých prohlížečích vykreslují jako zhroucené nebo překroucené zvýraznění. Struktura FS_QUADPOINTSF v knihovně PDFium kóduje přesně tuto konvenci: (x1,y1) je levý horní roh, (x2,y2) pravý horní, (x3,y3) levý dolní, (x4,y4) pravý dolní, v souřadnicích stránky, kde Y roste nahoru. Držte se tohoto pořadí a máte hotovo; renderery jsou shovívavé k mnoha věcem, ale zmatený čtyřúhelník mezi ně nepatří

Proč FPDFAnnot_SetAttachmentPoints vrací false?

Funkce FPDFAnnot_SetAttachmentPoints selhává u nové anotace, protože jejím kontraktem je nahradit čtyřúhelník na daném indexu a čerstvě vytvořená anotace má nula čtyřúhelníků k nahrazení. Signatura přijímá handle anotace, index quad_index a body; index 0 neznamená „první slot, který se v případě potřeby vytvoří“, ale znamená „existující čtyřúhelník číslo 0“, a když FPDFAnnot_CountAttachmentPoints nahlásí hodnotu 0, žádný takový čtyřúhelník neexistuje a volání vrátí false. Funkce, která slot vytvoří, je FPDFAnnot_AppendAttachmentPoints. Každá anotace vytvořená pomocí FPDFPage_CreateAnnot začíná s počtem nula, takže cesta vytváření musí nejprve zavolat Append a pouze následné aktualizace mohou volat Set

To zasáhlo i samotnou komponentu PDFium Component. Do verze v1.79.0 měla interní rutina sdílená metodami CreateAnnotation a SetAnnotation pevně zakódováno volání FPDFAnnot_SetAttachmentPoints(Annotation, 0, ...), což bylo správné pro aktualizaci existující anotace, ale zaručeně to selhalo u nové, což se projevilo jako výjimka EPdfException se zprávou „Cannot set attachment points“. Oprava dodaná ve verzi v1.79.1 se větví podle tohoto počtu

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

Stejný vzor platí, pokud voláte exportované funkce jazyka C přímo, což vám komponenta umožňuje, protože všechny vstupní body FPDFAnnot_* jsou zpřístupštěny v souboru PDFium.pas. Kdykoli držíte handle FPDF_ANNOTATION a chcete zapisovat čtyřúhelníky, zeptejte se nejprve na hodnotu FPDFAnnot_CountAttachmentPoints a podle toho směrujte volání. Pokud hledáte, proč „FPDFAnnot_SetAttachmentPoints vrací false“, je toto větvení s kontrolou počtu a následným připojením téměř jistě vaší odpovědí

Vytvoření zvýraznění pomocí TPdf.CreateAnnotation

Když za vás komponenta řeší směrování mezi Append a Set, vytvoření zvýraznění se redukuje na vyplnění záznamu. Níže uvedený příklad vytvoří stránku A4 a umístí poloprůhledné žluté zvýraznění nad oblast o rozměrech 200×20 bodů; všimněte si, že čtyřúhelník sleduje výše popsané pořadí Z a že Rectangle je nastaven tak, aby čtyřúhelník uzavíral, což zajišťuje správné chování v prohlížečích, které testují zásahy vůči 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;

Změna podtypu stojí jeden řádek. Typy anUnderline, anStrikeout a anSquiggly přebírají identický tvar záznamu, včetně čtyřúhelníků a všeho ostatního, protože norma ISO 32000-1 s nimi zachází jako se stejnou rodinou anotací, která se liší pouze způsobem dekorace oblasti čtyřúhelníku. Podtypy, které nejsou označením textu, jako jsou anSquare, anCircle a anText, se umisťují pouze pomocí Rectangle; pro ty ponechte vlastnost HasAttachmentPoints na False a mechanismus čtyřúhelníků se nespustí

Proč se AttachmentPoints[0] v Delphi přeloží, ale ve FPC selže?

Typ TQuadrilateralPoint je deklarován jako array [1..4] of TPdfPoint, což je pole indexované od 1, a to zmate každého, jehož prsty automaticky používají indexování od nuly. Když napíšete A.AttachmentPoints[0], kompilátor dcc32 v Delphi to přeloží bez jakýchkoli stížností, protože kontrola rozsahů je ve výchozím nastavení vypnutá; za běhu výraz tiše čte nebo zapisuje paměť těsně před polem, což je v záznamu TPdfAnnotation sousední pole. Vaše zvýraznění dostane jeden chybný roh nebo dojde k poškození sousedního pole a nic nevyvolá chybu. Free Pascal zachytil přesně tuto chybu v našich vlastních ukázkových zdrojích během portování na Lazarus: fpc provádí kontrolu rozsahů při kompilaci u konstantních indexů a odmítl AttachmentPoints[0..3] přímo, čímž se společně odhalila chyba posunu o jedna a chyba knihovny Set-versus-Append

Z toho plynou dva návyky. Indexujte čtyřúhelník od 1 do 4, což odpovídá pořadí rohů ve výše uvedeném kódu, a předtím, než mu začnete důvěřovat, sestavte svůj kód anotací alespoň jednou se zapnutou kontrolou rozsahů — buď pomocí {$R+} v Delphi, nebo při jakémkoli sestavení ve fpc. Úspěšný průchod výchozího sestavení v dcc32 není důkazem, že indexy jsou správné; je to pouze důkaz, že nic nezhavarovalo na paměti, která tam náhodou byla

Získávání souřadnic čtyřúhelníku ze skutečného textu

Pevně zakódované obdélníky jsou v pořádku pro ukázku, ale produkční zvýraznění kopírují skutečné glyfy a souřadnice by měly pocházet z geometrie textu stránky PDFium spíše než z dohadů. Rutiny popsané v našem průvodci extrakcí textu pomocí komponenty PDFium Component vám poskytují ohraničující rámečky (bounding boxes) pro jednotlivé znaky ve stejném souřadnicovém prostoru stránky, jaký používají čtyřúhelníky, takže výsledek vyhledávání se přímo převádí na rohové body: vlevo od prvního znaku, vpravo od posledního, nahoře a dole podle rozsahů řádku. Pokud generujete text sami a potřebujete vědět, kam řádky spadnou předtím, než existují, článek o měření textu a zalamování slov popisuje výpočet těchto rozsahů předem

Jeden upřímný limit: záznam TPdfAnnotation nese jediný bod TQuadrilateralPoint, takže jedno volání CreateAnnotation zapíše jeden čtyřúhelník. Výběr přesahující tři řádky vyžaduje tři čtyřúhelníky, jeden na řádek, podle §12.5.6.10, a k tomu máte dvě cesty. Jednoduchá cesta je jedna anotace na řádek, což se správně vykresluje všude a zachovává rozhraní API na úrovni komponenty. Kompaktní cesta — jedna anotace nesoucí tři čtyřúhelníky — znamená vytvoření anotace prostřednictvím komponenty a následné ruční volání exportované funkce FPDFAnnot_AppendAttachmentPoints pro druhý a třetí čtyřúhelník, což funguje právě proto, že Append sloty vytváří, místo aby je nahrazoval. Nepokoušejte se dosáhnout více čtyřúhelníků opakovaným voláním SetAttachmentPoints; každý index přesahující aktuální počet pouze vrátí false, ze stejného důvodu, z jakého jej vrátil index 0 u nové anotace

Po zápisu proveďte ověření v reálném prohlížeči, místo abyste se spoléhali pouze na návratové kódy: otevřete soubor v programu Acrobat nebo v jakémkoli prohlížeči založeném na PDFium a potvrďte, že označení sedí na textu, má zamýšlené krytí a přežije cyklus uložení a opětovného načtení. Typy anotací, zpracování čtyřúhelníků a zapisovač zohledňující počty zobrazený v tomto článku jsou součástí standardní komponenty PDFium Component pro Delphi, C++Builder a Lazarus; stránka produktu obsahuje kompletní referenční příručku pro anotace API spolu se zbytkem knihovny