מאמר טכני

המרת PDF ל-Markdown ול-DOCX ב-Delphi עם PDFlibPas

PDFlibPas ממיר תוכן PDF לשני פורמטים ניתנים לעריכה ללא אוטומציית Office. ExportPageMarkdown ו-ExportDocumentMarkdown מחזירים Markdown סמנטי עם כותרות משוערות, רשימות ממוספרות ולא ממוספרות וטבלאות בסגנון pipe, בעוד ש-SaveDOCXToFile ו-SaveDOCXToStream כותבים חבילת WordprocessingML המכילה פסקאות, כותרות, מספור רשימות מקורי, טבלאות מזוהות, עיצוב גופנים, מעברי עמוד ותמונות PNG ממוקמות

שניהם רצים לגמרי ב-Pascal, על שרת, ללא Word מותקן וללא COM. האילוץ הזה הוא הסיבה שהתכונה קיימת בספריית PDF ולא בכלי שולחני

מדוע "PDF ל-Word" קשה באמת?

מפני שעמוד PDF אינו מכיל פסקאות. הוא מכיל אופרטורי הצגת-טקסט שממקמים ריצות של גליפים בקואורדינטות, בסדר כלשהו שהמחולל פלט אותן בו, ללא כל התחייבות לציין ששתי ריצות שייכות לאותו משפט, שלא לדבר על אותו פריט רשימה. הפורמט תוכנן לתאר עמוד מודפס במדויק, והוא מצליח בכך על ידי השמטת המבנה שהפיק את העמוד

אז כל ממיר צריך לשחזר את מה שהמחולל השליך. קיבוץ שורות מגיע ממרווח אנכי וממיושור בסיס. גבולות פסקה מגיעים משינויי מרווח ומהזחה. כותרת היא שורה שהגופן שלה גדול או כבד יותר מגוף הטקסט ושעומדת בנפרד ממה שבא אחריה. רשימה היא רצף פסקאות שמתחילות בתו תבליט או בתבנית מספור. טבלה היא רשת של בלוקי טקסט שהקצוות שלהם מתיישרים בין שורות ועמודות. כל אחת מאלה היא הסקה, והסקה פירושה תוצאה טובה במסמכים שעוקבים אחרי מוסכמות טיפוגרפיות רגילות ותוצאה בינונית במסמכים שלא

מסמכי PDF מתויגים הם החריג, וחריג גדול. כשהמסמך נושא עץ מבנה, תפקידי הפסקה, הכותרת, הרשימה והטבלה נרשמים ולא מוסקים, וזו הסיבה שעבודת הנגישות המתוארת במבנה נגישות של PDF מתויג משתלמת גם באיכות ההמרה. אם שולטים במחולל, תיוג הפלט הוא הדבר בעל המנוף הגבוה ביותר שאפשר לעשות עבור כל מי שאחר כך יצטרך להמיר אותו

ייצוא Markdown, עמוד אחד בכל פעם

נתיב ה-Markdown הוא זה שכדאי לפנות אליו כשהיעד הוא צינור טקסט: אתר תיעוד, אינדקס חיפוש, קורפוס אחזור למערכת עוזר. האפשרויות הן מסכת ביטים: PDF_MARKDOWN_INCLUDE_PAGE_MARKERS, PDF_MARKDOWN_DETECT_HEADINGS, PDF_MARKDOWN_PRESERVE_STYLES, כאשר PDF_MARKDOWN_DEFAULT משלב את שלושתן

var
  Pdf: TPDFlib;
  Md: WideString;
begin
  Pdf := TPDFlib.Create;
  try
    Pdf.LoadFromFile('handbook.pdf', '');

    // עמוד אחד, כמחרוזת
    Md := Pdf.ExportPageMarkdown(1, PDF_MARKDOWN_DEFAULT);

    // טווח עמודים, זורם לדיסק כ-UTF-8 ללא BOM
    Pdf.SaveMarkdownToFile('1-40',
      PDF_MARKDOWN_DETECT_HEADINGS or PDF_MARKDOWN_PRESERVE_STYLES,
      'handbook.md');
  finally
    Pdf.Free;
  end;
end;

סמני עמוד מוכיחים את ערכם בעבודת אחזור. קטע טקסט שנושא את העמוד שממנו הגיע ניתן לציטוט מדויק, וקורא שעוקב אחרי הציטוט מגיע בדיוק לאן שהטענה נמצאת. כבה אותם כשה-Markdown מיועד לקריאה אנושית, שם גבולות עמוד מהפריסה המקורית הם רעש בלבד

נקודות הכניסה הזורמות חשובות למסמכים גדולים. SaveMarkdownToStream ו-SaveMarkdownToFile כותבים UTF-8 עמוד אחד בכל פעם ואינם צוברים את הפלט המלא בזיכרון, כך שמדריך בן 900 עמודים לא הופך תחילה למחרוזת בת 900 עמודים בזיכרון. גם היעדר סימן סדר הבתים מכוון: BOM בקובץ Markdown מבלבל מספר מפתיע של מחוללי אתרים סטטיים וכלי diff

DOCX ללא Office על המכונה

כותב ה-DOCX מפיק את החבילה בעצמו: ערכי ZIP הכתובים כ-Deflate גולמי עם בדיקות CRC, חלקי WordprocessingML, והקשרים שמאגדים אותם. שום דבר לא קורא ל-Word, מה שאומר שההמרה רצה על שרת ללא ראש, בתוך חשבון שירות, בתוך קונטיינר, בכל המקומות שבהם אוטומציית Office אינה מורשית, אינה יציבה או אסורה

var
  Pdf: TPDFlib;
  Target: TFileStream;
begin
  Pdf := TPDFlib.Create;
  Target := TFileStream.Create('handbook.docx', fmCreate);
  try
    Pdf.LoadFromFile('handbook.pdf', '');
    Pdf.SaveDOCXToStream('1-40',
      PDF_DOCX_INCLUDE_IMAGES or PDF_DOCX_DETECT_HEADINGS or
      PDF_DOCX_PRESERVE_STYLES or PDF_DOCX_PRESERVE_PAGE_BREAKS,
      Target);
  finally
    Target.Free;
    Pdf.Free;
  end;
end;

נתוני תמונה נכתבים תוך כדי עיבוד כל עמוד במקום להיאסף ולהתווסף בסוף, כך ששיא הזיכרון עוקב אחר עמוד אחד ולא אחר המסמך כולו. סדר העמודים המקורי נשמר, ועמוד ה-PDF שהיה נבחר משוחזר לאחר מכן, וזה חשוב כשהייצוא הוא שלב אחד בתוך עבודה ארוכה יותר שהיה לה עמוד נבחר מסיבות אחרות

מה קניית עטיפה דטרמיניסטית נותנת לך?

שחזוריות בייט-לבייט. שתי המרות של אותו קלט עם אותן אפשרויות מפיקות את אותה חבילה, מה שאומר שאפשר לגזור hash מהפלט כדי לזהות שינוי, להשוות שתי גרסאות בנייה של מסמך שנוצר, ולשמור במטמון באגרסיביות מבלי לחשוש שקלט זהה הפיק תוצר שונה

אוטומציית Office לא יכולה להבטיח זאת. היא מטמיעה חותמות זמן, מזהי גרסה ומטא-נתונים תלויי-מכונה, כך שאותו מסמך שהומר פעמיים נבדל בדרכים שמסכלות hash. אותו היגיון מניע גם את מזהי ה-PDF הדטרמיניסטיים הנדונים במזהי PDF דטרמיניסטיים לבניות ניתנות לשחזור: כשהפלט ניתן לשחזור, האימות הופך להשוואה במקום לבדיקה

איפה הפלט טוב, ואיפה לא

כדאי להיות כנים עם המשתמשים בעניין הזה, מפני שאיכות ההמרה משתנה יותר לפי הקלט מאשר לפי הממיר. מסמכי PDF מתויגים ומסמכים עסקיים שנוצרו בצורה נקייה, חשבוניות, דוחות, חוזים, מומרים היטב: כותרות נוחתות ככותרות, טבלאות שורדות, רשימות ממוספרות מחדש נכון ב-Word. פריסות אקדמיות דו-עמודתיות מומרות באופן סביר אם גיאומטריית העמודות סדירה. טבלאות שחוצות מעברי עמוד מורכבות מחדש על ידי הסקה ולפעמים מתפצלות. חומר שיווקי מעוצב בכבדות, שבו הטקסט ממוקם לאפקט חזותי ולא בסדר קריאה, מומר בצורה גרועה, ושום כמות הסקה לא מתקנת זאת

מסמכים סרוקים הם מקרה נפרד לגמרי. עמוד שהוא תמונה גדולה אחת לא מכיל אובייקטי טקסט, כך שאין מה לייצא עד שקיימת שכבת טקסט; נתיב ה-OCR שמפיק אותה הוא תנאי מוקדם, לא אפשרות. לפני הרצת מנה גדולה, כדאי לדגום תריסר קבצים מייצגים ולבחון את הפלט, ולשקול למנות תחילה את רכיבי העמוד, כמתואר בחיפוש טקסט ומניית רכיבי עמוד, כדי לראות מה בעצם נמצא בעמודים

לצינורות עוזר ואחזור נתיב ה-Markdown הוא בדרך כלל היעד הטוב יותר: כותרות הופכות לגבולות קטע, טבלאות נשארות קריאות כטבלאות pipe, וסמני עמוד נותנים לכל קטע מיקום ניתן לציטוט. לעריכה אנושית, DOCX הוא התשובה, מפני שמה שהמשתמש רוצה אינו הטקסט אלא היכולת לשנות אותו

PDFlibPas היא ספריית PDF ל-Delphi, ל-C++Builder ול-Lazarus עם ממשקי DLL ו-ActiveX תואמים, כך שאותן קריאות ייצוא זמינות מ-C#, מ-C++ או ממארחי סקריפטים. תיעוד מלא וגרסת ניסיון נמצאים בדף ספריית PDFlibPas ל-Delphi