מאמר טכני

קריאה וכתיבה של תוכן מסומן ב-PDF בדלפי

תוכן מסומן הוא המנגנון ש-ISO 32000-1 §14.6 מגדיר לתיוג תוכן עמוד, וגם PDF מתויג ו-PDF/UA בנויים עליו. רכיב PDFium חושף אותו ישירות: PageObjectMarks קורא כל תג BDC ואת רשימת המאפיינים שלו מאובייקט עמוד, AddPageObjectMark כותב אחד, RemovePageObjectMark מוחק אחד, ו-PageObjectMarkedContentID מדווח את ה-MCID שמקשר תוכן לעץ המבנה

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

מהו סימון, בבייטים?

אופרטור BDC עם שם תג ורשימת מאפיינים אופציונלית, שנסגר על ידי EMC. בזרם התוכן זה נראה כמו /P <</MCID 3>> BDC ... EMC: התג /P נותן שם לתפקיד, המילון נושא מאפיינים, וכל מה שבין האופרטורים הוא התוכן המסומן. אובייקט עמוד בתוך אותו מקטע נושא את הסימון, שזה מה ש-PDFium מחזיר ומה שרכיב PDFium הופך לרשומה

TPdfContentMark מחזיק ב-handle, ב-Name של התג, ובמערך של TPdfContentMarkParam. לכל פרמטר יש Key, Kind ושדה ערך אחד משמעותי שנבחר על ידי אותו סוג: pmpInt, pmpFloat, pmpString או pmpBlob. הסוג מגיע מדוח הסוג של PDFium עצמו ולא מאיזה getter שקרה להצליח, שזה ההבדל בין קריאת רשימת מאפיינים ובין ניחוש של אחת

var
  Marks: TPdfContentMarks;
  M: TPdfContentMark;
  P: TPdfContentMarkParam;
  I: Integer;
begin
  Pdf.PageNumber := 1;                    // PageNumber is 1-based
  for I := 0 to Pdf.ObjectCount - 1 do    // page object indexes are 0-based
  begin
    Marks := Pdf.PageObjectMarks(I);
    for M in Marks do
    begin
      Memo1.Lines.Add('mark ' + M.Name +
        ' (MCID ' + IntToStr(Pdf.PageObjectMarkedContentID(I)) + ')');
      for P in M.Params do
        case P.Kind of
          pmpInt:    Memo1.Lines.Add('  ' + P.Key + ' = ' + IntToStr(P.IntValue));
          pmpString: Memo1.Lines.Add('  ' + P.Key + ' = ' + P.StringValue);
          pmpFloat:  Memo1.Lines.Add('  ' + P.Key + ' = ' + FloatToStr(P.FloatValue));
          pmpBlob:   Memo1.Lines.Add('  ' + P.Key + ' = ' +
                       IntToStr(Length(P.BlobValue)) + ' bytes');
        end;
    end;
  end;
end;

למה pmpUnknown אומר שני דברים שונים

pmpUnknown מוחזר כש-PDFium מדווח על FPDF_OBJECT_UNKNOWN, ו-PDFium גם מחזיר את זה עבור מפתח שאינו קיים. אי אפשר להבחין בין שני המקרים בשכבה זו, והעמדת פנים אחרת תהיה גרועה מלומר זאת

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

רשומת סימון היא תמונת מצב, לא handle שבבעלותך

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

זוהי אותה משמעת שחלה על handle של אובייקט עמוד באופן כללי ב-PDFium, והיא תופסת אנשים באותו מקום: פקד רשימה שמאוכלס ברשומות סימון, משתמש שנונות לעמוד אחר, וקריסה שנראית לא-קשורה לניווט. העתק את הערכים שאתה צריך — השם, המפתחות, המספרים — ותן ל-handle ללכת. ההערות על handle של אובייקט עמוד שמתיישן לאחר transform מכסות את הכלל הכללי וכיצד הוא נושך במקומות אחרים

הוספת סימון, וצעד השמירה שקל לפספס

AddPageObjectMark לוקח את אינדקס אובייקט העמוד, שם תג וסט פרמטרים שלם. פרמטרים נכתבים כסט במקום להטלאות מפתח אחד בכל פעם, ולכן ל-TPdfContentMarkParam אין sentinels מסוג Has* — מקרה "עדכן שדה אחד של רשומה קיימת" שאלה היו שומרים עליו אינו מתעורר

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

var
  Params: TPdfContentMarkParams;
begin
  SetLength(Params, 1);
  Params[0].Key := 'MCID';
  Params[0].Kind := pmpInt;
  Params[0].IntValue := NextMcid;
  Pdf.AddPageObjectMark(ObjectIndex, 'P', Params);   // rebuilds the content stream
  Pdf.UpdatePage;
  Pdf.SaveAs('tagged-out.pdf');
end;

מה זה עושה ומה לא הופך מסמך לכזה

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

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

רכיב PDFium נותן ליישומי דלפי, C++Builder ו-Lazarus ממשק API VCL ברמה גבוהה מעל מנוע PDFium, עם תוכן מסומן, עצי מבנה ותיקוף נגישות שמגיעים מקוד פסקל רגיל — ראה את דף המוצר של רכיב PDFium למשטח ה-API המלא