Техническа статия

Текстови анотации (Markup Annotations) с PDFium QuadPoints в Delphi

Компонентът PDFium създава текстови анотации (Markup Annotations) — маркиране (highlight), подчертаване (underline), зачеркване (strikeout) и вълнообразно подчертаване (squiggly) — чрез TPdf.CreateAnnotation: задавате HasAttachmentPoints := True в записа TPdfAnnotation и попълвате неговия четириъгълник AttachmentPoints, а компонентът записва записа QuadPoints, дефиниран в ISO 32000-1 §12.5.6.10; Това е целият приложен програмен интерфейс (API); Причината за съществуването на тази статия е това, което се случва под повърхността, тъй като суровата верига от извиквания на PDFium има режим на отказ, който произвежда най-малко полезния симптом в инструментариума: FPDFAnnot_SetAttachmentPoints връща false за новосъздадена анотация всеки път, без код за грешка и без никаква подсказка; Това е съпътстващата статия от страната на създаването към нашата статия за четене и преглед на съществуващи анотации, която преминава в другата посока през същите структури

Сцената за дебъгване винаги е една и съща; Създавате анотация за маркиране, извиквате метода за задаване на точки на прикачване с индекс 0, функцията връща false и започвате да се съмнявате в координатите си; Транспонирате точките, обръщате оста Y, заменяте пространството на страницата с пространството на устройството; Нищо от това не помага, защото координатите никога не са били проблемът; Проблемът е семантиката на индексите в C API и след като ги разберете, корекцията е от два реда

Какво означават QuadPoints в ISO 32000-1

QuadPoints е масив от 8×n числа, описващи n четириъгълника, и ISO 32000-1 §12.5.6.10 го изисква за всяка текстова анотация: всеки четириъгълник маркира дума или група от съседни думи, за които се отнася маркирането, подчертаването или зачеркването; Записът Rect на анотацията все още съществува, но за подтипове за маркиране той само очертава областта; четириъгълниците са това, което рендериращият двигател действително рисува; Използва се четириъгълник, а не правоъгълник, тъй като текстът може да бъде завъртян или наклонен, така че четирите ъгъла се съхраняват като четири независими точки: x1 y1 x2 y2 x3 y3 x4 y4

Редът на тези четири точки е мястото, където спецификацията и практиката се разделят; Текстът на спецификацията описва точките като очертаващи четириъгълника обратно на часовниковата стрелка, но собственият рендериращ двигател на Adobe винаги ги е интерпретирал в Z-образен модел: първо горния ръб отляво надясно, след това долния ръб отляво надясно; Тъй като всеки създател е тествал спрямо Acrobat, на практика всеки рендериращ двигател, включително PDFium, следва Z-образния модел, а файловете, които следват буквалния текст на спецификацията, се рендерират като сринати или усукани маркирания в някои визуализатори; Структурата FS_QUADPOINTSF на PDFium кодира точно тази конвенция: (x1,y1) е горният ляв ъгъл, (x2,y2) е горният десен, (x3,y3) е долният ляв, (x4,y4) е долният десен, в координати на страницата, където Y расте нагоре; Следвайте този ред и всичко ще бъде наред; визуализаторите са снизходителни към много неща, но объркан четириъгълник не е едно от тях

Защо FPDFAnnot_SetAttachmentPoints връща false?

FPDFAnnot_SetAttachmentPoints се проваля при нова анотация, защото неговият договор е да замени четириъгълника при даден индекс, а новосъздадената анотация има нула четириъгълника за замяна; Сигнатурата приема дескриптор на анотация, quad_index и точките; индекс 0 не означава „първия слот, като се създава при необходимост“, той означава „съществуващия четириъгълник номер 0“, и когато FPDFAnnot_CountAttachmentPoints докладва 0, такъв четириъгълник няма и извикването връща false; Функцията, която създава слот, е FPDFAnnot_AppendAttachmentPoints; Всяка анотация, създадена чрез FPDFPage_CreateAnnot, започва с брой нула, така че пътят за създаване трябва първо да извика Append, и само следващите актуализации могат да извикват Set

Това засегна самия компонент PDFium; До v1.79.0 вътрешната рутина, споделена от CreateAnnotation и SetAnnotation, твърдо кодираше FPDFAnnot_SetAttachmentPoints(Annotation, 0, ...), което беше правилно за актуализиране на съществуваща анотация и гарантирано се проваляше при нова, проявявайки се като EPdfException със съобщението 'Cannot set attachment points'; Корекцията, доставена във v1.79.1, се разклонява в зависимост от броя

// Вътре в писателя на анотации на компонента (v1.79.1+):
// новата анотация все още няма слотове за четириъгълници, така че Append създава
// първия; Set само заменя слот, който вече съществува
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');

Същият модел се прилага, ако извиквате изнесените C функции директно, което компонентът ви позволява да правите, тъй като всички входни точки FPDFAnnot_* са налични в PDFium.pas; Винаги, когато държите дескриптор FPDF_ANNOTATION и искате да запишете четириъгълници, първо попитайте FPDFAnnot_CountAttachmentPoints и се насочете съответно; Ако търсите защо „FPDFAnnot_SetAttachmentPoints връща false“, това разклонение с преброяване и последващо добавяне почти сигурно е вашият отговор

Създаване на маркиране с TPdf.CreateAnnotation

След като компонентът извършва насочването между Append и Set вместо вас, създаването на маркиране се свежда до попълване на запис; Примерът по-долу създава страница A4 и поставя полупрозрачно жълто маркиране върху област с размери 200×20 точки; обърнете внимание, че четириъгълникът следва Z-образния ред, описан по-горе, и че Rectangle е зададен да включва четириъгълника, което поддържа правилното поведение на визуализаторите, които извършват hit-test спрямо 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% непрозрачност
    A.HasAttachmentPoints := True;
    A.AttachmentPoints[1].X := 50;  A.AttachmentPoints[1].Y := 700; // горна лява
    A.AttachmentPoints[2].X := 250; A.AttachmentPoints[2].Y := 700; // горна дясна
    A.AttachmentPoints[3].X := 50;  A.AttachmentPoints[3].Y := 680; // долна лява
    A.AttachmentPoints[4].X := 250; A.AttachmentPoints[4].Y := 680; // долна дясна
    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;

Смяната на подтиповете струва един ред; anUnderline, anStrikeout and anSquiggly приемат еднаква форма на записа, четириъгълници и всичко останало, тъй като ISO 32000-1 третира и четирите като едно и също семейство анотации, различаващи се само по начина, по който се декорира областта на четириъгълника; Подтиповете, които не са текстови анотации, като anSquare, anCircle и anText, се позиционират само по Rectangle; оставете HasAttachmentPoints на False за тях и механизмът за четириъгълници никога не се стартира

Защо AttachmentPoints[0] се компилира в Delphi, но се проваля във FPC?

TQuadrilateralPoint е деклариран като array [1..4] of TPdfPoint — 1-базиран масив, и това препъва всеки, чиито пръсти автоматично пишат индекси от нула; Напишете A.AttachmentPoints[0] и dcc32 на Delphi ще го компилира без оплакване, тъй като проверката на диапазоните е изключена по подразбиране; по време на изпълнение изразът тихомълком чете или записва паметта непосредствено преди масива, което в записа TPdfAnnotation е съседно поле; Вашето маркиране получава един ъгъл с боклук стойности, или съседно поле се поврежда, и нищо не съобщава за това; Free Pascal улови точно този бъг в нашите собствени демонстрационни източници по време на порта за Lazarus: fpc извършва проверка на диапазоните по време на компилация за константни индекси и отхвърли директно AttachmentPoints[0..3], което разкри грешката с разлика от едно и грешката с Set-versus-Append в библиотеката едновременно

Оттук следват два навика; Индексирайте четириъгълника от 1 до 4, съответстващ на реда на ъглите в кода по-горе, и изграждайте кода си за анотации поне веднъж с активирана проверка на диапазоните, било то {$R+} в Delphi или каквато и да е компилация с fpc, преди да му се доверите; Успешната компилация по подразбиране с dcc32 не е доказателство, че индексите са правилни; тя е доказателство само, че нищо не се е сринало върху паметта, която случайно е била там

Получаване на координати на четириъгълника от реален текст

Твърдо кодираните правоъгълници са подходящи за демо, но производствените маркирания очертават реални глифове (glyphs) и координатите трябва да идват от геометрията на текстовата страница на PDFium, а не от налучкване; Рутините, разгледани в нашето ръководство за извличане на текст с компонента PDFium, ви дават очертаващи рамки (bounding boxes) за всеки символ в същото координатно пространство на страницата, което използват четириъгълниците, така че намереното при търсене съвпадение се преобразува директно в ъглови точки: отляво на първия символ, отдясно на последния, отгоре и отдолу от размерите на реда; Ако генерирате текста сами и трябва да знаете къде ще попаднат редовете, преди да съществуват, статията за измерване на текст и пренасяне на редове описва предварителното изчисляване на тези размери

Една честна граница: записът TPdfAnnotation носи единичен TQuadrilateralPoint, така че едно извикване на CreateAnnotation записва един четириъгълник; Избор, обхващащ три реда, се нуждае от три четириъгълника, по един на ред, съгласно §12.5.6.10, и имате два начина да постигнете това; Лесният начин е една анотация на ред, което се рендерира правилно навсякъде и запазва API на ниво компонент; Компактният начин — една анотация, носеща три четириъгълника — означава да създадете анотацията чрез компонента и след това да извикате изнесената функция FPDFAnnot_AppendAttachmentPoints сами за втория и третия четириъгълник, което работи именно защото Append създава слотове, а не ги заменя; Не се опитвайте да постигнете множество четириъгълници чрез многократни извиквания на SetAttachmentPoints; всеки индекс извън настоящия брой просто връща false по същата причина, поради която индекс 0 го направи при новата анотация

След записване проверете в реален визуализатор, вместо да се доверявате на върнатите кодове: отворете файла в Acrobat или всеки базиран на PDFium визуализатор и потвърдете, че маркирането попада върху текста, чете се с желаната непрозрачност и оцелява при двупосочно преминаване за запазване и повторно зареждане; Типовете анотации, обработката на четириъгълници и писателят с отчитане на броя, показани тук, са част от стандартния PDFium Component за Delphi, C++Builder и Lazarus; продуктовата страница носи пълния справочник за API на анотациите заедно с останалата част от библиотеката