רכיב PDFium יוצר הערות סימון טקסט (text markup annotations), כלומר הדגשה (highlight), קו תחתון (underline), קו חוצה (strikeout) וקו גלי (squiggly), באמצעות TPdf.CreateAnnotation: אתם מגדירים HasAttachmentPoints := True ברשומת ה-TPdfAnnotation וממלאים את מרובע ה-AttachmentPoints של ה-TPdfAnnotation, והרכיב כותב את ערך ה-QuadPoints המוגדר בתקן ISO 32000-1 §12.5.6.10. זהו כל שטח הפנים של ה-API. הסיבה לקיומו של מאמר זה היא מה שקורה מתחת לפני השטח, מכיוון שלשרשרת הקריאות הגולמית של PDFium יש מצב כשל המייצר את הסימפטום הפחות מועיל בארגז הכלים: הפונקציה FPDFAnnot_SetAttachmentPoints מחזירה false על גבי הערה שזה עתה נוצרה, בכל פעם מחדש, ללא קוד שגיאה וללא כל רמז. זהו המאמר המלווה מצד היצירה למאמר שלנו על קריאה וסקירה של הערות קיימות, הצועד בכיוון ההפוך דרך אותם מבנים
סצנת הדיבוג היא תמיד זהה. אתם יוצרים הערת הדגשה, קוראים ל-setter של נקודות החיבור עם אינדקס 0, הפונקציה מחזירה false, ואתם מתחילים להטיל ספק בקואורדינטות שלכם. אתם מטים את הנקודות, הופכים את ציר ה-Y, מחליפים את מרחב העמוד במרחב ההתקן. שום דבר מזה לא עוזר, מכיוון שהקואורדינטות מעולם לא היו הבעיה. הבעיה היא סמנטיקת האינדקסים של ה-C API, וברגע שמבינים אותה, התיקון הוא בן שתי שורות
מה המשמעות של QuadPoints בתקן ISO 32000-1?
הערך QuadPoints הוא מערך של 8×n מספרים המתארים n מרובעים, ותקן ISO 32000-1 §12.5.6.10 דורש אותו בכל הערת סימון טקסט: כל מרובע מסמן מילה או קבוצת מילים רציפות שההדגשה, הקו התחתון או הקו החוצה חלים עליהן. ערך ה-Rect של ההערה עדיין קיים, אך עבור תת-סוגי סימון הוא רק מגדיר את הגבול; המרובעים (quads) הם מה שהמציג (renderer) מצייר בפועל. מדובר במרובע ולא במלבן מכיוון שטקסט יכול להיות מסובב או נטוי, ולכן ארבעת הפינות נשמרות כארבע נקודות עצמאיות: 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 נכשלת על גבי הערה חדשה מכיוון שהחוזה שלה הוא להחליף את המרובע באינדקס נתון, ולהערה שזה עתה נוצרה יש אפס מרובעים להחלפה. החתימה מקבלת מזהה (handle) של ההערה, 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, מפצל את הנתיב בהתאם לספירה
// 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');
יצירת הדגשה באמצעות 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% 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;
החלפת תת-סוגים עולה שורה אחת. anUnderline, anStrikeout ו-anSquiggly מקבלים את אותו מבנה רשומה, כולל מרובעים והכל, מכיוון שתקן ISO 32000-1 מתייחס לכל הארבעה כאל אותה משפחת הערות המובחנת רק באופן שבו אזור המרובע מקושט. תת-סוגים שאינם סימוני טקסט, כגון anSquare, anCircle ו-anText, מציבים את עצמם על פי Rectangle בלבד; השאירו את HasAttachmentPoints על False עבור אלו, ומנגנון המרובעים לעולם לא ירוץ
מדוע AttachmentPoints[0] מתקמפל בדלפי אך נכשל ב-FPC?
הטיפוס TQuadrilateralPoint מוצהר כ-array [1..4] of TPdfPoint, מערך מבוסס 1 (1-based), וזה מכשיל כל מי שאצבעותיו רגילות כברירת מחדל לאינדקס מבוסס אפס. כתיבת A.AttachmentPoints[0] תתקמפל על ידי ה-dcc32 של דלפי ללא תלונה, מכיוון שבדיקת טווחים (range checking) כבויה כברירת מחדל; בזמן ריצה הביטוי קורא או כותב בשקט את הזיכרון שנמצא ממש לפני המערך, שברשומת TPdfAnnotation הוא שדה סמוך. ההדגשה שלכם מקבלת פינת זבל אחת, או ששדה שכן מושחת, ושום דבר אינו מעלה חריגה. Free Pascal תפס בדיוק את הבאג הזה במקורות הדמו שלנו במהלך הפורט ל-Lazarus: מנוע fpc מבצע בדיקת טווחים בזמן קומפילציה על אינדקסים קבועים ופסל את AttachmentPoints[0..3] לחלוטין, וכך נחשפו יחד שגיאת ה-off-by-one ובאג הספרייה של Set-versus-Append
קבלת קואורדינטות מרובע מטקסט אמיתי
מלבנים מוגדרים מראש (hardcoded) טובים עבור דמו, אך הדגשות ייצור עוקבות אחר תווים (glyphs) אמיתיים, והקואורדינטות צריכות להגיע מגאומטריית עמוד הטקסט של PDFium ולא מניחושים. השגרות המכוסות במדריך שלנו לחילוץ טקסט עם רכיב PDFium מעניקות לכם תיבות חוסמות לכל תו באותו מרחב קואורדינטות עמוד שבו המרובעים משתמשים, כך שהתאמת חיפוש מומרת ישירות לנקודות פינה: משמאל לתו הראשון, מימין לאחרון, והקצוות העליונים והתחתונים מתוך גבולות השורה. אם אתם מייצרים את הטקסט בעצמכם וצריכים לדעת היכן שורות ייפלו לפני שהן קיימות, המאמר על מדידת טקסט וגלישת מילים מכסה את חישוב הגבולות הללו מראש
מגבלה כנה אחת: רשומת ה-TPdfAnnotation נושאת מרובע TQuadrilateralPoint יחיד, כך שקריאת CreateAnnotation אחת כותבת מרובע אחד. בחירה המשתרעת על פני שלוש שורות זקוקה לשלושה מרובעים, אחד לכל שורה, לפי סעיף §12.5.6.10, ויש לכם שתי דרכים להגיע לכך. הדרך הפשוטה היא הערה אחת לכל שורה, שמיוצגת נכון בכל מקום ושומרת על ה-API ברמת הרכיב. הדרך הדחוסה, הערה אחת הנושאת שלושה מרובעים, פירושה יצירת ההערה דרך הרכיב ואז קריאה ל-FPDFAnnot_AppendAttachmentPoints המיוצאת בעצמכם עבור המרובע השני והשלישי, מה שעובד בדיוק מכיוון ש-Append יוצר חריצים במקום להחליף אותם. אל תנסו להגיע למרובעים מרובים באמצעות קריאות חוזרות ל-SetAttachmentPoints; כל אינדקס מעבר לספירה הנוכחית פשוט יחזיר false, מאותה סיבה שאינדקס 0 עשה זאת על גבי ההערה הטרייה
לאחר הכתיבה, אשרו במציג אמיתי במקום לבטוח בקודי החזרה: פתחו את הקובץ ב-Acrobat או בכל מציג מבוסס PDFium ואשרו שהסימון נוחת על הטקסט, נקרא בשקיפות המיועדת ושורד סבב מלא של שמירה וטעינה מחדש. סוגי ההערות, הטיפול במרובעים והכותב המודע לספירה המוצגים כאן הם כולם חלק מ-רכיב PDFium הסטנדרטי עבור דלפי, C++Builder ו-Lazarus; דף המוצר נושא את הפניית ה-API המלאה להערות לצד יתר הספרייה