บทความเทคนิค

การแสดงผล PDF แบบหลายเอ็นจิ้นใน Delphi: ในตัว, Cairo และ PDFium ด้วย PDF Library for Delphi

ตัวแปลงภาพแรสเตอร์สามตัวอ่านไฟล์ PDF ฉบับเดียวกันแล้วอาจเห็นไม่ตรงกันว่าไฟล์นั้นบอกอะไร เอนจินในตัวของ PDF Library for Delphi คือตัวที่มาพร้อมกันโดยไม่ต้องมีไฟล์เพิ่มและเรนเดอร์ทุกอย่างได้อย่างมีฝีมือ จึงสมควรได้ตำแหน่งค่าเริ่มต้น Cairo นำไปป์ไลน์ความโปร่งใสและการลบรอยหยักคนละแบบเข้ามา และมักเป็นตัวที่คนหยิบมาใช้เมื่อ soft mask หรือ blend mode ออกมาผิดเพี้ยนในที่อื่น ส่วน PDFium พกโค้ดเรนเดอร์ของ Chrome มาด้วย หน้าที่ดูถูกต้องในเบราว์เซอร์จึงมักดูถูกต้องภายใต้ PDFium เช่นกัน โดยแลกกับ DLL ขนาดใหญ่และ bitness ที่มันยืนกรานว่าต้องตรงกัน ไม่มีตัวใดในสามตัวนี้ที่ถูกต้องแบบลอย ๆ ความถูกต้องขึ้นอยู่กับเอกสารแต่ละฉบับ และทางเดียวที่ซื่อตรงในการรู้ว่าเอนจินใดรับมือกับคลังเอกสารชุดหนึ่งได้ดี คือการรันคลังเอกสารชุดนั้นผ่านทุกตัว

นั่นคือเหตุผลที่ควรมองเอนจินเป็นตัวเลือกระดับรันไทม์ ไม่ใช่ระดับตอนบิลด์ PDF Library for Delphi ซึ่งเป็นไลบรารี PDF สำหรับ Delphi และ C++Builder จาก losLab วางทั้งสามตัวไว้หลังพื้นผิวการเรนเดอร์เดียวกัน การตัดสินใจจึงมีต้นทุนเพียงจำนวนเต็มหนึ่งตัวแทนที่จะเป็นการแตกสาขาของโค้ด ที่เหลือจากนี้จึงเป็นเรื่องของการเลือกใช้ระหว่างกันอย่างปลอดภัย การยืนยันว่าไบนารีที่ดีพลอยไปแล้วมีเอนจินใดติดไปด้วยจริง ๆ และการกันไม่ให้สถานะการเรนเดอร์ไปทำพิษกับงานถัดไปอย่างเงียบ ๆ

ตัวแปลงภาพแรสเตอร์สามตัวหลังพื้นผิวการเรียกเดียว

ไลบรารีให้หมายเลขกับเอนจินของมัน เอนจิน 1 คือตัวเรนเดอร์ในตัวซึ่งเป็นค่าเริ่มต้น พร้อมตัวเลือกการลบรอยหยักของ GDI+ บน Windows เอนจิน 2 คือ Cairo และเอนจิน 3 คือ PDFium ทั้งคู่เลือกได้ที่รันไทม์ผ่าน SelectRenderer เอนจินภายนอกสองตัวนี้โหลดมาจาก DLL ที่คุณระบุพาธไว้ด้วย SetCairoFileName และ SetPDFiumFileName ก่อนจะเลือกใช้ ไม่ว่าเอนจินใดจะทำงานอยู่ งานก็ผ่านการเรียกชุดเดียวกัน คือ RenderPageToFile, RenderPageToStream และ RenderDocumentToFile การสลับเอนจินคือการขยับตัวเลขหนึ่งตัว ส่วนโค้ดเรนเดอร์ที่เหลือของคุณไม่รู้สึกอะไรเลย

โมเดลปลายทางไปไกลกว่าบิตแมปมาก คลาสตัวเรนเดอร์ยังส่งออกไปยัง metafile (WMF, EMF, EMF+), EPS, device context โดยตรง เครื่องพิมพ์ และ HTML5 ได้ด้วย โดย Cairo และ PDFium จะโผล่มาเป็นปลายทางเพิ่มเติมก็ต่อเมื่อถูกคอมไพล์รวมเข้ามาแล้วเท่านั้น เอาต์พุตแบบแรสเตอร์คือจุดที่เอนจินทั้งสามแตกต่างกันชัดที่สุด ตัวอย่างในบทความนี้จึงใช้เอาต์พุตแบบนั้น

สามเอนจินเรนเดอร์ PDF อยู่หลังพื้นผิวเรียกชุดเดียว: SelectRenderer สลับระหว่างเอนจินในตัว, Cairo และ PDFium ขณะที่โค้ดแอปพลิเคชันยังเรียกฟังก์ชันเรนเดอร์ชุดเดิม
SelectRenderer แลกเลขจำนวนเต็มหนึ่งตัวเพื่อย้ายงานระหว่างเอนจิน built-in, Cairo และ PDFium โค้ดของแอปพลิเคชันยังคงเรียก RenderPageToFile และเพื่อนต่อไป ไม่ว่าเอนจินใดจะผลิตพิกเซลก็ตาม

อย่าสมมติว่าเอนจินมีอยู่จริง: ตรวจสอบตอนเริ่มโปรแกรม

Cairo และ PDFium เป็นฟีเจอร์แบบคอมไพล์ตามเงื่อนไข ซึ่งหมายความว่าไบนารีหนึ่งอาจถูกบิลด์ขึ้นโดยไม่มีทั้งสองตัวเลย เมื่อเป็นเช่นนั้น การขอเอนจิน 2 หรือ 3 จะไม่ทำให้เกิดข้อยกเว้นใด ๆ SelectRenderer เพียงคืนค่าที่ไม่ใช่ ID ที่คุณร้องขอ และโค้ดที่เมินค่าคืนกลับก็จะเรนเดอร์ต่อไปด้วยเอนจินตัวที่ทำงานอยู่ก่อนแล้ว ทางป้องกันคือการตรวจสอบตอนเริ่มโปรแกรม โดยให้เอนจินแต่ละตัวแสดงตัวออกมาแล้วบันทึกคำตอบไว้:

function ProbeEngines(PDF: TPDFlib): string;
begin
  Result := 'built-in';                        // เอนจิน 1 มีอยู่เสมอ
  if (PDF.SetCairoFileName('cairo.dll') = 1) and (PDF.SelectRenderer(2) = 2) then
    Result := Result + ', cairo';
  if (PDF.SetPDFiumFileName('pdfium.dll') = 1) and (PDF.SelectRenderer(3) = 3) then
    Result := Result + ', pdfium';
  PDF.SelectRenderer(1);                       // คืนค่าเริ่มต้นก่อนเริ่มงานจริง
end;

จงรันการตรวจสอบนั้นครั้งเดียวตอนเริ่มโปรแกรม แล้วเขียนผลลัพธ์ลงล็อกควบคู่ไปกับงานเรนเดอร์ทุกงาน คำถามเดียวที่พบบ่อยที่สุดเมื่อลูกค้ารายงานความต่างของการเรนเดอร์คือ การติดตั้งของเขามีเอนจินใดอยู่บ้างจริง ๆ และคำตอบบรรทัดเดียวที่นอนอยู่ในล็อกก็ปิดประเด็นได้โดยไม่ต้องเปิดรีโมตเดสก์ท็อป ผลพลอยได้ที่มีประโยชน์คือ ถ้า SetPDFiumFileName เองคืนค่า 0 คุณก็รู้ทันทีว่าปัญหาอยู่ที่ตัว DLL (พาธผิด bitness ผิด หรือขาด dependency) ไม่ใช่ไบนารีที่คอมไพล์มาโดยไม่รองรับ PDFium เพราะการเรียกกำหนดพาธนั้นหาอะไรไม่เจอตั้งแต่ก่อนที่ SelectRenderer จะได้ทำงานเสียอีก

รูปแบบเอาต์พุตสิบชนิดหลังจำนวนเต็ม Options ตัวเดียว

พารามิเตอร์ Options ในการเรียกเรนเดอร์เป็นตัวเลือกการเข้ารหัสเอาต์พุต โดย 0 คือ BMP, 1 คือ JPEG, 2 คือ WMF, 3 คือ EMF, 4 คือ EPS, 5 คือ PNG, 6 คือ GIF, 7 คือ TIFF, 8 คือ EMF+ และ 9 คือ HTML5 ค่า PNG (5) เป็นค่าเริ่มต้นที่สมเหตุสมผลสำหรับภาพพรีวิวและภาพหน้าเอกสารเพื่อการจัดเก็บ ส่วน JPEG (1) ที่จับคู่กับ SetJPEGQuality เป็นตัวเลือกที่ดีกว่าสำหรับงานสแกนภาพถ่ายซึ่งขนาดไฟล์สำคัญกว่าความคมของขอบ

มีรูปแบบหนึ่งที่ซ่อนข้อกำหนดเกี่ยวกับสตรีมปลายทางไว้ เส้นทางของ BMP จะเขียนข้อมูลภาพก่อน แล้วจึงย้อนกลับไปที่ offset 0x26 เพื่อปะค่าฟิลด์ความละเอียดในส่วนหัว หากคุณชี้มันไปยังสตรีมที่เดินหน้าได้อย่างเดียว เช่น ตัวห่อการบีบอัดหรือซ็อกเก็ตเครือข่าย การเรียกจะล้มเหลวในลักษณะที่อ่านดูเหมือนความผิดพลาดของเอนจิน ทั้งที่ไม่ใช่ เมื่อเลี่ยงปลายทางที่ seek ไม่ได้ไม่พ้น ให้เรนเดอร์เป็น PNG แทน หรือพัก BMP ไว้ในเมมโมรีสตรีมก่อนแล้วค่อยคัดลอกออกไปเมื่อเสร็จสมบูรณ์

ค่า DPI ที่คุณส่งเข้าไปไม่ใช่ค่า DPI ที่คุณได้

การเรียกเรนเดอร์ทุกครั้งรับอาร์กิวเมนต์ DPI แต่ความละเอียดที่คุณได้จริงคือค่านั้นคูณด้วยสเกลการเรนเดอร์ระดับส่วนกลาง SetRenderScale เริ่มต้นที่ 1.0 และเมื่อคุณเปลี่ยนค่าแล้ว ตัวคูณใหม่จะมีผลกับการเรนเดอร์ทุกครั้งถัดไปบนอินสแตนซ์นั้นอย่างเงียบ ๆ:

PDF.SetRenderScale(2.0);                    // การเรนเดอร์ทุกครั้งถัดไปถูกคูณสอง
PDF.RenderPageToFile(150, 1, 5, 'p1.png');  // เท่ากับ 300 DPI จริง
PDF.SetRenderScale(1.0);                    // รีเซ็ต ไม่อย่างนั้นภาพย่อจะออกมาใหญ่มหึมา

ความติดค้างแบบเดียวกันนี้ใช้กับ SetRenderCropType และค่าคุณภาพ JPEG ด้วย ในบริการที่ผลิตทั้งภาพย่อ ภาพพรีวิว และภาพความละเอียดระดับงานพิมพ์จากอินสแตนซ์ที่ใช้ร่วมกันเพียงตัวเดียว ค่าที่ตกค้างเหล่านี้คือเบื้องหลังจริงของทิกเก็ตที่โผล่มาเป็นครั้งคราวว่า "ภาพย่อใหญ่ 40 MB ขึ้นมาเฉย ๆ" ทางออกที่สะอาดมีสองทาง คือรีเซ็ตสถานะที่เกี่ยวข้องที่ต้นของทุกการดำเนินการ หรือแยกอินสแตนซ์ต่างหากให้กับโปรไฟล์เอาต์พุตแต่ละแบบ จะได้ไม่มีอะไรรั่วข้ามกัน

ผังงานตรวจเอนจินตอนเริ่มทำงานของ PDF Library for Delphi: เรนเดอเรอร์แต่ละตัวยืนยันพาธ DLL ของตัวเองและคำตอบของ SelectRenderer ก่อนสรุปความพร้อมใช้งานถูกบันทึกไว้เคียงงานเรนเดอร์ทุกงาน
การเรียก path ที่ล้มเหลวชี้โทษไปที่ DLL ขณะที่ผล SelectRenderer ที่ไม่ตรงหมายความว่าไบนารีไม่เคยคอมไพล์เอนจินนั้นเข้าไปเลย ตัวสำรวจรันครั้งเดียว และสรุปหนึ่งบรรทัดของมันเคลียร์คำถามการเรนเดอร์ของลูกค้าส่วนใหญ่

การปรับจูนเอนจินค่าเริ่มต้นก่อนจะไปคว้าตัวอื่น

คำร้องขอแบบ "เราต้องใช้เอนจินตัวอื่น" จำนวนมากอย่างน่าประหลาดใจกลับกลายเป็นปัญหาการตั้งค่าที่ปลอมตัวมา ตัวเรนเดอร์ในตัวเปิดเผยพฤติกรรมการลบรอยหยักผ่าน SetGDIPlusOptions และตระกูล SetRenderOptions ที่กว้างกว่า ส่วน SetGDIPlusFileName ให้คุณเล็งไปยังรันไทม์ GDI+ ตัวที่ต้องการได้ เมื่อสภาพแวดล้อมการติดตั้งมีตัวที่ผิดแผกไปจากปกติ ลายเส้นหยักที่ DPI ต่ำ ตัวอักษรฟุ้งในภาพย่อ แถบสีเป็นชั้นในการไล่ระดับ ทั้งหมดนี้ตอบสนองต่อปุ่มปรับเหล่านั้น และการหมุนปุ่มก็ไม่มีต้นทุนอะไรกับตัวติดตั้งเลย ในทางกลับกัน การเพิ่ม Cairo หรือ PDFium หมายถึงการต้องส่ง DLL เพิ่ม ต้องตามรอย bitness อีกหนึ่งหรือสองรุ่น และต้องแบกภาระอัปเดตมันเอง

ดังนั้นการร้องเรียนเรื่องคุณภาพจึงมีลำดับการทำงานตามธรรมชาติ อย่างแรกให้ทำซ้ำที่ค่า DPI และสเกลตรงตามที่ลูกค้าใช้เป๊ะ ๆ เพราะครึ่งหนึ่งของกรณีความต่างจะระเหยหายไปเมื่อสองค่านั้นตรงกัน จากนั้นจึงลองตัวเลือกการลบรอยหยักของเอนจินในตัว แล้วค่อยวางหน้านั้นเทียบกันข้ามเอนจินโดยตรึงตัวแปรอื่นทั้งหมดไว้คงที่ คือเรนเดอร์เป็น PNG ผ่านเอนจิน 1, 2 และ 3 ที่ DPI เท่ากัน แล้วแนบมาทั้งสามภาพ โดยทั่วไปสองในสามจะตรงกัน และเสียงข้างมากนั้นจะบอกคุณว่าตัวที่แปลกแยกเกิดจากเอกสารถูกตีความต่างออกไป หรือเกิดจากความคาดหวังพื้นฐานของคุณเองที่คลาดเคลื่อน ภาพจริงสามภาพยุติข้อโต้แย้งเรื่อง "เรนเดอร์ผิด" ได้เร็วกว่าคำบรรยายเต็มย่อหน้าอยู่มาก

ห่วงโซ่การถอยกลับที่อธิบายตัวเองได้

เมื่อการตรวจสอบเอนจินและวินัยเรื่องสถานะเข้าที่แล้ว ตัวห่วงโซ่การถอยกลับเองก็สั้นมาก การตรวจจับความล้มเหลวอาศัย LastRenderError ซึ่งเก็บข้อความของเอนจินเองสำหรับการเรนเดอร์ครั้งล่าสุด และจะว่างเปล่าเมื่อการเรนเดอร์สำเร็จ:

procedure RenderPageWithFallback(PDF: TPDFlib; Page: Integer; const OutFile: string);
begin
  PDF.SelectRenderer(1);                            // ลองตัวในตัวก่อน
  PDF.RenderPageToFile(200, Page, 5, OutFile);      // 5 = PNG
  if PDF.LastRenderError = '' then Exit;
  LogEngineFailure('built-in', Page, PDF.LastRenderError);
  if PDF.SelectRenderer(3) = 3 then                 // PDFium เป็นตัวถอยกลับสำรองตัวหนัก
  begin
    PDF.RenderPageToFile(200, Page, 5, OutFile);
    if PDF.LastRenderError = '' then Exit;
    LogEngineFailure('pdfium', Page, PDF.LastRenderError);
  end;
  raise Exception.CreateFmt('Page %d failed on all available engines', [Page]);
end;

มีจุดออกแบบสองข้อที่มีน้ำหนักตรงนี้ ข้อแรก ห่วงโซ่นี้บันทึกว่าทำไมการสลับแต่ละครั้งจึงเกิดขึ้น เพราะบรรทัดล็อกที่เขียนว่า "หน้านี้ถอยไปใช้ PDFium ตั้งแต่รุ่น 3.7" คือสัญญาณการถดถอยที่คุณอยากเห็นแนวโน้มของมันในระบบมอนิเตอร์ ไม่ใช่ปล่อยให้หายไป ข้อสอง ลำดับการถอยกลับเองก็เป็นนโยบายที่ควรเลือกตามภาระงานแต่ละแบบ เอนจินในตัวดีพลอยได้โดยไม่ต้องมี DLL เพิ่ม จึงเป็นตัวลองอันดับแรกที่ถูกต้องในการติดตั้งส่วนใหญ่ ขณะที่เอกสารซึ่งหนาแน่นด้วย transparency group หรือ shading ที่ผิดแผก คือเหตุผลตามปกติที่ทีมงานจะต่อเอนจินทางเลือกเข้ามาตั้งแต่แรก ไม่มีเอนจินตัวใดเร็วที่สุดแบบทั่วไป ซึ่งนั่นคือเหตุผลทั้งหมดของการเลือกเป็นราย ๆ ไป จงวัดประสิทธิภาพแต่ละตัวกับตัวอย่างเอกสารจริงของคุณที่ DPI จริงของคุณ และกลับมาวัดใหม่ทุกครั้งที่ DLL ของเอนจินหรือส่วนผสมของเอกสารเปลี่ยนไป คลังเอกสารเป็นฝ่ายชนะการถกเถียงเสมอ

ห่วงโซ่สำรองการเรนเดอร์ PDF: เอนจินในตัวลองก่อน ความล้มเหลวถูกบันทึกลง log, PDFium ลองซ้ำ และเมื่อเอนจินที่มีทั้งหมดล้มเหลวกับหน้าหนึ่ง exception จะถูกยกขึ้นรายงาน
แต่ละความพยายามตรวจ LastRenderError และบันทึกเหตุผลก่อนสลับเอนจิน ห่วงโซ่จะ raise เมื่อเอนจินที่ติดตั้งทุกตัวล้มเหลวเท่านั้น โดยสาเหตุที่เก็บไว้นั่งรออยู่ใน log เรียบร้อยแล้ว

เลยหน้าเดี่ยวออกไป: แบตช์ TIFF และ device context ที่ทำงานอยู่จริง

เพื่อนบ้านอีกสองตัวของการเรียกแบบรายหน้าช่วยเติมเต็มกล่องเครื่องมือ RenderAsMultipageTIFFToFile เรนเดอร์นิพจน์ช่วงหน้าตรงเข้าไปเป็นไฟล์ TIFF หลายหน้า ซึ่งเป็นรูปทรงตามธรรมชาติสำหรับการส่งมอบเพื่อจัดเก็บให้ระบบจัดการเอกสารที่เก่าแก่กว่า PDF ส่วน RenderPageToDC วาดลงบน device context ของ Windows โดยตรงสำหรับคอนโทรลพรีวิว และถูกกำกับด้วยการตั้งค่าติดค้างสามตัวของมันเอง (SetRenderDCOffset, SetRenderDCErasePage และชนิดการครอป) ซึ่งต้องการวินัยการรีเซ็ตแบบเดียวกับตัวคูณสเกล การพรีวิวบนหน้าจอและการเรนเดอร์ตามเส้นทางงานพิมพ์มีกับดักของตัวเองมากพอที่จะสมควรมีบทความเฉพาะ ซึ่งลิงก์ไว้ด้านล่าง

ไปต่อที่ไหนดี

มีนิสัยหนึ่งที่ควรติดตัวไว้ เนื่องจาก SelectRenderer มีผลกับการเรียกทุกครั้งถัดไปบนอินสแตนซ์นั้น หน้าที่ดื้อเพียงหน้าเดียวจึงลองใหม่บนเอนจินอื่นได้ ในขณะที่ส่วนที่เหลือของเอกสารยังอยู่กับค่าเริ่มต้น สำหรับการวาดภาพพรีวิว การเลือกเครื่องพิมพ์ และการจัดการ DevMode อ่านต่อได้ที่บทความว่าด้วยการพรีวิวงานพิมพ์และ device context เมื่อการเรนเดอร์ป้อนเข้าสู่ไปป์ไลน์ปริมาณสูงบนไฟล์ขนาดใหญ่มาก แนวทางที่อิงแฮนเดิลในคู่มือ direct access ก็เข้าคู่กันได้อย่างเป็นธรรมชาติกับการเรนเดอร์รายหน้าผ่าน DARenderPageToFile

รายละเอียดการแพ็กเกจเอนจิน รูปแบบไฟล์ที่รองรับ และบิลด์ทดลองใช้ อยู่ที่หน้าผลิตภัณฑ์ PDF Library for Delphi