מאמר טכני

מזהי אובייקט עמוד PDFium מיושנים אחרי טרנספורם בדלפי

כש-FPDFPage_TransFormWithClip כותבת מחדש עמוד, כל מזהה FPDF_PAGEOBJECT שאתה כבר מחזיק עדיין מתאר את הפענוח מלפני הטרנספורם. PDFium Component עבור דלפי ו-C++Builder פותרת זאת בתוך TransformPageContent, שמורידה את עמוד הטקסט, מייצרת תוכן מחדש, ואז טוענת מחדש את העמוד כך ששאילתות מאוחרות רואות את הקואורדינטות החדשות

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

למה גבולות אובייקט עמוד מתיישנים ללא שגיאה?

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

תקן ISO 32000-1 §7.8.2 מגדיר את זרם התוכן כרצף האופרטורים שמצייר עמוד, ו-§8.3.3 מגדיר כיצד מטריצת הטרנספורמציה הנוכחית ממפה את מרחב-המשתמש למרחב-ההתקן. טרנספורם ברמת-עמוד מבוטא בעטיפה וכתיבה-מחדש של האופרטורים האלה, לא בעריכת קואורדינטות לכל-אובייקט במקום. אז הקואורדינטות שהאובייקטים נושאים אולי לא משתנות כלל; מה שמשתנה הוא המטריצה שבתוקף כשהם מצוירים. כל מזהה שנותח תחת המטריצה הישנה עונה לשאלות גיאומטריה תחת המטריצה הישנה, ועונה בלי תלונה

מה FPDFPage_TransFormWithClip בעצם כותבת מחדש

היא כותבת מחדש את העמוד, לא את תמונות המצב שלך. FPDFPage_TransFormWithClip לוקחת FS_MATRIX ומלבן חיתוך FS_RECTF ומחילה את שניהם על כל תוכן העמוד. זו הקריאה הנכונה עבור שוליים, קנה-מידה imposition, ונרמול עמוד בגודל מוזר כנגד תיבת יעד. זו הקריאה השגויה להושיט אליה יד אם אתה מצפה שמזהים קיימים יעקבו לאורך, וגם שווה זכירה שהיא נוגעת רק בתוכן העמוד: הערות הן שכבה נפרדת וזקוקות ל-TransformPageAnnotations, שמעבירה את אותם שישה מקדמי מטריצה ל-FPDFPage_TransformAnnots

var
  Info: TPdfPageObjectInfo;
  Scale: FS_MATRIX;
  Clip: TPdfRectangle;
begin
  Pdf.PageNumber:= 1;
  Info:= Pdf.PageObjectInfo(0);           // snapshot taken before the transform

  Scale.a:= 0.9;   Scale.b:= 0.0;
  Scale.c:= 0.0;   Scale.d:= 0.9;
  Scale.e:= 29.7;  Scale.f:= 42.0;        // 5% margin, A4 in points
  Clip:= Pdf.GetPageBox(pbMedia);
  Pdf.TransformPageContent(Scale, Clip);

  // Info.Bounds still holds pre-transform geometry, and Info.Handle now
  // points into a page that TransformPageContent has already replaced
end;

סדר הרענון ש-TransformPageContent משתמשת בו

ארבעה צעדים, בסדר הזה: הורד את עמוד הטקסט, בצע טרנספורם, ייצר תוכן, טען מחדש את העמוד. TPdf.TransformPageContent מריצה בדיוק את הרצף הזה. היא קוראת ל-CheckPageActive, מעתיקה את המטריצה והחיתוך לתוך צורות הרשומה הילידיות שלהם, קוראת ל-UnloadTextPage, ואז FPDFPage_TransFormWithClip, ואז UpdatePage, שהיא העטיפה סביב FPDFPage_GenerateContent, ולבסוף ReloadPage

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

// After the transform, re-enumerate. Do not reuse anything captured earlier.
var
  I: Integer;
  Info: TPdfPageObjectInfo;
begin
  Pdf.TransformPageContent(Scale, Clip);   // unload text page, transform,
                                           // generate content, reload page
  for I:= 0 to Pdf.ObjectCount- 1 do
  begin
    Info:= Pdf.PageObjectInfo(I);          // handle and bounds from the new parse
    if Info.Bounds.Right> PageWidth then
      Log('object '+ IntToStr(I)+ ' still overflows after scaling');
  end;
end;

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

אל תישא מזהים לאורך הטעינה-מחדש

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

האם גטר יכול להיכשל ועדיין להיראות כמו נתונים תקפים?

כן, וזה החצי השני של אותה בעיה. FPDFPageObj_GetRotatedBounds ו-FPDFPageObj_GetIsActive הם גטרים עם ארגומנט-פלט: הם מחזירים דגל הצלחה int וכותבים את התשובה האמיתית לתוך ארגומנט הפניה. שניהם יכולים להחזיר FALSE עבור אובייקט שנוצר אבל שהעמוד שלו עדיין לא פוענח-מחדש. כשזה קורה ארגומנט הפלט נשאר לא-נגוע, ורשומת Pascal שאותחלה עם Default(TPdfPageObjectInfo) כולה אפסים, כך שהקורא רואה מרובע עם ארבע נקודות בראשית ודגל Active של False. קריאה שנכשלה קודמה בשקט לנתונים שנראים סבירים

TPdfPageObjectInfo עונה על זה עם sentinels מפורשים. HasRotatedBounds נושא את התוצאה של קריאת FPDFPageObj_GetRotatedBounds, HasActiveState נושא את התוצאה של FPDFPageObj_GetIsActive, ושדות הגיאומטריה והמצב נכתבים רק כש-sentinel המתאים הוא True. אותה צורה חוזרת על פני הרשומה עבור שאר הגטרים עם ארגומנט-פלט, כך ש-HasMatrix, HasFillColor, HasStrokeColor, ו-HasStrokeWidth כולם אומרים אותו דבר: הקריאה הילידית הצליחה והשדה השכן משמעותי

Info:= Pdf.PageObjectInfo(I);

if Info.HasRotatedBounds then
  // RotatedBounds is array [1..4] of TPdfPoint, in draw order
  UseQuad(Info.RotatedBounds[1], Info.RotatedBounds[2],
          Info.RotatedBounds[3], Info.RotatedBounds[4])
else
  // the native call failed; fall back to the axis-aligned rectangle
  UseRect(Info.Bounds);

if Info.HasActiveState and (not Info.Active) then
  SkipObject(I);         // genuinely inactive
// if HasActiveState is False, the object state is unknown, not inactive

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

איפה זה עדיין נושך

שלושה גבולות כנים. ראשית, הרענון הוא לכל-עמוד: טרנספורם עמוד שתיים וכל מזהה שאתה מחזיק עבור עמוד אחד לא מושפעים, אבל עכשיו יש לך שני עמודים שפוענחו בזמנים שונים ועליך לזכור אילו תמונות-מצב הגיעו מאיזה. שנית, יציבות אינדקס לא מובטחת על פני ייצור-מחדש של תוכן — אחרי הטעינה-מחדש, אינדקס 3 הוא מה שאינדקס 3 הוא בפענוח החדש, כך שזהה מחדש אובייקטים לפי הסוג והגיאומטריה שלהם ולא בהנחה שמיקומים החזיקו. שלישית, מלבן החיתוך ב-FPDFPage_TransFormWithClip מוחל על תוכן העמוד ולא משנה גודל אף אחת מתיבות העמוד; אם אתה מקנה-מידה תוכן כלפי מטה כדי ליצור שוליים, ה-MediaBox עדיין באותו גודל שהוא תמיד היה, וצופה יציג את הגיליון המקורי עם השרטוט מכווץ בתוכו. שום דבר מזה אינו אקזוטי — זו ההשלכה הרגילה של API ב-C שמוסר מצביעים לתוך מצב מפוענח ומשאיר את מחזור-החיים לקורא. התיקון הוא זה שעובד בכל מקום אחר: הגדר בדיוק מתי תמונת-מצב פגה תוקף, רענן בגבול ההוא, ולעולם אל תן לקריאה כושלת להתחפש לערך

אם אתה עובד דרך התנהגות מטריצה בכלליות רבה יותר, סדר הכפל שמכריע איפה טרנספורם נוחת מכוסה בהמאמר על prepend, append, ו-pivot עם מטריצות. ה-API של הטרנספורם ואובייקט העמוד המתוארים כאן משתלבים עם PDFium Component עבור דלפי ו-C++Builder, שעמוד המוצר שלו נושא את הפניית המלאה עבור רשומת תמונת-מצב אובייקט העמוד ושדות ה-sentinel שלה