Komponent PDFium tworzy adnotacje wyróżniania tekstu (oznaczające wyróżnienie, podkreślenie, przekreślenie i wężyk) za pomocą metody TPdf.CreateAnnotation: ustawiasz wartość właściwości HasAttachmentPoints := True w rekordzie TPdfAnnotation i wypełniasz czworokąt AttachmentPoints, a komponent zapisuje wpis QuadPoints zdefiniowany w normie ISO 32000-1 §12.5.6.10. To cały interfejs API. Powodem powstania tego artykułu jest to, co dzieje się pod spodem, ponieważ surowy łańcuch wywołań PDFium ma tryb awaryjny dający mało pomocny objaw: funkcja FPDFAnnot_SetAttachmentPoints zwraca wartość false dla nowo utworzonej adnotacji, za każdym razem, bez kodu błędu i żadnych wskazówek. Jest to artykuł uzupełniający do naszego przewodnika po odczytywaniu i recenzowaniu istniejących adnotacji, opisujący ten sam mechanizm od drugiej strony
Scenariusz debugowania jest zawsze taki sam. Tworzysz adnotację wyróżnienia, wywołujesz funkcję zapisu punktów (attachment-points) z indeksem 0, funkcja zwraca wartość false, a Ty zaczynasz analizować swoje współrzędne. Transponujesz punkty, odwracasz oś Y, zamieniasz współrzędne strony na współrzędne urządzenia. Nic z tego nie pomaga, ponieważ współrzędne nigdy nie były problemem. Problemem jest semantyka indeksowania interfejsu API języka C, a po jej zrozumieniu poprawka zajmuje zaledwie dwie linijki
Czym są QuadPoints w normie ISO 32000-1?
QuadPoints to tablica składająca się z 8xN liczb opisujących N czworokątów, a norma ISO 32000-1 §12.5.6.10 wymaga jej dla każdej adnotacji wyróżniania tekstu: każdy czworokąt oznacza słowo lub grupę sąsiednich słów, do których stosuje się wyróżnienie, podkreślenie lub przekreślenie. Wpis Rect adnotacji wciąż istnieje, ale dla podtypów wyróżniania ogranicza on jedynie obszar; renderujący w rzeczywistości maluje czworokąty (quads). Stosuje się czworokąt zamiast prostokąta, ponieważ tekst może być obrócony lub ścięty, więc cztery rogi są przechowywane jako cztery niezależne punkty: x1 y1 x2 y2 x3 y3 x4 y4
Kolejność tych czterech punktów to miejsce, w którym specyfikacja rozmija się z rzeczywistymi implementacjami. Tekst specyfikacji opisuje punkty jako zakreślające czworokąt w kierunku przeciwnym do ruchu wskazówek zegara, jednak własny silnik renderujący firmy Adobe od zawsze interpretował je w układzie litery Z: najpierw górna krawędź od lewej do prawej, potem dolna krawędź od lewej do prawej. Ponieważ wszyscy autorzy testowali swoje pliki w programie Acrobat, praktycznie każdy silnik (w tym PDFium) stosuje układ Z, a pliki zgodne z dosłownym brzmieniem specyfikacji renderują się w niektórych przeglądarkach jako spłaszczone lub skręcone wyróżnienia. Struktura FS_QUADPOINTSF w PDFium koduje dokładnie tę konwencję: (x1,y1) to lewy górny róg, (x2,y2) prawy górny, (x3,y3) lewy dolny, (x4,y4) prawy dolny, we współrzędnych strony, w których oś Y rośnie w górę. Trzymaj się tej kolejności — silniki renderujące są wyrozumiałe dla wielu rzeczy, ale zniekształcony czworokąt do nich nie należy
Dlaczego FPDFAnnot_SetAttachmentPoints zwraca false?
Funkcja FPDFAnnot_SetAttachmentPoints kończy się niepowodzeniem dla nowej adnotacji, ponieważ jej zadaniem jest zastąpienie czworokąta pod podanym indeksem, a nowo utworzona adnotacja ma zero czworokątów do zastąpienia. Sygnatura przyjmuje uchwyt adnotacji, indeks quad_index oraz punkty; indeks 0 nie oznacza „pierwszego wolnego miejsca, tworzonego w razie potrzeby”, lecz „istniejący czworokąt o numerze 0”. Gdy FPDFAnnot_CountAttachmentPoints zwraca 0, taki czworokąt nie istnieje i wywołanie zwraca wartość false. Funkcją, która tworzy miejsce, jest FPDFAnnot_AppendAttachmentPoints. Każda adnotacja utworzona za pomocą FPDFPage_CreateAnnot zaczyna się od liczby zero, więc ścieżka tworzenia musi najpierw wywołać Append, a dopiero kolejne aktualizacje mogą wywyływać Set
Problem ten dotknął również sam Komponent PDFium. Do wersji 1.79.0 wewnętrzna procedura współdzielona przez CreateAnnotation and SetAnnotation miała na sztywno wpisane wywołanie FPDFAnnot_SetAttachmentPoints(Annotation, 0, ...), co było poprawne dla aktualizacji istniejącej adnotacji, ale gwarantowało błąd przy nowej, generując wyjątek EPdfException z komunikatem „Cannot set attachment points”. Poprawka wprowadzona w wersji 1.79.1 rozgałęzia kod w zależności od liczby punktów
// 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');
Ten sam wzorzec ma zastosowanie, jeśli wywołujesz wyeksportowane funkcje C bezpośrednio, co komponent umożliwia, ponieważ wszystkie punkty wejściowe FPDFAnnot_* są ujawnione w module PDFium.pas. Zawsze, gdy posiadasz uchwyt FPDF_ANNOTATION i chcesz zapisać czworokąty, najpierw zapytaj o wynik FPDFAnnot_CountAttachmentPoints i odpowiednio skieruj proces. Jeśli szukasz informacji, dlaczego „FPDFAnnot_SetAttachmentPoints zwraca false”, to rozgałęzienie count-then-append jest najpewniej rozwiązaniem
Tworzenie wyróżnienia za pomocą TPdf.CreateAnnotation
Ponieważ komponent sam zajmuje się kierowaniem żądań do Append lub Set, tworzenie wyróżnienia sprowadza się do uzupełnienia rekordu. Poniższy przykład tworzy stronę A4 i nakłada półprzezroczyste żółte wyróżnienie na obszar o wymiarach 200x20 punktów; zauważ, że czworokąt zachowuje opisany wyżej układ Z, a pole Rectangle jest ustawione tak, by otaczać czworokąt, co pozwala na poprawne działanie testów trafienia (hit-test) w przeglądarkach
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;
Zmiana podtypu wymaga modyfikacji jednej linii. Podtypy anUnderline, anStrikeout oraz anSquiggly przyjmują identyczny kształt rekordu wraz z punktami czworokąta, ponieważ norma ISO 32000-1 traktuje te cztery rodzaje jako tę samą rodzinę adnotacji, różniącą się jedynie sposobem dekoracji obszaru czworokątnego. Podtypy, które nie są wyróżnieniami tekstu (jak anSquare, anCircle czy anText), pozycjonują się wyłącznie na podstawie Rectangle; dla nich właściwość HasAttachmentPoints należy pozostawić z wartością False, a mechanizm czworokątów w ogóle nie zostanie uruchomiony
Dlaczego AttachmentPoints[0] kompiluje się w Delphi, ale zgłasza błąd w FPC?
Typ TQuadrilateralPoint is zadeklarowany jako array [1..4] of TPdfPoint — czyli tablica indeksowana od 1 — co może zmylić każdego, kto domyślnie zaczyna indeksowanie od zera. Jeśli napiszesz A.AttachmentPoints[0], kompilator dcc32 w Delphi skompiluje to bez problemu, ponieważ sprawdzanie zakresów jest domyślnie wyłączone. W czasie wykonywania wyrażenie to po cichu odczyta lub zapisze pamięć znajdującą się tuż przed tablicą, co w rekordzie TPdfAnnotation oznacza sąsiednie pole. Twoje wyróżnienie otrzyma jeden błędny narożnik lub sąsiednie pole ulegnie uszkodzeniu i żaden błąd nie zostanie zgłoszony. Free Pascal wykrył ten błąd w naszych własnych plikach demonstracyjnych podczas tworzenia wersji dla środowiska Lazarus: fpc wykonuje weryfikację zakresów stałych indeksów w czasie kompilacji i odrzucił zapis AttachmentPoints[0..3], co pomogło nam odkryć ten błąd oraz problem z Set i Append
Zalecamy dwa nawyki. Indeksuj czworokąty od 1 do 4, dopasowując kolejność rogów do powyższego kodu, oraz kompiluj kod adnotacji przynajmniej raz z włączonym sprawdzaniem zakresów (dyrektywa {$R+} w Delphi lub dowolna kompilacja w fpc) przed wdrożeniem go produkcyjnie. Pomyślna kompilacja w dcc32 nie jest dowodem na to, że indeksy są poprawne — świadczy jedynie o tym, że program nie zawiesił się na pamięci, która akurat tam się znajdowała
Pobieranie współrzędnych czworokąta z rzeczywistego tekstu
Współrzędne wpisane na sztywno nadają się do celów demonstracyjnych, ale w systemach produkcyjnych wyróżnienia muszą podążać za rzeczywistymi glifami, a ich współrzędne powinny pochodzić z geometrii tekstu strony PDFium. Procedury opisane w naszym przewodniku po ekstrakcji tekstu za pomocą Komponentu PDFium dostarczają ramki ograniczające (bounding boxes) poszczególne znaki w tej samej przestrzeni współrzędnych strony, z której korzystają czworokąty. Dzięki temu wynik wyszukiwania można bezpośrednio przekształcić w punkty narożne: lewą stronę pierwszego znaku, prawą stronę ostatniego oraz górę i dół na podstawie rozmiarów wiersza. Jeśli sam generujesz tekst i chcesz wiedzieć, gdzie znajdą się linie przed ich utworzeniem, przeczytaj artykuł o pomiarze tekstu i zawijaniu wierszy
Jedna ważna uwaga: rekord TPdfAnnotation zawiera pojedynczy czworokąt TQuadrilateralPoint, więc jedno wywołanie CreateAnnotation zapisuje jeden czworokąt. Zaznaczenie obejmujące trzy wiersze wymaga trzech czworokątów (po jednym na wiersz, zgodnie z §12.5.6.10), co można rozwiązać na dwa sposoby. Prostszy sposób to jedna adnotacja na wiersz, co renderuje się poprawnie w każdym programie i pozwala zachować prostotę interfejsu API komponentu. Sposób bardziej zwięzły — jedna adnotacja niosąca trzy czworokąty — wymaga utworzenia adnotacji przez komponent, a następnie samodzielnego wywołania wyeksportowanej funkcji FPDFAnnot_AppendAttachmentPoints dla drugiego i trzeciego czworokąta, ponieważ Append tworzy nowe sloty zamiast zastępować istniejące. Nie próbuj dodawać wielu czworokątów poprzez powtarzanie wywołań SetAttachmentPoints; każdy indeks wykraczający poza bieżący licznik zwróci wartość false, z tego samego powodu co indeks 0 przy nowej adnotacji
Po zapisaniu danych sprawdź wynik w rzeczywistej przeglądarce, zamiast ufać samym kodom powrotnym: otwórz plik w programie Acrobat lub dowolnej przeglądarce opartej na PDFium i upewnij się, że wyróżnienie leży dokładnie na tekście, ma zamierzoną przezroczystość i przetrwa ponowne ładowanie pliku. Typy adnotacji, obsługa czworokątów oraz opisana tutaj metoda zapisu są częścią standardowego pakietu PDFium Component dla Delphi, C++Builder i Lazarus; strona produktu zawiera pełną dokumentację API adnotacji oraz opis reszty możliwości biblioteki