רכיב HotPDF לדלפי בונה מחדש רווחי מילים ומעברי שורות ב-THotPDF.ExtractLoadedPageText מתוך גיאומטריית ה-glyph-ים, ולא מתוך תווי רווח. רווח נכנס כשהמרווח אחרי הרוחב של ה-glyph עצמו חורג מ-0.15 מגובה הטקסט, ושורה חדשה מתחילה רק כשמקור הטקסט נע מעבר לכיוון הכתיבה ביותר מחצי מגובה הטקסט. מאז v2.768.3 טקסט העמוד כולל גם טקסט שנצייר דרך Form XObjects ומשמיט glyph-ים מחוץ לאזור ה-crop הנראה. שאר הרשומה מסבירה למה כל כלל נראה כמו שהוא נראה, כי כל אחד מהם החליף כלל פשוט יותר שהניב פלט סביר אבל שגוי על מסמכים אמיתיים
התסמינים מוכרים לכל מי שהזין טקסט PDF אל אינדקס חיפוש. עמוד שער נחלץ כ-PDFReferenceManualNovember4,1998, טופס מס מתפצל ל-156 שורות, watermark אלכסוני מגיע תו אחד לשורה, והגהת עבודה חתוכה נפתחת בשורת ה-slug של המדפסת שאף מציג לעולם לא מציג. אף אחד מהקבצים האלה אינו שבור. כל אחד מהם משתמש בדרך חוקית לחלוטין למקם טקסט שמחלץ תמים מפרש שלא נכון
למה טקסט PDF שחולץ מאבד את רווחי המילים שלו?
טקסט חלוץ מאבד רווחי מילים כי לעולם לא נדרש מ-PDF להכיל אותם. מפיק יכול להפריד מילים בהצגת תו רווח, אבל הוא יכול באותה מידה להזיז את העט עם מספר בתוך מערך TJ (ISO 32000-1 §9.4.3) או עם Td טרי (§9.4.2), ופלט TeX, קבצי Distiller רבים ורוב הפריסות המיושרות עושים בדיוק זאת. לפני v2.766.76, ה-HPDFAssemblePageText הביט רק בתנועה אנכית, ולכן הפרדת מילים שנעשתה במיקום פשוט נעלמה. המרכיב מודד עכשיו, לאורך כיוון הכתיבה של ה-glyph הקודם, את המרחק מסוף הרוחב של אותו glyph עצמו אל מקור ה-glyph הנוכחי, ומכניס רווח אחד כשהמרחק חורג מ-0.15 מגובה התיבה של ה-glyph הנוכחי, הנמדד מעלייה עד ירידה במרחב המשתמש. לא נוסף רווח כשכל צד כבר ריק, ואף אחד בין שני תווי CJK, כי יישור מותח אידיאוגרפיות זו מזו בלי שהמתיחה הזאת פירושה גבול מילה. רשומות ה-glyph חושפות את אותה גיאומטריה, ולכן אתם יכולים לשחזר את ההחלטה כשקובץ מסוים מבלבל אתכם
uses
SysUtils, HPDFDoc, HPDFContentStream;
procedure DumpWordGaps(Pdf: THotPDF; PageIndex: Integer);
var
Glyphs: THPDFGlyphArray;
I: Integer;
Height, Gap: Double;
begin
if not Pdf.ExtractLoadedPageGlyphs(PageIndex, Glyphs) then
Exit;
for I := 1 to High(Glyphs) do
begin
// גובה עלייה-עד-ירידה של תיבת ה-glyph, במרחב המשתמש
Height := Sqrt(Sqr(Glyphs[I].QuadX[3] - Glyphs[I].QuadX[0]) +
Sqr(Glyphs[I].QuadY[3] - Glyphs[I].QuadY[0]));
// טקסט אופקי: המרווח מסוף הרוחב של ה-glyph הקודם עצמו
Gap := Glyphs[I].BaselineStartX - Glyphs[I - 1].GlyphEndX;
if (Height > 0) and (Gap > 0.15 * Height) then
Writeln(Format('U+%.4x gap %.2f height %.2f: space',
[Glyphs[I].Unicode, Gap, Height]));
end;
end;
למה למדוד מהרוחב של ה-glyph עצמו במקום ממיקום העט?
HotPDF מודד מרווחי מילים מה-GlyphEndX / ה-GlyphEndY כי מיקום העט אחרי glyph כבר מכיל ריווח שאינו מרווח. ISO 32000-1 §9.4.4 מגדיר את ההזחה האופקית כרוחב ה-glyph כפול גודל הפונט, ועוד ריווח תווים Tc, ועוד ריווח מילים Tw, הכול בהכפלת Tz. ה-BaselineEndX / ה-BaselineEndY מחזיקים את אותה הזחה מלאה, בזמן שה-GlyphEndX / ה-GlyphEndY מחזיקים רק את ההתקדמות של הפונט ואת ה-Tz. ההבדל חשוב למפיקים שמהדקים tracking עם Tc שלילי ואז מחזירים את הרווח דרך התאמת TJ אחרי כל glyph: נמדד ממיקום העט, ההחזר נראה כמו מרווח, והמונח הסיני “95后” חולץ כ-“9 5 后”. הסף קשור לגובה תיבת ה-glyph ולא לגודל ה-Tf מאותה סיבה. ייצוא של Word כותב לעיתים קרובות 1 Tf ונושא את הגודל האמיתי ב-Tm מוכפל, כך שה-Tfs אומר 1 בזמן שהטקסט גבוה 10 נקודות, וכלל שנקשר ל-Tfs היה מתייחס אחרת אל שתי האיותים של אותו עמוד
לכלל יש קצוות כנים. כותרת שנקבעה עם tracking רפוי מאוד, שבו ה-Tc לבדו פותח יותר מ-0.15 מגובה הטקסט בין אותיות, חולץ עם רווח בין כל אות, שזה מה שהעמוד נראה אבל כנראה לא מה שרציתם לאנדקס. חתיכות שצוירו מחוץ לסדר על baseline אחד מניבות מרווח שלילי ומתחברות בלי רווח. אף מקרה אינו נפוץ בטקסט גוף, ועל קורפוס בדיקות השינוי העלה את התאמות המילים מול מחלץ ייחוס ב-28 עמודים בלי להוריד אף אחת
מתי HotPDF פותח שורה חדשה בטקסט חלוץ?
מאז v2.766.79, שורה חדשה נפתחת כשהתזוזה ממקור ה-glyph הקודם אל הנוכחי, מוקרנת על הנורמל של כיוון הכתיבה הקודם, חורגת מחצי מגובה התיבה הגדול מבין שני ה-glyph-ים. הכלל הקודם השווה את תזוזת ה-Y הגולמית אל חצי מה-Tfs, מה שנכשל בשני כיוונים. עם 1 Tf ו-Tm מוכפל הסף התכווץ לחצי יחידה, כך שכתב-על (superscript) שהורם ב-text rise של 0.4 או רעד baseline רגיל שברו את השורה. הכלל גם התעלם מה-X לגמרי, ולכן טקסט תחת Tm מסובב ירד בעמוד עם כל glyph ויצא glyph אחד לשורה. הקרנה על נורמל הכיוון גורמת לריצות מסובבות להתנהג כמו אופקיות, ולקיחת הגובה הגדול מבין השניים שומרת מילת דוגמה גדולה ואת הכתובית הקטנה שלה בשורה אחת כשהן חולקות baseline. בטופס המס המוזכר למעלה, מספר השורות צנח מ-156 ל-97. טקסט אנכי ב-writing mode 1 (§9.7.4.3) הולך בנתיב נפרד: ה-glyph-ים האלה נאספים לעמודות, נקראים מימין לשמאל ומלמעלה למטה, עם מעבר שורה בכל החלפת עמודה
איזה טקסט ה-ExtractLoadedPageText כולל או משמיט?
ה-ExtractLoadedPageText מחזיר את הטקסט שמציג מציג. מאז v2.766.80 הוא עובד מה-glyph-ים הנראים בלבד, משמיט כל glyph שמרכז התיבה שלו נופל מחוץ ל-GetLoadedPageVisibleBox, שהוא ה-CropBox חתוך אל ה-MediaBox (§14.11.2). זה מסיר שורות slug וסימני מדפסת אחרים שהוצבו כטקסט מחוץ לאזור החיתוך. ה-ExtractLoadedPageGlyphs ממשיך בכוונה להחזיר כל glyph של זרם התוכן של העמוד, כך שעדיין תוכלו למצוא את החומר הזה כשאתם צריכים אותו. המסנן הוא בדיקת תיבה, לא בדיקת נראות: טקסט שמוסתר על ידי נתיב חיתוך, נצייר בלבן או מכוסה בתמונה עדיין חולץ
var
Pdf: THotPDF;
Glyphs: THPDFGlyphArray;
PageText: UnicodeString;
L, B, R, T: Single;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.LoadFromFile('trimmed-proof.pdf');
if Pdf.GetLoadedPageVisibleBox(0, L, B, R, T) then
Writeln(Format('Visible box: %.1f %.1f %.1f %.1f', [L, B, R, T]));
// כל glyph של זרם התוכן של העמוד, שורת ה-slug כלולה
if Pdf.ExtractLoadedPageGlyphs(0, Glyphs) then
Writeln(Length(Glyphs), ' glyphs in the page content stream');
// רק מה שהעמוד מציג, עם טקסט Form XObject משולב פנימה
if Pdf.ExtractLoadedPageText(0, PageText) then
Writeln(PageText);
finally
Pdf.Free;
end;
end;
טקסט שנצייר דרך Form XObjects הוא חלק מטקסט העמוד מאז v2.768.3. כותרות עליונות, חותמות ו-watermarks חיים לעיתים קרובות מאוד בטפסים, וכמה מסמכי תקנים איבדו 30 עד 35 אחוז מהתווים שלהם לפני השינוי. ה-THotPDF.InterpretContentWithForms רושם כל Do יחד עם ה-CTM התקף, מפרש את הטופס בה-/Matrix שלו כפול אותו CTM (§8.10.1), ומשלב את ה-glyph-ים של הטופס במיקום ה-Do, תוך רקורסיה אל טפסים מקוננים. טופס בלי /Resources משלו שואל את אלה של הזרם שמצייר אותו, כפי ש-§7.8.3 מרשה. glyph-ים של טפסים נושאים TokenIndex = -1, וה-ExtractLoadedPageGlyphs עדיין מחזיר רק glyph-ים של זרם העמוד, כי חיפוש, החלפה והשחרה כותבים שינויים חזרה דרך ה-TokenIndex והיו עורכים בייטים לא נכונים אם glyph של טופס היה מחליק פנימה. שני פישוטים שווה להכיר: טקסט טופס אינו נחתך אל ה-/BBox של הטופס, והרקורסיה עוצרת ב-12 רמות במקום באמצעות זיהוי מעגלים, כך שטופס משובש שמצייר את עצמו חוזר על הטקסט שלו עד שהוא מגיע לתקרה הזאת
למה טקסט אחרי אופרטור Q פוענח כג'יבריש?
טקסט אחרי Q יכול היה להתפענח שלא נכון לפני v2.766.73 כי המחלץ שמר רק את ה-CTM על q. פרמטרי מצב הטקסט, כלומר פונט, גודל, Tc, Tw, Tz, TL, מצב רינדור ו-rise, שייכים למצב הגרפיקה (§9.3.1), ולכן ה-Q חייב לשחזר אותם יחד עם כל השאר על המחסנית (§8.4.2). דוח תעשייה אחד בחר פונט Identity-H דו-בייתי בתוך q … Q ואז הציג טקסט WinAnsi חד-בייתי בלי Tf משלו. המחלץ שמר על הפונט הפנימי, קרא את הנקודות-מובילות ואת המילה “Adobe” בתוכן העניינים כקודים דו-בייתיים, והשמיט 15% מתווי העמוד. מחסנית ה-q/Q של המפרש מחזיקה עכשיו את מצב הטקסט המלא. כללי החילוץ המתוארים כאן חלים על כל עמוד, ולכן מסמך שלם יכול ללכת אל קובץ בקריאה אחת
var
Output: TFileStream;
Pages: Integer;
begin
Output := TFileStream.Create('report.txt', fmCreate);
try
// טווח ריק = כל העמודים; form feed בין עמודים; BOM של UTF-8
Pages := Pdf.ExtractLoadedPagesTextToStream(Output, '', #12, True);
Writeln(Pages, ' pages extracted');
finally
Output.Free;
end;
end;
איזה API טקסט של HotPDF כדאי לכם להשתמש בו?
ה-ExtractLoadedPageText נשאר בסדר זרם התוכן, שהוא ברירת המחדל הנכונה עבור חיפוש ואינדוקס; שרשרת הפענוח מתחתיו מכוסה בחילוץ טקסט מ-PDF טעונים עם HotPDF. עבור מסמכים מתויגים שסדר היצירה שלהם חשוב, חילוץ טקסט בסדר המבנה הולך בעץ המבנה במקום לנחש מהגיאומטריה, ועבור נתונים נעולים בטבלאות, חילוץ טבלאות מטיפוס על פני מעברי עמודים מחזיר תאים ולא שורות. הפניית API מלאה והורדת ניסיון נמצאות בעמוד המוצר של HotPDF Delphi PDF Component