אל השולחן מגיעה בקשה: לקחת אצווה של דוחות שכבר רנדרו, להשחיר את מספרי החשבון, ולארוז שני עמודים על גיליון אחד כדי לחסוך בנייר. שני חלקי המשימה הם ניתוח content-stream של PDF שלא יצרת, ולכן אין לך קנבס ידידותי לצייר עליו ואין מנהל גופנים להישען עליו. אתה עורך ישירות את גרף האובייקטים של מסמך טעון, ומצרף פקודות ציור גולמיות לדף שמישהו אחר פרש. HotPDF חושף בדיוק שתי נקודות כניסה לזה, והמסוכנת יותר מבין השתיים היא זו שנראית תמימה
HotPDF הוא רכיב PDF מקורי ל-VCL עבור Delphi ו-C++Builder. ממשק ה-loaded-document של סבב תשע הוסיף את השיטות הראשונות שיוצרות תוכן חדש לגמרי על דף שפתחת מהדיסק, במקום דף שבנית מאפס. שתי השיטות שבמרכז המאמר הן RedactLoadedRect, שמציירת מלבן אטום מעל אזור מסוים, ו-StitchLoadedPage, שמשנה קנה מידה לדף אחד ומציירת אותו על גבי דף אחר. שתיהן עובדות על ידי כתיבת אופרטורים של content-stream לפי ISO 32000-1 §8.5 אל ה-/Contents של הדף. להבין מה האופרטורים האלה עושים, וחשוב לא פחות מה הם לא עושים, הוא ההבדל בין כלי עובד לבין דליפת נתונים
הוספת אופרטורים לדף טעון
כאשר בונים דף עם ממשק HotPDF הרגיל, הרכיב הוא הבעלים של ה-content stream והוא מסדר עבורך את הקריאות ל-TextOut ולגרפיקה וקטורית. דף טעון הוא סיפור אחר: ה-/Contents שלו הוא stream קיים, ייתכן שמשותף, ייתכן שחלק ממערך תוכן, וצריך לשלב בו תוספות בלי להשחית את מה שכבר שם. סבב תשע הוסיף שלושה עזרנים קטנים שהופכים את זה לבטוח. NewIndirectStream מקצה THPDFStreamObject עקיף חדש עם באפר ריק ורשומת /Length 0; ResolveLoadedStream עוקב אחרי הפניה עקיפה עד ל-stream הבסיסי; ו-AppendLoadedStream כותב בתים גולמיים בסוף ה-stream וכותב מחדש את /Length כדי שהאובייקט הנשמר יישאר תקין
הדפוס ששתי השיטות הציבוריות עוקבות אחריו זהה. מוצאים את ה-/Contents של הדף, מפענחים אותו ל-stream, ואם אין stream שמישים, יוצרים אחד ומצרפים אותו. אחר כך מוסיפים את האופרטורים. מכיוון שהבתים החדשים נכתבים בסוף ה-stream, מודל ה-painter מבטיח שהם יוצגו מעל כל מה שהפריסה המקורית ציירה. הסדר הזה הוא כל המנגנון מאחורי מלבן ההסתרה, והוא גם הסיבה שהמלבן הזה הוא לא מה שרוב האנשים מניחים שהוא
RedactLoadedRect: כיסוי אטום, לא מחיקה
RedactLoadedRect מקבלת אינדקס דף מבוסס-אפס, ארבע קואורדינטות במרחב המשתמש, ושלושה רכיבי צבע בטווח 0 עד 1:
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('statement.pdf') > 0 then
begin
// Cover the account-number band on page 1 with solid black.
// Coordinates are PDF user space: origin bottom-left, points.
Pdf.RedactLoadedRect(0, 56, 690, 320, 706, 0, 0, 0);
Pdf.SaveLoadedDocument('statement-covered.pdf');
end;
finally
Pdf.Free;
end;
end;
מאחורי הקלעים השיטה פולטת שלושה אופרטורים אל ה-content stream: קביעת צבע מילוי ב-DeviceRGB (r g b rg), נתיב מלבני (x y w h re), ומילוי (f). הרוחב והגובה נגזרים כ-X2 - X1 ו-Y2 - Y1, כך שאתה מספק שתי פינות מנוגדות והשיטה מחשבת את הממדים. תן 0, 0, 0 לצבע ותקבל פס שחור; תן 1, 1, 1 למלבן לבן שמתאים לדף לבן. הקואורדינטות הן מרחב המשתמש של הדף הטעון עצמו, כלומר המקור הוא הפינה השמאלית-תחתונה והיחידות הן נקודות, וגם זה אומר שאתה צריך את ה-/MediaBox של הדף כדי למקם משהו בדיוק; GetLoadedPageBox עם pbMediaBox נותן לך את זה
קרא את זה פעמיים: מלבן מלא מכסה תוכן מבחינה חזותית, אבל לא מסיר אותו. הטקסט, התמונה או הגרפיקה הווקטורית שמתחת למלבן עדיין קיימים ב-PDF, עדיין בגרף האובייקטים, ועדיין ניתנים לחילוץ על ידי כל מי שמעתיק את הדף, מריץ מחלץ טקסט או פשוט מוחק את המלבן שלך מה-content stream. זו הסוואה חזותית, לא redaction במובן המשפטי או האבטחתי. אם אתה מסתיר נתונים רגישים באמת, מספרי חשבון, רשומות רפואיות, זהויות, או כל דבר שמפוקח רגולטורית, כיסוי שלהם בקופסה שחורה ושליחת הקובץ הוא דליפת נתונים שמחכה להתגלות. redaction אמיתי דורש מחיקה של אובייקטי התוכן הבסיסיים, לא צביעה מעליהם
שם השיטה אומר "Redact", וזה אזהרה מועילה לגבי האופן שבו יפרשו את התוצאה, לא הבטחה לגבי מה היא מוחקת. המימוש כן לגבי זה בהערה שלו עצמו: הוא מכנה את עצמו "visual redaction primitive" ומציין ש-redaction שמסירה תוכן דורשת מפרש content-stream שעובר על האופרטורים הקיימים וכותב אותם מחדש. מסלול ה-loaded-document של HotPDF לא עושה זאת כאן. לכן כלל הבטיחות צר: השתמש ב-RedactLoadedRect להסתרה קוסמטית ולא רגישה, למשל הסתרת watermark של טיוטה, ריקון אזור לפני צילום מסך, או כיסוי לוגו מיושן בהגהה פנימית. ברגע שהדבר שמתחת לקופסה עלול להיות בעייתי אם ידלוף, זו השיטה הלא נכונה, והפתרון הנכון הוא ליצור מחדש את המסמך בלי הנתונים או להשתמש בצינור הסרת תוכן אמיתי
StitchLoadedPage: קנה מידה, הזזה, ציור
פריסת N-up היא הבעיה הידידותית יותר, כי שום דבר לא מוסתר, רק מסודר מחדש. StitchLoadedPage מקבלת אינדקס דף יעד, אינדקס דף מקור, היסט X/Y וגורם קנה מידה, והיא מציירת את דף המקור על היעד במיקום ובגודל האלה:
// Overlay page 2 (index 1) onto page 1 (index 0),
// scaled to 70% and nudged up-right.
Pdf.StitchLoadedPage(0, 1, 40, 380, 0.7);
// Convenience 2-up: source page on the right half of the target.
Pdf.StitchLoadedPageSideBySide(0, 1);
מחרוזת האופרטורים שהיא מוסיפה היא רצף סטנדרטי של transform-and-paint: q לשמירת מצב הגרפיקה, מטריצת cm שמחזיקה את קנה המידה באלכסון ואת ההיסט במקומות ההזזה, /StitchSrc Do להפעלת אובייקט חיצוני, ו-Q לשחזור המצב. הזוג q/Q חשוב כי הוא מבודד את ה-transform כך שהדף המודבק לא ידליף את מערכת הקואורדינטות שלו לכל מה שמתווסף אחר כך. השיטה גם שומרת מפני טעויות ברורות, אינדקסים מחוץ לטווח, יעד זהה למקור, או scale לא חיובי, שאותו היא מהדקת ל-1.0, ואז יוצאת בשקט במקום לזרוק חריגה, ולכן כדאי לבדוק את הקלטים כי no-op שקט נראה בדיוק כמו הצלחה
StitchLoadedPageSideBySide היא מעטפת נוחה דקה מעל השיטה הכללית. היא קוראת את רוחב ה-media-box של היעד, מחלקת אותו לשניים, וקוראת ל-StitchLoadedPage עם חצי הרוחב הזה כהיסט X ו-scale קבוע של 0.5, כך שדף המקור יושב במחצית הימנית. ה-0.5 הקשיח הזה מניח שלמקור וליעד יש אותו רוחב; אם לא, המקור לא ימלא את המחצית שלו בצורה נקייה, ואז תצטרך את StitchLoadedPage הכללית עם scale שתחשב בעצמך משתי תיבות ה-media box
האסטרטגיה הפשוטה של XObject והמחיר שלה לפי ISO
כאן המימוש עושה קיצור דרך מכוון שצריך להכיר לפני שסומכים על הפלט בין מציגים שונים. פריסת N-up תקינה עוטפת את תוכן דף המקור בתוך Form XObject, אובייקט drawable עצמאי שלפי ISO 32000-1 §8.10.1 חייב לשאת /Type /XObject, /Subtype /Form ותיבת חיתוך משלו /BBox. ה-stitch של סבב תשע לא בונה את העטיפה הזאת. במקום זאת הוא רושם את מילון הדף עצמו ישירות תחת /Resources /XObject של היעד בשם StitchSrc, ואז מצייר אותו עם Do. למילון דף ול-Form XObject יש מספיק במשותף במודל התוכן שלהם, שניהם מפנים ל-content stream ול-resource dictionary, ולכן הרבה קוראים יציגו את התוצאה
אבל זה לא Form XObject תואם. חסר לו הסימון /Subtype /Form וגם /BBox משלו, מה שאומר שלצרכן מחמיר מותר להתעלם מ-Do או לגזור אותו אחרת ממה שציפית. ב-TechnicalNotes של הסבב הזה כתוב זאת במפורש: הגישה "מציגה ברוב הקוראים" אבל "אינה Form XObject תואם ISO באופן מחמיר", ותאימות מלאה דורשת יצירה של Form XObject אמיתי כשלב נפרד. לכן התייחס לפלט של stitch כמו לכל מבנה לא תואם: אמת אותו במציגים הספציפיים שהלקוחות שלך מריצים, לא רק בזה שעל המכונה שלך, ואם אתה צריך ארכיון או PDF שעובר מאמתים מחמירים, אל תסמוך על הנתיב הזה. אותה משמעת חלה על כל מה שאתה בונה על גרף האובייקטים הטעון, ולכן בדיקת preflight של PDF ב-Delphi תופסת את מקומה בצינור השחרור בכל פעם שאתה משנה מסמכים בתכנות
היכן זה מתאים, והיכן לא
שתי השיטות הן כלי content-stream, ולכן המודל המנטלי הוא אותו מודל שבו אתה משתמש בציור ישיר. אם בנית דפים מאפס עם הרכיב, אופרטורי הווקטור והצבע שמאחורי הקריאות האלה ייראו מוכרים מהקישור שרטוט קנבס ב-HotPDF ב-Delphi; ההבדל היחיד הוא שכאן אתה מוסיף ל-stream שמישהו אחר כתב, ולא לכזה שבבעלותך. שמור שלושה גבולות בראש:
- הסתרה היא קוסמטית.
RedactLoadedRectמציירת מעל תוכן ואינה מוחקת אותו. לכל דבר רגיש, צור מחדש את המקור או השתמש במחיקה אמיתית של תוכן, כי קופסה שחורה אינה אבטחה - Stitch אינו תואם במכוון. דף המקור מופנה כ-pseudo-XObject ללא ה-§8.10.1
/Subtype /Formו-/BBox, לכן אמת את ההצגה במציגי היעד שלך והימנע מזה במקום שבו נדרשת ולידציה מחמירה - הקואורדינטות הן מרחב המשתמש של הדף. מקור בפינה השמאלית-תחתונה, יחידות בנקודות, לפי ה-media box של הדף עצמו. קרא את התיבה עם
GetLoadedPageBoxלפני שאתה ממקם משהו, כי הדף שטענת אולי לא בגודל שהנחת
בשימוש בתוך הגבולות האלה, הזוג הזה מכסה תהליך עבודה אמיתי: סידור מחדש של דפים להדפסה, הסתרת אזורים לא-סודיים, וכתיבת התוצאה חזרה עם SaveLoadedDocument, והכול בלי רינדור מלא מחדש. ממשק ה-loaded-document שכולל את רכיבי ה-stitch וה-mask האלה מגיע עם רכיב HotPDF עבור Delphi ו-C++Builder, לצד שיטות שדות הטופס, ההערות ו-FDF מאותו סבב