PDFium Component הופך PDF בעל פריסה קבועה למודל סמנטי הניתן לזרימה מחדש, באמצעות BuildReflowDocument, ומייצא את המודל הזה כ-HTML עצמאי דרך ToHtml. כותרות נשארות כותרות, פריטי רשימה נשארים פריטי רשימה, וטבלאות שזוהו בעמוד יוצאות כתגי טבלה אמיתיים עם תאי כותרת ומיזוגים משומרים. שום דבר בפלט לא מפנה לסקריפט או גיליון סגנון חיצוני
הסיבה לרצות זאת היא שעמוד PDF הוא קבוצה של גליפים ממוקמים, וזה בדיוק שגוי עבור מסך טלפון, קורא מסך, או אינדקס חיפוש. כל ניסיון לפתור זאת על ידי חילוץ טקסט פשוט מאבד את המבנה שהפך את המסמך לקריא, וכל ניסיון לפתור זאת על ידי המרת עמודים לתמונות מאבד את הטקסט לגמרי. מודל זרימה מחדש שומר על שניהם: המילים והקשרים ביניהן
מאיפה מגיע המידע הסמנטי?
הכל מתחיל מ-GetStructuredText, המקור היחיד לטקסט ולסמנטיקה ברכיב. כאשר ה-PDF נושא עץ מבנה, PDF מתויג כפי שמוגדר ב-ISO 32000-1 סעיף 14.7, המודל עוקב אחרי ההיררכיה הלוגית שהמפיק רשם. כשאין כזה, ורוב קובצי ה-PDF בטבע אין להם, המודל נופל בחזרה לסדר הפריסה הפיזי שכבר חושב למטרות סדר קריאה
הבחירה הזו שומרת על גבול קשיח: אין מפרש PDF שני ואין מנוע הצגה שני שמוכנסים כדי לענות על שאלות שהקיים כבר יכול לענות עליהן. מנגנון סדר הקריאה מתחת מתואר בבלוקי טקסט מובנה וסדר קריאה, ומודל הזרימה מחדש הוא שכבה סמנטית מעליו ולא תחליף לו
כל צומת רושם מהיכן הגיע המידע שלו, כך שצרכן יכול להבחין בין כותרת שהמסמך הכריז עליה לבין כותרת שהיוריסטיקות של הפריסה הסיקו. צינורות עבודה רגישי-ביטחון צריכים לקרוא את השדה הזה במקום להתייחס לכל הצמתים כבעלי סמכות שווה
עץ שטוח, ולמה זה לא עץ של אובייקטים
המודל הוא עץ שוטח בסדר קדם: מערך של צמתים שבו כל צומת נושא ParentIndex ו-Depth, במקום רשומה רקורסיבית או גרף אובייקטים עם בעלות. עמודים, כותרות, פסקאות, רשימות, פריטי רשימה, איורים, כיתובים, טבלאות, שורות ותאים כולם חיים באותו מערך ליניארי אחד
שני יתרונות נובעים מכך. צרכנים יכולים לזרום על המערך בסדר בלי רקורסיה, מה שהופך פליטת HTML, Markdown או תצוגת עץ ללולאה פשוטה. והפריסה נשארת ניידת בין דלפי, C++Builder ו-Free Pascal, שנבדלים באופן שבו הם מטפלים בטיפוסים רקורסיביים מנוהלים על פני גבול ABI. רשומה רקורסיבית של מערכים דינמיים היא בדיוק הסוג של מבנה שמתקמפל בכל מקום ומתנהג באופן שונה בעדינות בכל אחד
uses
PDFium;
var
Pdf: TPdf;
Options: TPdfReflowOptions;
Doc: TPdfReflowDocument;
I: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'report.pdf';
Pdf.LoadDocument;
Options := TPdfReflowOptions.Default;
Options.FullDocument := True;
Options.DetectTables := True;
Options.IncludeCss := True; // בלוק סגנון מוטבע, ללא קובץ חיצוני
Options.MaxNodes := 200000; // תקציב כשל-סגור
Options.MaxCharacters := 4000000;
Doc := Pdf.BuildReflowDocument(Options);
for I := 0 to High(Doc.Nodes) do
case Doc.Nodes[I].Kind of
prnkHeading:
Writeln(Format('%sH%d: %s', [StringOfChar(' ', Doc.Nodes[I].Depth),
Doc.Nodes[I].HeadingLevel, Doc.Nodes[I].Text]));
prnkParagraph:
Writeln(Format('%sp: %s', [StringOfChar(' ', Doc.Nodes[I].Depth),
Copy(Doc.Nodes[I].Text, 1, 60)]));
prnkTable:
Writeln(Format('table on page %d', [Doc.Nodes[I].PageNumber]));
end;
Writeln(Format('%d node(s), %d table(s), %d character(s)',
[Length(Doc.Nodes), Doc.TableCount, Doc.CharacterCount]));
finally
Pdf.Free;
end;
end;
איך טבלאות נשמרות מלהופיע פעמיים?
זיהוי טבלאות רץ אחרי שטקסט מובנה כבר נאסף לעמוד, מה שיוצר סכנה ברורה: אותו תוכן תא קיים גם בבלוקי הטקסט וגם בטבלה המזוהה. פליטת שניהם מפיקה HTML שבו כל טבלה מלווה בתוכן שלה עצמה שוב כפסקאות משוחררות
הכלל שפותר זאת הוא גיאומטרי. כאשר טבלה מזוהה מכסה יותר מחצי משטח בלוק טקסט, צומת הטבלה מחליף את הבלוק הזה במקום להצטרף אליו. אינדוקס תאים בתוך שורה נבנה על ידי ספירה לתוך דליים, כך שבניית המודל נשארת ליניארית בתאים ובשורות במקום לסרוק מחדש כל תא לכל שורה, מה שחשוב במסמכים פיננסיים שבהם עמוד בודד יכול לשאת מאות תאים
מבנה מזוהה כן לגבי היותו זיהוי. טבלה עם קווי סרגול מזוהה באופן אמין יותר מאחת המיושרת רק על ידי רווחים לבנים, והביטחון של הצומת משקף זאת. עבור תוכן שבו טבלה שגויה עדיפה על אין טבלה, השאירו את הזיהוי פועל; עבור המרה ארכיונית שבה טבלה שגויה גרועה יותר, סננו לפי ביטחון
ייצוא HTML שנשאר עצמאי
ToHtml עוברת על המודל שכבר נבנה ולעולם לא חוזרת ל-PDFium, כך שייצוא פעמיים לא עולה כלום נוסף ולא יכול להפיק תוצאה שונה מאותו מודל. טקסט וערכי תכונות נמלטים באופן אחיד, רמות כותרת נצמדות לטווח h1 עד h6 ש-HTML באמת מגדיר, ותאי כותרת, RowSpan ו-ColumnSpan עוברים כפי שנכתבו
ה-CSS האופציונלי הוא בלוק סגנון מוטבע פשוט. אין סקריפט, אין גופן רשת ואין משאב חיצוני מכל סוג, וזה מה שהופך את הפלט לבטוח להטמעה בדוא"ל, בצופה עזרה או בפקד דפדפן מוגן:
var
Html: WideString;
Stream: TFileStream;
Bytes: TBytes;
begin
Options := TPdfReflowOptions.Default;
Options.FullDocument := True;
Options.IncludeCss := True;
Options.IncludePageSections := True; // שמרו גבולות עמוד גלויים
Options.PreserveLineBreaks := False; // תנו לדפדפן לגלוש פסקאות
Html := Pdf.BuildReflowDocument(Options).ToHtml;
Bytes := TEncoding.UTF8.GetBytes(string(Html));
Stream := TFileStream.Create('report.html', fmCreate);
try
if Length(Bytes) > 0 then
Stream.WriteBuffer(Bytes[0], Length(Bytes));
finally
Stream.Free;
end;
end;
PreserveLineBreaks היא האפשרות הכי שווה מחשבה. שבירת שורה ב-PDF היא החלטת סידור שנעשתה עבור רוחב עמוד קבוע, כך ששימורה על מסך צר משכפל בדיוק את הבעיה שזרימה מחדש קיימת כדי לפתור. שמרו שבירות עבור שירה, רשימות קוד וכתובות; השמיטו אותן עבור פרוזה
תקציבים, ביטול ומצב עמוד
תווים, צמתים, טבלאות ותאים כל אחד נושא תקרה, וכל אחת נבדקת לפני הקצאה ולא אחרי, כך שמסמך פגום או עוין נכשל בנקיות במקום לצרוך זיכרון עד שמשהו אחר יעשה זאת. אסימון הביטול נבדק בגבולות עמוד, בלוק, טבלה, שורה ותא, מה ששומר על סריקה שבוטלה של מסמך בן אלף עמודים רספונסיבית
התנהגות אחת חשובה לאפליקציות GUI באופן ספציפי: כל סריקת המסמך רצה בתוך היקף שמשחזר את העמוד הפעיל, כך שהצלחה, כשל תקציב וביטול כולם משאירים את העמוד הנוכחי של הקורא ללא נגיעה. צופה שמאפשר למשתמש לייצא בעודו מסתכל בעמוד 340 מוצא את עצמו עדיין בעמוד 340 אחר כך
למה זרימה מחדש טובה, ומה היא לא
פלט זרימה מחדש הוא קלט מצוין לאינדוקס חיפוש, תצוגות קריאה נגישות, תצוגה ניידת והגירת תוכן. זהו לא ממיר שומר-נאמנות: מיקומים מוחלטים, גופנים מדויקים, אמנות וקטורית וגיאומטריית עמוד מדויקת נמצאים מחוץ למטרתו במכוון. כשעבודה צריכה שהעמוד ייראה זהה, הציגו אותו; כשהיא צריכה שהעמוד יהיה קריא במקום אחר, זרמו אותו מחדש
עבור טכנולוגיה מסייעת באופן ספציפי, מודל הזרימה מחדש משתלב עם תכונות הקריאה המתוארות בבניית קורא נגיש, ומסמכים שנושאים עץ מבנה אמיתי מפיקים מודלים טובים יותר באופן ניכר, מה שטיעון טוב לאימות תיוג במעלה הזרם כמתואר באימות עץ מבנה PDF/UA
זרימה מחדש, טקסט מובנה, אימות תיוג והצגה חולקים אובייקט מסמך אחד בדלפי, C++Builder ו-Lazarus; מלוא ה-API מתואר בעמוד רכיב PDFium Component לדלפי