מערכת פקס לא רוצה את רינדור הדף שלך ב-24 ביט. גם לא תהליך הארכיון ששומר מיליון חשבוניות שנראות סרוקות, וגם לא מנוע ה-OCR שמבצע סף לכל דבר עוד לפני שהוא מחפש תו. שלושתם רוצים אותו דבר: bitmap נקי של 1-ביט, ביט אחד לכל פיקסל, שבו כל נקודה היא דיו או נייר. תן להם BMP צבעוני מלא והם יזרקו בכל מקרה 23 ביט לפיקסל, בדרך כלל עם שלב דיפדוף גרוע יותר מזה שהיית יכול לבצע בעצמך. השאלה המעניינת היא היכן צריך להתבצע אותו שינוי עומק, והתשובה ב-PDFlibPas מתבררת ככזו שאומרת משהו שימושי על הרחבת מנוע רינדור שאתה מעדיף לא לכתוב מחדש
PDFlibPas היא ספריית PDF מקורית ב-Object Pascal עבור Delphi ו-C++Builder. ליבת הרינדור שלה ממפה דף ל-bitmap ויכולה להפיק BMP, PNG, JPEG, WMF ועוד כמה פורמטים. מה שהיא לא ידעה לעשות עד לאחרונה היה להחזיר bitmap מונוכרומי אמיתי, או לרנדר רק חלק מהדף. שני הדברים הגיעו ב-v3.83.0, ושניהם נבנו כשכבות נוחות דקות מעל המנוע הקיים ולא כשינוי בממפה הפיקסלים עצמו. המגבלה הזו היא כל הסיפור
למה לבצע המרה ל-1 ביט אחרי הרינדור, ולא בתוך המנוע
הדרך הברורה לייצר תמונה של 1-ביט היא להגיד לממפה לצייר ב-1-ביט. זו גם הדרך ששוברת את כל השאר. ה-bitmap הפנימי של המנוע נוצר עם PixelFormat := pf24bit מקודד מראש בבנאי PDFlibRenderer, והמשטח של 24 ביט משותף לכל נתיבי הרינדור: ייצוא PNG, התצוגה דרך device context, פלט JPEG, הכול. אם תהפוך אותו ל-pf1bit במקור, לא הוספת יכולת מונוכרום, אלא פגעת בנאמנות הצבע של כל קורא בספרייה וחתמת על דיבוג של תריסר רגרסיות במורד השרשרת
לכן RenderPageToMonochromeFile בוחרת במסלול ההפוך. היא מרנדרת את הדף כרגיל, ל-BMP זמני של 24 ביט, ורק אחר כך מכווצת אותו ל-1 ביט כשלב עיבוד-אחרי. המנוע נשאר ללא מגע. ההתנהגות המונוכרומית חיה כולה בתוך מתודת הנוחות, ולכן היא לא יכולה להשפיע על מי שלא קורא לה. זו בדיוק פשרה ששווה לציין במפורש: שלב עיבוד-אחרי עולה בעוד הקצאת bitmap אחת וקובץ זמני, ובתמורה הוא משאיר את הליבה הנטענת מחוץ לתחום. עבור יכולת שנועדה לשרת מקרי קצה של פקס וארכיון, זה הצד הנכון של החשבון
var
Pdf: TPDFlib;
begin
Pdf := TPDFlib.Create(nil);
try
Pdf.LoadFromFile('invoice.pdf');
// 200 DPI is the classic Group 4 fax resolution; page index is 1-based
Pdf.RenderPageToMonochromeFile(200, 1, 'invoice-page1.bmp');
finally
Pdf.Free;
end;
end;
איך בפועל מתבצעת ההמרה ל-1 ביט
ההמרה נשענת על GDI ולא על לולאת סף ידנית, והבחירה הזו משפיעה על איכות הפלט. בתוך המתודה ה-BMP הזמני של 24 ביט נטען ל-TBitmap, נוצר TBitmap שני עם PixelFormat := pf1bit באותם ממדים, והפיקסלים עוברים בבליט יחיד:
// inside RenderPageToMonochromeFile, after loading the 24-bit ColorBmp
MonoBmp.PixelFormat := pf1bit;
MonoBmp.Width := ColorBmp.Width;
MonoBmp.Height := ColorBmp.Height;
// HALFTONE tells GDI to dither the 24-bit source down to 1-bit
SetStretchBltMode(MonoBmp.Canvas.Handle, HALFTONE);
StretchBlt(MonoBmp.Canvas.Handle, 0, 0, MonoBmp.Width, MonoBmp.Height,
ColorBmp.Canvas.Handle, 0, 0, ColorBmp.Width, ColorBmp.Height, SRCCOPY);
MonoBmp.SaveToFile('out.bmp');
הטריק הוא SetStretchBltMode עם HALFTONE. גם כשהמקור והיעד באותו גודל, כך שלא מתבצע קנה מידה, מצב המתיחה עדיין קובע איך GDI ממפה צבעים לפלטת 1-ביט. HALFTONE גורם להחלת דיפדוף halftone, והופך אזורים אפורים וקצוות טקסט מוחלקים לדפוסים של נקודות שחורות ולבנות במקום חיתוך חד אל הצבע הקרוב מבין השניים. אם מסירים את הקריאה למצב, או משתמשים בברירת המחדל BLACKONWHITE, תוכן אפור מתפרק לצורות סף גסות. עבור פלט של מסמכים סרוקים והכנה ל-OCR, התוצאה המדופדפת היא כמעט תמיד מה שאתה רוצה
פרט אחד הוא לא נתון למשא ומתן וקל לטעות בו: הרינדור הזמני חייב להיות BMP. RenderPageToMonochromeFile קוראת למנוע הכללי עם קוד אפשרויות 0, כלומר BMP. ארגומנט האפשרויות ב-RenderPageToFile הוא enum קטן של מספרים שלמים, והערכים אינם ניתנים להחלפה לצורך הזה: 0 הוא BMP, 1 JPEG, 2 WMF, 3 EMF, 5 PNG, וכן הלאה. אחר כך הממיר מבצע TBitmap.LoadFromStream על הקובץ הזמני. אם תיתן לו WMF, למשל על ידי העברת 2, הטעינה תזרוק "Bitmap image is not valid", כי Windows Metafile הוא זרם רשומות וקטורי, לא DIB. המרה ל-1 ביט היא פעולה רסטרית מקצה לקצה, ולכן גם הביניים חייב להיות פורמט רסטר
רינדור רק תת-אזור של דף
המתודה השנייה, RenderPageRegionToFile, מרנדרת רק מלבן מהדף במקום את כולו. מקרי השימוש מוכרים לכל מי שבנה פעם מציג מסמכים: חיתוך בלוק חתימה מתוך חוזה, יצירת אריח למפת זום של שרטוט גדול, או שליפת אזור מוטבע אחד עבור תמונה ממוזערת בלי לשלם על רינדור כל הדף ב-DPI גבוה. החתימה פשוטה:
// Clip is "Left,Top,Width,Height" in PDF points (72 pt = 1 inch)
// Here: a 2.5in x 1in box, one inch in from the top-left of the page
Pdf.RenderPageRegionToFile(150, 1, '72,72,180,72', 'sig-block.bmp');
מחרוזת ה-clip היא ארבעה מספרי double מופרדים בפסיקים, שנפרשת ידנית בתוך המתודה כדי לעקוף מוזרויות של locale ושל DelimitedText. מתוך הרוחב והגובה המתודה מחשבת את גודל ה-bitmap בפלט כ-Round(Width * DPI / 72) על Round(Height * DPI / 72), מקצה bitmap בזיכרון מסוג pf24bit בדיוק בגודל הזה, ומרנדרת אל תוך ה-device context שלו דרך RenderPageToDCClip. קובץ התוצאה מכיל רק את המלבן שנחתך, בגודל האזור ולא בגודל הדף כולו
פרמטר החיתוך שלא עשה דבר
וכאן העבודה הייתה חדה יותר ממה שהיא נראית. ל-RenderPageToDCClip היה פרמטר Clip במשך זמן רב, והוא היה שקר. הקריאה קיבלה את הארגומנט, העבירה אותו ל-TPDFPageTree.RenderPageToDC, והמימוש הזה התעלם ממנו לחלוטין, בלי להעביר אותו למנוע. יכולת להעביר כל מלבן שרצית ולקבל בחזרה את כל הדף. מי שחיבר את RenderPageToDCClip בציפייה לחיתוך, קיבל רינדור מלא של הדף, ובהתאם לפריסה שלו אולי אפילו לא שם לב
v3.83.0 חיברה סוף סוף את הנתיב. RenderPageToDC מנתחת עכשיו את אותו מלבן נקודות של "Left,Top,Width,Height" ומחילה אותו כאזור חיתוך GDI אמיתי על ה-device context היעד לפני שהמנוע מצייר. ההמרה מנקודות לפיקסלים של התקן היא מקדם הסקייל הרגיל DPI / 72, שמוחל על כל ארבעת הקצוות. הרצף סביב הרינדור הוא ריקוד ה-save/clip/restore הרגיל:
// inside TPDFPageTree.RenderPageToDC, when Clip is non-empty
ScaleFactor := DPI / 72;
SaveDC(TargetDC);
IntersectClipRect(TargetDC,
Round(ClipLeft * ScaleFactor),
Round(ClipTop * ScaleFactor),
Round((ClipLeft + ClipWidth) * ScaleFactor),
Round((ClipTop + ClipHeight) * ScaleFactor));
// ... renderer draws the page here ...
// in the finally block:
RestoreDC(TargetDC, -1);
זוג SaveDC / RestoreDC(-1) הוא מה שהופך את זה לבטוח לקריאה חוזרת: אזור החיתוך נדחף למחסנית המצבים של ה-DC, הדף מצויר, והחיתוך המקורי חוזר לאחור בלי קשר לאופן שבו הרינדור מסתיים. RestoreDC(TargetDC, -1) משחזר את המצב האחרון שנשמר, וזה האידיום המקובל ל-save/restore מאוזן. דלג על השחזור, וקריאה שמשתמשת שוב באותו DC עבור רינדור מלא עוקב הייתה מוצאת את עצמה חתוכה באופן מסתורי לאזור האחרון. תיקון הפרמטר המת גם תיקן בחינם את RenderPageRegionToFile, כי המתודה החדשה הזו עוברת בדיוק במסלול הזה
נקודה התנהגותית שחשוב להפנים: החיתוך crops, הוא לא scales. הדף עדיין עובר rasterization ב-DPI שביקשת, במיקום הרגיל שלו, ואזור החיתוך פשוט משליך כל מה שמחוץ למלבן. אתה לא מגדיל את האזור כדי שימלא את הפלט, אלא חותך חלון מתוך הרינדור ברזולוציה מלאה. אם אתה רוצה שהאזור יוגדל, העלה את ה-DPI. הקואורדינטות של המלבן מפורשות ב-device space אחרי סקיילינג של נקודות לפיקסלים, ונמדדות מפינת שמאל-עליון של המשטח המרונדר, כך שכדאי לתכנן את Left ואת Top מהחלק העליון של הדף כלפי מטה. לסיור עמוק יותר באופן שבו PDFlibPas מזינה device context לפלט על המסך, המאמר המשלי על תצוגה מקדימה לפני הדפסה ופלט דרך device context עובר על אותה תשתית DC מצד התצוגה
הגבול הכנה: BMP של 1 ביט, לא G4 TIFF
יהיה קל למכור את זה יתר על המידה כ-"פלט מוכן לפקס", אז הנה הגבול נאמר בפשטות. RenderPageToMonochromeFile מפיקה BMP מסוג pf1bit. היא לא מפיקה CCITT Group 4 TIFF, שהוא הפורמט שתהליך פקס אמיתי או ארכיון TIFF בדרך כלל מצפים לו. הסיבה היא עניינית ולא חוסר תשומת לב: יחידת ה-CCITT של PDFlibPas יודעת כרגע לפענח זרמי G4 אבל אין לה מקודד G4. בלי מקודד אין לאן לכתוב רצפים מונוכרומיים דחוסים, ולכן נתיב המונוכרום נעצר ב-DIB לא דחוס של 1 ביט
בפועל זה עדיין שימושי. BMP של 1 ביט הוא פורמט הפיקסלים הנכון, מדופדף ומוכן, ורוב שרשראות הכלים של פקס, ארכיון או OCR יקלטו אותו בשמחה או ימירו אותו ל-G4 בעצמן בצעד downstream אחד. אבל אם הדרישה שלך היא ממש TIFF של Group 4 ישר מהספרייה, זה עדיין לא זה, ואתה צריך לתכנן שלב דחיסה משלך. לדעת היכן יכולת נעצרת שווה לא פחות מלדעת מה היא עושה
שתי המתודות קטנות בכוונה, וזו תובנת התכנון שכדאי לקחת מהעמוד הזה: API נוח שיושב מעל מנוע רינדור יכול להוסיף יכולת אמיתית, פלט מונוכרומי, חיתוך אזור, בלי להיכנס ל-rasterizer ולערער את שאר הקוראים. כשכן צריך לבחור בין מנועי רינדור עבור הרסטריזציה הבסיסית, הסקירה של רינדור PDF רב-מנועי ב-Delphi מפרטת את הפשרות לעומק. כדי לראות את כל משטח הרינדור ושאר ה-API, דף המוצר של PDFlibPas Delphi PDF Library מציג את התמונה המלאה