พิกัดของ PDF อยู่ในหน่วยพอยต์ (points) ส่วนพิกัดของเครื่องพิมพ์อยู่ในหน่วยของอุปกรณ์ (device units) และทั้งสองอย่างนี้ไม่มีความเกี่ยวข้องกันเลยจนกว่าคุณจะแปลงค่ามันอย่างจงใจ ความไม่สอดคล้องกันตรงนี้คือต้นเหตุของผลลัพธ์การพิมพ์ที่แย่ที่สุดในแอปพลิเคชัน Delphi: โค้ดส่งไฟล์ที่ถูกต้องไป แต่หน้ากระดาษกลับออกมาถูกตัดแหว่ง ยืดผิดส่วน หรือเป็นหน้าขาว PDFium Component จัดการเรื่องการเรนเดอร์ได้อย่างหมดจด; ส่วนระบบท่อส่งไปยังเครื่องพิมพ์ก็เป็นมาตรฐานของ VCL สองสิ่งนี้จะประกอบเข้าด้วยกันด้วยโค้ดเพียงเล็กน้อยเมื่อคุณเข้าใจว่าแต่ละฝั่งต้องการอะไร
ท่อส่งข้อมูลการเรนเดอร์แล้วพิมพ์ (render-then-print) ทำงานอย่างไร
PDFium Component ไม่ได้คุยกับเครื่องพิมพ์โดยตรง รูปแบบการทำงานคือ: เรนเดอร์หน้ากระดาษไปที่ TBitmap ด้วยความละเอียดที่คุณต้องการ จากนั้นโอนบิตแมปนั้นไปยังแคนวาส (canvas) ของเครื่องพิมพ์ด้วย StretchDIBits TPdf.RenderPage จะส่งคืนบิตแมปที่ผู้เรียก (caller) เป็นเจ้าของ ดังนั้นคุณจึงควบคุมขนาดพิกเซลได้ ส่งค่า [rePrinting] เข้าไปในชุดตัวเลือก แล้ว PDFium จะเปลี่ยนเส้นทางการเรนเดอร์ไปเป็นโหมดที่ละทิ้งเอฟเฟกต์สำหรับหน้าจอเท่านั้น เช่น LCD subpixel hinting และจัดการกับ MediaBox ของหน้ากระดาษได้อย่างถูกต้องสำหรับผลลัพธ์การพิมพ์ หากละทิ้ง rePrinting สิ่งที่คุณส่งไปยังเครื่องพิมพ์จะเป็นผลลัพธ์แบบที่ใช้บนหน้าจอ ซึ่งดูดีบนมอนิเตอร์ แต่มีแนวโน้มที่จะให้ผลลัพธ์ที่นุ่มนวลเบลอเกินไปบนเครื่องพิมพ์ความละเอียดสูง (high-DPI) เพราะการตัดสินใจเลือก hinting สำหรับหน้าจอ 96 DPI ไม่เหมาะกับการพิมพ์ระดับ 300 หรือ 600 DPI
TPdf.Active เป็นประตูด่านเดียวที่ต้องตรวจสอบก่อนสัมผัสคุณสมบัติของหน้ากระดาษใด ๆ คอมโพเนนต์จะกลืนกินข้อผิดพลาดในการโหลดแบบเงียบ ๆ: การตั้งค่า Active := True กับไฟล์ที่เสียหายหรือติดรหัสผ่านจะไม่แจ้ง exception ขึ้นมา; แต่มันจะปล่อย Active ให้เป็น False เสมอ ต้องตรวจสอบมันทุกครั้งหลังการกำหนดค่า การอ่านค่า PageCount หรือ PageWidth บนเอกสารที่ไม่แอ็คทีฟจะคืนค่าศูนย์ ซึ่งก่อให้เกิด no-ops (ไม่มีการดำเนินการ) แบบเงียบ ๆ ที่ยากต่อการวิเคราะห์หาสาเหตุมากเมื่อมันถูกส่งไปถึงสปูลเลอร์ (spooler) แล้ว
ลูปการพิมพ์แบบน้อยที่สุด (minimal print loop)
กรณีที่ใช้งานได้ง่ายที่สุดคือการโหลดไฟล์ เปิดงานพิมพ์ วนลูปหน้ากระดาษ และปิดงาน รายละเอียดที่ซับซ้อนมีเพียงเรื่องเดียวคือ Printer.NewPage ห้ามถูกเรียกก่อนหน้าแรก ดังนั้นจึงต้องมีแฟล็ก (flag) FirstPage การถ่ายโอน StretchDIBits ผ่านการเรียก GetDIBSizes และ GetDIB เพื่อดึงข้อมูลบิตที่เป็นอิสระจากอุปกรณ์ออกจากแฮนเดิล (handle) ของบิตแมป จากนั้นจึงระบายลงบนแคนวาสของเครื่องพิมพ์ด้วยขนาดหน้ากระดาษเต็มรูปแบบ:
procedure PrintPdfFile(const FileName: string);
var
Pdf: TPdf;
I: Integer;
Bitmap: TBitmap;
InfoHeaderSize, ImageSize: DWORD;
InfoHeader: PBitmapInfo;
Image: Pointer;
FirstPage: Boolean;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := FileName;
Pdf.Active := True;
if not Pdf.Active then
Exit; // load failed silently; bail out
Printer.Title := Pdf.Title;
Printer.BeginDoc;
try
FirstPage := True;
for I := 1 to Pdf.PageCount do
begin
if FirstPage then
FirstPage := False
else
Printer.NewPage;
Pdf.PageNumber := I;
// Render at printer resolution; rePrinting adjusts the render path
Bitmap := Pdf.RenderPage(
0, 0,
Printer.PageWidth,
Printer.PageHeight,
ro0,
[rePrinting]
);
try
GetDIBSizes(Bitmap.Handle, InfoHeaderSize, ImageSize);
InfoHeader := AllocMem(InfoHeaderSize);
try
Image := AllocMem(ImageSize);
try
GetDIB(Bitmap.Handle, 0, InfoHeader^, Image^);
StretchDIBits(
Printer.Canvas.Handle,
0, 0, Printer.PageWidth, Printer.PageHeight,
0, 0, Bitmap.Width, Bitmap.Height,
Image, InfoHeader^, DIB_RGB_COLORS, SRCCOPY
);
finally
FreeMem(Image);
end;
finally
FreeMem(InfoHeader);
end;
finally
Bitmap.Free;
end;
end;
finally
Printer.EndDoc;
end;
finally
Pdf.Active := False;
Pdf.Free;
end;
end;
การส่งค่า Printer.PageWidth และ Printer.PageHeight เป็นขนาดบิตแมป หมายความว่าคุณทำการเรนเดอร์ในระดับขนาดพิกเซลที่แท้จริงของเครื่องพิมพ์ ซึ่งครอบคลุมถึง DPI ของอุปกรณ์เรียบร้อยแล้ว การเรียก StretchDIBits หลังจากนั้นจะทำผังพิกเซลเหล่านั้นในอัตราส่วน 1:1 ลงบนหน้ากระดาษ วิธีนี้ช่วยให้ได้ความแม่นยำสูงที่สุดเท่าที่จะเป็นไปได้โดยไม่ต้องคิดคำนวณ DPI โดยตรง แต่มันจะใช้งานได้ก็ต่อเมื่อหน้า PDF และกระดาษจริงมีขนาดเท่ากันเท่านั้น เมื่อขนาดต่างกัน คุณจำเป็นต้องสเกล (scale) อย่างชัดเจน
การปรับสเกลเมื่อขนาดหน้าและกระดาษแตกต่างกัน
หน้า PDF ในรูปแบบ A4 แนวตั้งไม่ได้พอดีกับเครื่องพิมพ์ US Letter โดยอัตโนมัติ และหน้ากระดาษแนวนอนที่ถูกส่งไปยังเครื่องพิมพ์แนวตั้งก็จะถูกตัดส่วนขอบออก แนวทางมาตรฐานคือการคำนวณสัดส่วนสเกลที่สม่ำเสมอจากอัตราส่วนของพิกเซลเครื่องพิมพ์ต่อพอยต์ของ PDF แล้วปรับใช้กับทั้งสองแกนเพื่อรักษาสัดส่วนภาพ (aspect ratio) Pdf.PageWidth และ Pdf.PageHeight เปิดเผยขนาดหน้ากระดาษปัจจุบันเป็นหน่วยพอยต์ โดยหนึ่งพอยต์เท่ากับ 1/72 นิ้ว การคูณด้วยเป้าหมาย DPI แล้วหารด้วย 72 จะเป็นการแปลงหน่วยเป็นพิกเซลที่ความละเอียดนั้น ๆ นำ Min ของอัตราส่วนแกน X และ Y เพื่อให้ได้สเกลสูงสุดที่ยังคงพอดียกเข้าไปอยู่ในพื้นที่ที่สามารถพิมพ์ได้:
// Fit PDF page to printable area, preserving aspect ratio
var
ScaleX, ScaleY, Scale: Double;
DestWidth, DestHeight: Integer;
Dpi: Integer;
begin
Dpi := 300; // target render resolution
Pdf.PageNumber := PageIndex;
ScaleX := Printer.PageWidth / (Pdf.PageWidth * Dpi / 72);
ScaleY := Printer.PageHeight / (Pdf.PageHeight * Dpi / 72);
Scale := Min(ScaleX, ScaleY);
// Clamp to 1.0 for shrink-to-fit only (no enlargement)
if Scale > 1.0 then Scale := 1.0;
DestWidth := Round(Pdf.PageWidth * Dpi / 72 * Scale);
DestHeight := Round(Pdf.PageHeight * Dpi / 72 * Scale);
Bitmap := Pdf.RenderPage(0, 0, DestWidth, DestHeight, ro0,
[rePrinting, reAnnotations]);
// ... transfer with StretchDIBits as above
end;
การเรนเดอร์ที่ Dpi = 300 เหมาะสมที่สุดสำหรับเครื่องพิมพ์ในสำนักงานทั่วไป ที่ระดับ 600 DPI บิตแมปสำหรับหน้ากระดาษ A4 หน้าเดียวจะมีขนาดประมาณ 34 ล้านพิกเซล ซึ่งคิดเป็นพื้นที่ประมาณ 100 MB หากบันทึกเป็นบิตแมป 32-bit; การยกระดับคุณภาพที่ได้มาสำหรับเอกสารข้อความธรรมดานั้นถือว่าน้อยมาก และยังกินหน่วยความจำต่อหน้ากระดาษอย่างมากอีกด้วย เก็บ 600 DPI ไว้ใช้สำหรับร้านรับพิมพ์งาน หรือแบบแปลนเชิงเทคนิคที่มีกราฟิกเวกเตอร์สูง ๆ ที่ต้องการรายละเอียดอย่างแท้จริง
แฟล็ก reAnnotations ในโค้ดบล็อกที่สองเป็นอิสระจาก rePrinting รวมมันเข้าไปเมื่อผู้ใช้คาดหวังว่าตราประทับ ไฮไลต์ และกล่องคอมเมนต์จะไปปรากฏบนหน้ากระดาษด้วย หากตัดทิ้งไปจะได้เฉพาะเนื้อหา แฟล็กทั้งสองสามารถนำมาใช้ร่วมกันได้อย่างอิสระ
การหมุนหน้ากระดาษ
PDFium เก็บการหมุนหน้ากระดาษใน PDF เป็นรายการ /Rotate สามารถเข้าถึงได้ผ่าน Pdf.PageRotation ซึ่งคืนค่าเป็นข้อมูลแบบ TRotation (ro0, ro90, ro180, ro270) ระบบพิกัดของเครื่องพิมพ์จะกลับด้านการหมุน 90 และ 270 องศาเมื่อเทียบกับหน้าจอ หากคุณส่งค่า PageRotation ดิบ ๆ ให้ RenderPage โดยตรงโดยไม่มีการปรับแต่ง หน้ากระดาษแนวนอนที่แทรกอยู่ในเอกสารแนวตั้งจะพิมพ์กลับหัวกลับหางบนไดรเวอร์เครื่องพิมพ์ส่วนใหญ่ของ Windows วิธีแก้คือสลับค่าง่าย ๆ ก่อนการเรียก render: แมป ro90 ให้กลายเป็น ro270 และ ro270 กลับมาเป็น ro90 ส่วน ro0 และ ro180 ปล่อยไว้ดังเดิม
ตรวจสอบพฤติกรรมนี้กับเครื่องพิมพ์เป้าหมายของคุณโดยเฉพาะก่อนการส่งมอบงาน พฤติกรรมของไดรเวอร์เกี่ยวกับเรื่องการหมุนหน้าไม่ได้เป็นมาตรฐานเดียวกันในทุกผู้ผลิต และไดรเวอร์บางตัวมีการแก้ไขทิศทางหมุนด้วยตัวเองในระดับ GDI ด้วย หากคุณเจออาการเอกสารหมุนซ้อนกันสองครั้ง (double rotation) ให้นำการสลับที่เขียนไว้ออก; แต่ถ้าไม่มีการแก้ไขทิศทางเลย ก็ให้ใส่เข้าไป เอกสารที่มีทิศทางหน้ากระดาษปะปนกัน (สลับแนวตั้งแนวนอนไปมา) คือวิธีทดสอบความผิดพลาดทั้งสองแบบได้รวดเร็วที่สุดในระหว่างช่วงทดสอบ
การบริหารจัดการหน่วยความจำผ่านงานพิมพ์ยาว ๆ
การเรียก RenderPage แต่ละครั้งจะจองพื้นที่บิตแมป TBitmap อันใหม่ที่ฝั่งเรียกเป็นเจ้าของ และต้องทำลายทิ้งด้วยตัวเอง ในลูปด้านบน โค้ดบล็อก try/finally Bitmap.Free จะจัดการเรื่องนี้ได้อย่างถูกต้องทีละหน้ากระดาษ อย่าสะสมบิตแมปผ่านการวนลูปหลาย ๆ หน้า: การเรนเดอร์ระดับ 300-DPI สำหรับเอกสารความยาว 200 หน้า จะเผาผลาญพื้นที่ขนาดหลายกิกะไบต์ก่อนที่หน้าแรกจะไปถึงสปูลเลอร์เสียด้วยซ้ำ จงทำลายบิตแมปในทุก ๆ หน้าระหว่างการวนลูป
คู่การเรียก AllocMem / FreeMem ที่อยู่ภายในบล็อกการถ่ายโอนบิตก็ใช้กฎแบบเดียวกัน GetDIBSizes จะบอกให้คุณรู้ว่าโครงสร้างหัวไฟล์ DIB และข้อมูลพิกเซลต้องการพื้นที่หน่วยความจำมากเท่าไหร่; คุณจัดสรร เติมเต็ม วาด และทำลายล้างทุกสิ่งทุกอย่างที่อยู่ในสโคปของหน้ากระดาษเดียว หากปล่อยให้บล็อกใดบล็อกหนึ่งรั่วไหล จะส่งผลให้งานพิมพ์สูบเอาพื้นที่ heap ทั้งหมดของโปรเซสซิงไปจนเกลี้ยงในการรับมือกับเอกสารที่ยาวกว่าสักหลายสิบหน้ากระดาษ
หากคุณต้องการทำงานพิมพ์ผ่านเธรดเบื้องหลัง (background thread) ให้รักษาทั้ง TPdf และทุกการเรียกเครื่องพิมพ์ในฝั่ง VCL ให้อยู่ในเธรดเดียวกัน TPdf ตัวของมันเองไม่ได้รองรับเรื่อง thread-safe ต่อการใช้งานร่วมกันข้ามอินสแตนซ์ที่จะไปเรียกสถานะร่วม (global state) ของ PDFium DLL; โมเดลการใช้งานที่ปลอดภัยที่สุดคือ สร้าง TPdf 1 ตัวต่อ 1 เธรด โดยที่แต่ละอันจะแยกโหลดเอกสารก๊อปปี้ของตัวเอง
API สำหรับการเรนเดอร์และเอกสารที่แสดงไว้ที่นี้ เป็นส่วนหนึ่งของ PDFium Component สำหรับ Delphi และ C++Builder