การเรนเดอร์หน้า PDF ลงบน device context ของ Windows เพื่อแสดงตัวอย่างก่อนพิมพ์ เป็นการเอาระบบพิกัดสามระบบมาไว้ในโค้ดบรรทัดเดียวกัน และทั้งสามระบบก็แทบไม่เคยตรงกันเลย หน้า PDF วัดเป็นพอยต์โดยมีจุดกำเนิดอยู่ที่มุมล่างซ้าย DC ของหน้าจอวัดเป็นพิกเซลโดยมีจุดกำเนิดอยู่ที่มุมบนซ้าย พร้อมตัวคูณซูมที่คุณเลือกเอง ส่วน DC ของเครื่องพิมพ์ ซึ่งเป็นสิ่งที่ภาพตัวอย่างมีหน้าที่ทำนายให้ถูก วัดเป็นพิกเซลที่ความละเอียดของอุปกรณ์ แต่วางจุดกำเนิดไว้ที่มุมของพื้นที่ที่พิมพ์ได้ ไม่ใช่มุมของแผ่นกระดาษ พลาดไปเพียงระบบใดระบบหนึ่ง ภาพตัวอย่างก็จะดูดี ขณะที่หน้าซึ่งพิมพ์ออกมากลับเลื่อน ย่อขยายผิด หรือถูกตัดขอบไปด้านใดด้านหนึ่ง อาการที่พบบ่อยคือแบบฟอร์มมีกรอบที่ดูตัวอย่างแล้วอยู่กึ่งกลางพอดี แต่พิมพ์ออกมาแล้วเส้นกรอบด้านบนและด้านซ้ายถูกเฉือนหายไป เพราะเครื่องพิมพ์เลเซอร์ลงหมึกในขอบนอกไม่กี่มิลลิเมตรไม่ได้ และไม่มีใครไปบอกตัวแสดงตัวอย่าง losLab PDF Library (PDF Library for Delphi) ครอบคลุมเส้นทางนี้ทั้งสาย ด้วยการเรียกเรนเดอร์ลง device context ชั้นตั้งค่าเครื่องพิมพ์เสมือน และภาพตัวอย่างที่สร้างจากค่าเมตริกของเครื่องพิมพ์เอง ซึ่งเป็นส่วนที่ทำให้ภาพตัวอย่างซื่อสัตย์ต่อระยะขอบนั้น
เรขาคณิตของกระดาษไม่ใช่เรขาคณิตของพื้นที่ที่พิมพ์ได้
สี่เหลี่ยมสองรูปอธิบายเป้าหมายการพิมพ์ใด ๆ ก็ตาม และระยะเหลื่อมระหว่างสองรูปนั้นคือที่อยู่ของบั๊กเรื่องภาพตัวอย่างเสียเป็นส่วนใหญ่ สี่เหลี่ยมกระดาษคือแผ่นจริงทางกายภาพ ส่วนสี่เหลี่ยมที่พิมพ์ได้คือบริเวณเล็กกว่าซึ่งกลไกการพิมพ์เอื้อมถึงได้จริง ร่นเข้ามาด้วยระยะขอบเชิงฮาร์ดแวร์ที่ต่างกันไปตามรุ่นเครื่องพิมพ์ และบางครั้งก็ต่างกันตามถาดกระดาษด้วย ชั้นการพิมพ์ของไลบรารีวัดทั้งสองรูป คลาส TPLPrinter ที่อยู่เบื้องหลังเปิด PageWidth และ PageHeight ให้สำหรับพื้นที่ที่พิมพ์ได้ เปิด FullPageWidth และ FullPageHeight สำหรับแผ่นเต็ม และเปิด PrintOffsetX คู่กับ PrintOffsetY สำหรับช่องว่างระหว่างจุดกำเนิดของทั้งสอง ทั้งหมดเป็นพิกเซลของอุปกรณ์ที่ความละเอียดซึ่ง GetDPI รายงานมา ภาพตัวอย่างที่ซื่อสัตย์จะย่อตัวเลขชุดเดียวกันนี้ลงมาที่ความละเอียดหน้าจอ แทนที่จะวาดหน้าลงในสี่เหลี่ยมใดก็ตามที่คอนโทรลบังเอิญมีอยู่ ถ้าข้ามขั้นตอนนั้นไป ภาพตัวอย่างก็จะสมมติเงียบ ๆ ว่าระยะขอบเป็นศูนย์ ซึ่งเป็นค่าเดียวที่ไม่มีเครื่องพิมพ์จริงเครื่องใดใช้
แสดงตัวอย่างบนหน้าจอผ่าน RenderPageToDC
สำหรับคอนโทรลแสดงตัวอย่างบนหน้าจอ RenderPageToDC(DPI, Page, DC) วาดหน้าหนึ่งของเอกสารที่โหลดไว้ลงบน device context ของ GDI ตัวใดก็ได้โดยตรง ไม่ว่าจะเป็นแคนวาสของ TPaintBox บิตแมปนอกจอ หรือ DC ของ metafile อาร์กิวเมนต์ DPI เป็นตัวกำหนดระดับซูม ค่า 96 ใกล้เคียงกับมุมมอง 100% บนจอแบบคลาสสิก และการเพิ่มค่าเป็นสองเท่าก็ทำให้ขนาดที่เรนเดอร์ออกมาใหญ่ขึ้นสองเท่า
procedure TPreviewForm.PreviewBoxPaint(Sender: TObject);
begin
// สามบรรทัดนี้เป็นสถานะติดค้างของไลบรารี ไม่ใช่พารามิเตอร์รายการเรียก:
FPdf.SetRenderDCOffset(FOffsetX, FOffsetY);
FPdf.SetRenderDCErasePage(1);
FPdf.SetRenderCropType(0);
FPdf.RenderPageToDC(FPreviewDpi, FCurrentPage, PreviewBox.Canvas.Handle);
end;
กับดักคือเส้นทางการเรนเดอร์ลง DC ถูกบังคับทิศด้วยสถานะติดค้างของไลบรารี ไม่ใช่ด้วยพารามิเตอร์รายการเรียก ทั้ง SetRenderDCOffset, SetRenderDCErasePage และ SetRenderCropType ต่างคงค่าอยู่จนกว่าจะมีอะไรมาเปลี่ยน ลูปสร้างภาพย่อที่รันหลังจากผู้ใช้ปรับมุมมองซูมไปแล้ว จึงรับ offset หรือค่าการครอบตัดที่เส้นทางโค้ดก่อนหน้าทิ้งค้างไว้มาเต็ม ๆ อาการคือภาพตัวอย่างที่เพี้ยนเฉพาะในลำดับการนำทางบางแบบ ซึ่งเป็นบั๊กที่ทำซ้ำได้ยากพอ ๆ กับบั๊กชนิดใดก็ตาม การตั้งค่าสถานะที่เกี่ยวข้องทั้งหมดไว้ที่หัวของ paint handler อย่างในตัวอย่างข้างต้นไม่มีต้นทุนอะไรเลย และกำจัดปัญหาทั้งกลุ่มนี้ทิ้งไป ยังมีตัวคูณอีกตัวซ่อนอยู่ใกล้ ๆ ความละเอียดผลลัพธ์ที่แท้จริงคือ render scale คูณกับอาร์กิวเมนต์ DPI และแม้ SetRenderScale จะมีค่าเริ่มต้นเป็น 1.0 แต่มันก็คงค่าไว้เมื่อถูกเปลี่ยนเช่นกัน ฟีเจอร์ส่งออกที่ไปดันค่านั้นขึ้นจึงย่อขยายภาพตัวอย่างทุกภาพหลังจากนั้นอย่างเงียบ ๆ จนกว่าจะมีอะไรมาตั้งค่ากลับคืน
ตัวแสดงผลแบบเลื่อนหน้าจอและการวาดซ้ำเพียงบางส่วนมีตัวแปรเฉพาะของมันเอง RenderPageToDCClip รับข้อกำหนดการตัดขอบมาพร้อมกับ device context การทำให้แถบหนึ่งของหน้าต่างเป็นโมฆะจึงวาดซ้ำเฉพาะแถบนั้น แทนที่จะแรสเตอไรซ์ทั้งหน้าใหม่ ที่ระดับซูมสูงบนหน้ากระดาษขนาดใหญ่ นี่คือเส้นแบ่งระหว่างตัวแสดงผลที่วิ่งตามแถบเลื่อนได้ทัน กับตัวที่ลากเป็นรอยเปื้อนตามหลัง
งานพิมพ์ที่ตรงกับภาพตัวอย่าง
ฝั่งการพิมพ์ทำงานผ่านเครื่องพิมพ์เสมือน NewCustomPrinter โคลนเครื่องพิมพ์ของระบบมาเป็นการตั้งค่าส่วนตัวของไลบรารี และ SetupPrinter ปรับโคลนตัวนั้นโดยไม่แตะ DevMode ระดับทั้งเครื่อง กระดาษป้อนเข้ามาเป็นการตั้งค่าหมายเลข 1 (ค่าคงที่ DMPAPER_*) และแนวการวางเป็นการตั้งค่าหมายเลข 11 ผลตอบแทนคือการแยกขาดจากกัน บริการหนึ่งพิมพ์ฉลาก A4 ได้ในขณะที่เครื่องพิมพ์เริ่มต้นของโฮสต์ยังตั้งเป็น Letter อยู่ และไม่มีอะไรต้องคืนค่าภายหลัง
var
Pdf: TPDFlib;
Virt: WideString;
Opt: Integer;
begin
Pdf := TPDFlib.Create;
try
if Pdf.LoadFromFile('report.pdf', '') <> 1 then
raise Exception.Create('load failed');
Virt := Pdf.NewCustomPrinter(Pdf.GetDefaultPrinterName);
Pdf.SetupPrinter(Virt, 1, 9); // การตั้งค่า 1 = กระดาษ, DMPAPER_A4
Pdf.SetupPrinter(Virt, 11, 1); // การตั้งค่า 11 = แนววาง, 1 = แนวตั้ง
Opt := Pdf.PrintOptions(1, 1, 'Monthly Report'); // พอดีกระดาษ, หมุนอัตโนมัติ + จัดกึ่งกลาง
Pdf.PrintDocument(Virt, 1, Pdf.PageCount, Opt);
finally
Pdf.Free;
end;
end;
PrintOptions สมควรได้รับการอ่านอย่างระมัดระวัง มันคืนค่าเป็น options handle ที่คุณต้องส่งต่อให้ PrintDocument หรือ PrintPages มันไม่ใช่สถานะแวดล้อมที่มีผลได้เอง การสร้างตัวเลือกขึ้นมาแล้วลืมส่ง handle ต่อจะล้มเหลวอย่างเงียบเชียบ งานพิมพ์จะเดินด้วยค่าเริ่มต้น และไม่มีใครสังเกตจนกว่าจะพบว่าตั้งใจใช้นโยบายพอดีกระดาษ แต่หน้าที่ใหญ่เกินกลับพิมพ์ออกมาแบบถูกตัดขอบแทน อาร์กิวเมนต์การย่อขยายหน้าคือที่อยู่ของนโยบายนั้น การไม่ย่อขยายเลยรักษาความแม่นยำเชิงมิติไว้ ซึ่งสำคัญกับแบบฟอร์มที่ถูกเอาไม้บรรทัดมาวัด ส่วนแบบพอดีกระดาษจะย่อขยายทุกอย่างให้เข้ากับแผ่น และแบบย่อเฉพาะหน้าใหญ่จะปล่อยหน้าปกติไว้เฉย ๆ แล้วเข้าแทรกเมื่อหน้าใดเกินพื้นที่ที่พิมพ์ได้เท่านั้น ซึ่งมักเป็นค่าเริ่มต้นที่ถูกต้องสำหรับชุดเอกสารที่คละกัน ส่วนแฟล็กหมุนอัตโนมัติและจัดกึ่งกลางก็รับมือหน้าแนวนอนได้โดยไม่ต้องเขียนเส้นทางโค้ดที่สอง
แอปพลิเคชันที่จัดการ TPrinter ผ่านโฟลว์ไดอะล็อกของ VCL อยู่แล้ว ส่งต่อมันมาได้โดยตรง PrintDocumentToPrinterObject และ PrintPagesToPrinterObject รับอินสแตนซ์ TPrinter ที่ตั้งค่าไว้แล้ว ซึ่งรักษาไดอะล็อกพิมพ์มาตรฐานไว้เป็นพื้นผิวการตั้งค่าที่ผู้ใช้เห็น ขณะที่ไลบรารีรับหน้าที่เรนเดอร์หน้าเอกสาร การผสมสองแนวทางนี้ในเส้นทางโค้ดเดียวมักจะดึงความคลาดเคลื่อนเชิงเรขาคณิตที่งานทั้งหมดนี้ตั้งใจกำจัดกลับเข้ามาใหม่ ฉะนั้นให้เลือกอย่างใดอย่างหนึ่ง เส้นทางเครื่องพิมพ์เสมือนเหมาะกับบริการที่ทำงานโดยไม่มีคนดูแล ส่วนเส้นทาง TPrinter เหมาะกับแอปพลิเคชันแบบโต้ตอบ
การพิมพ์แบบเลือกหน้าก็ทำงานแบบเดียวกัน PrintPages รับสตริงระบุช่วง การส่งชื่อเครื่องพิมพ์เสมือน ค่า '2-5,12' และ options handle เข้าไป จึงพิมพ์หน้า 2 ถึง 5 และหน้า 12 โดยสัญญาเชิงเรขาคณิตยังคงอยู่ครบ และไวยากรณ์เดียวกันนี้ก็ขับตัวแปรที่พิมพ์ลงไฟล์ด้วย ตัวแปรพิมพ์ลงไฟล์เหล่านั้นคือคำตอบเชิงปฏิบัติสำหรับสภาพแวดล้อมที่ไม่มีคนดูแลและไม่มีอุปกรณ์จริงต่ออยู่ นั่นคือการทดสอบการถดถอยของเรขาคณิตงานพิมพ์บนเซิร์ฟเวอร์บิลด์ที่ไม่มีคิวไดรเวอร์ใด ๆ เลย จงเรนเดอร์เอกสารเดียวกันผ่านตัวเลือกเดียวกันลงเป็นไฟล์ผลลัพธ์ในทุกบิลด์ แล้วการถดถอยเชิงเรขาคณิตก็จะกลายเป็น diff แทนที่จะเป็นรายงานจากลูกค้าในอีกสามสัปดาห์ถัดมา
ภาพตัวอย่างที่ใช้ค่าเมตริกของเครื่องพิมพ์เอง
ภาพตัวอย่างที่เรนเดอร์ที่ 96 DPI บนขนาดหน้าที่สมมติเอาเองนั้นตอบผิดคำถาม มันแสดงว่าหน้านั้นหน้าตาเป็นอย่างไร ไม่ได้แสดงว่าเครื่องพิมพ์เครื่องนี้จะลงอะไรบนกระดาษแผ่นนี้ GetPrintPreviewBitmapToString ปิดช่องว่างนั้นด้วยการสร้างภาพตัวอย่างจากเครื่องพิมพ์กำหนดเองตัวเดียวกันและ options handle ตัวเดียวกันกับงานพิมพ์ที่จะเกิดขึ้นจริง ขนาดกระดาษ แนววาง นโยบายการย่อขยาย การหมุน และระยะเหลื่อมเชิงฮาร์ดแวร์จึงป้อนเข้าสู่บิตแมปทั้งหมด สิ่งที่ได้กลับมาคือสิ่งที่จะปรากฏบนแผ่นกระดาษจริง
procedure ShowPrinterTruePreview(Pdf: TPDFlib; const Virt: WideString; Opt: Integer);
var
Data: AnsiString;
Strm: TMemoryStream;
Bmp: TBitmap;
begin
Data := Pdf.GetPrintPreviewBitmapToString(Virt, 1, Opt, 1200, 0);
Strm := TMemoryStream.Create;
try
Strm.WriteBuffer(PAnsiChar(Data)^, Length(Data));
Strm.Position := 0;
Bmp := TBitmap.Create;
try
Bmp.LoadFromStream(Strm);
PreviewImage.Picture.Assign(Bmp);
finally
Bmp.Free;
end;
finally
Strm.Free;
end;
end;
อาร์กิวเมนต์ MaxDimension จำกัดด้านยาวของบิตแมปเอาไว้ ค่า 1200 พิกเซลยังคมพอสำหรับไดอะล็อกแสดงตัวอย่าง และคุมการใช้หน่วยความจำให้อยู่ในระดับพอประมาณแม้กับแบบวิศวกรรมขนาด E ซึ่งการเรนเดอร์เต็มความละเอียดที่ 600 DPI ของเครื่องพิมพ์จะกินพื้นที่ถึงระดับกิกะไบต์
จดจำตัวเลือกเครื่องพิมพ์ของผู้ใช้
ไดอะล็อกพิมพ์ที่ลืมการตั้งค่าของตัวเองระหว่างการใช้งานแต่ละครั้งก็สร้างทิกเก็ตสนับสนุนขึ้นมาเองได้ คู่ฟังก์ชัน DevMode คือ GetPrinterDevModeToString และ SetPrinterDevModeFromString ทำหน้าที่แปลงการตั้งค่าไดรเวอร์ทั้งชุดของเครื่องพิมพ์ให้เป็นสตริงทึบที่คุณเก็บไว้ในค่ากำหนดของผู้ใช้แล้วเรียกคืนในครั้งถัดไปได้ รวมถึงตัวเลือกเฉพาะไดรเวอร์ที่ไม่มี API ทั่วไปตัวใดสนใจจะสร้างแบบจำลองให้ จงบันทึกเครื่องพิมพ์ด้วยชื่อที่ได้จาก GetPrinterNames อย่าบันทึกด้วยดัชนีในลิสต์เด็ดขาด ลำดับดัชนีเปลี่ยนทุกครั้งที่มีการเพิ่มหรือลบเครื่องพิมพ์ ดัชนีที่บันทึกไว้จึงชี้ไปยังอุปกรณ์ผิดตัวอย่างเงียบ ๆ ในครั้งถัดไปที่ลิสต์ขยับ ส่วน GetDefaultPrinterName รองรับกรณีถอยกลับเมื่ออุปกรณ์ที่จำไว้หายไปจากระบบเสียแล้ว
การเลือกถาดกระดาษเป็นชิ้นส่วนสุดท้ายของเรื่องการจดจำค่า GetPrinterBins รายงานแหล่งกระดาษที่ไดรเวอร์เปิดเผยออกมา ซึ่งสำคัญกับงานที่ใช้กระดาษหัวจดหมาย โดยหน้าแรกดึงจากถาดกระดาษหัวจดหมายและหน้าที่เหลือดึงจากกระดาษเปล่า นั่นคือนโยบายที่ผู้ใช้คาดหวังให้แอปพลิเคชันจำไว้พร้อมกับทุกอย่างที่เหลือ และงานพิมพ์ที่ลงบนกระดาษผิดชนิดก็อ่านออกมาเป็นบั๊กในสายตาผู้ใช้ แม้ทุกไบต์ของ PDF จะถูกต้องก็ตาม
ใช้เอนจินเดียวกันทั้งฝั่งตัวอย่างและฝั่งพิมพ์
ยังมีการตัดสินใจข้อสุดท้ายที่กำกับความเที่ยงตรงอยู่เงียบ ๆ การเลือกเอนจินเรนเดอร์มีผลกับปลายทางทั้งฝั่งหน้าจอและฝั่งเครื่องพิมพ์ สิ่งล่อใจจึงเป็นการแสดงตัวอย่างด้วยเอนจินที่เร็วแล้วพิมพ์ด้วยเอนจินที่แม่น จงต้านทานความคิดนั้นไว้ การขับภาพตัวอย่างกับงานพิมพ์ผ่านเอนจินคนละตัวจะดึงความคลาดเคลื่อนเชิงความเที่ยงตรงที่ภาพตัวอย่างแบบตรงกับเครื่องพิมพ์ถูกสร้างขึ้นมากำจัด กลับเข้ามาใหม่ และดึงกลับมาในแบบที่มองเห็นได้บนกระดาษเท่านั้น การชั่งน้ำหนักระหว่างเอนจินในตัว Cairo และ PDFium อยู่ใน การเรนเดอร์ PDF แบบหลายเอนจินใน Delphi เลือกมาหนึ่งตัวแล้วใช้มันทั้งสองฝั่ง
เอกสารที่ใหญ่เกินกว่าจะโหลดได้อย่างสบายใจก่อนพิมพ์ สามารถเปิดผ่านเส้นทาง direct access ที่อธิบายไว้ใน การรวม แยก และเข้าถึงไฟล์ PDF ขนาดใหญ่โดยตรง ซึ่งเรนเดอร์หน้าลง device context จาก file handle ได้โดยไม่ต้องสร้างต้นไม้เอกสารขึ้นมา เอกสารอ้างอิง API การพิมพ์ฉบับสมบูรณ์อยู่ที่หน้าผลิตภัณฑ์ losLab PDF Library for Delphi