HotPDF คอมโพเนนต์ PDF แบบเนทีฟสำหรับ Delphi และ C++Builder นำเข้า metafile ของ Windows แบบ EMF และ WMF ด้วยการแปลผล (interpret) แต่ละ record ของ GDI ให้เป็น PDF operator โดยตรง แทนที่จะ flatten ไฟล์ให้เป็นบิตแมป นั่นคือ gradient fill กลายเป็น PDF axial shading pattern, hatch brush กลายเป็น PDF tiling pattern และมี path-state gate ที่รวมศูนย์คอยกันไม่ให้ record ที่ผิดรูปแบบทำให้เอาต์พุตเสียหาย กราฟใดๆ ที่ TChart, พื้นผิว GDI+ หรือ TCanvas ธรรมดา export ออกมาเป็น enhanced metafile ได้ ล้วนเป็นตัวเลือกสำหรับเส้นทางนี้ และความแตกต่างจะปรากฏขึ้นทันทีที่มีใครซูมเข้าไปดูหน้ากระดาษหรือส่งไปพิมพ์ด้วยเครื่องพิมพ์ความละเอียดสูง
ทางเลือกที่นักพัฒนา Delphi ส่วนใหญ่มักใช้เป็นค่าเริ่มต้นคือการแปลง metafile เป็นบิตแมปก่อนวางลงบนหน้ากระดาษ และต้นทุนของวิธีนี้จะปรากฏทีหลัง กราฟแท่งที่คมชัดบนหน้าจอจะเห็นเป็นก้อนพิกเซลชัดเจนทันทีที่พิมพ์ PDF ที่ 600 DPI หรือฉายบนจอห้องประชุม และพื้นที่ CAD ที่เติมด้วย hatch จะยุบเหลือเป็นสี่เหลี่ยมสีเทาแบนราบเดียวถ้าไม่ได้นำ fill style ติดไปด้วย การอ่าน metafile ในฐานะโปรแกรมแทนที่จะเป็นภาพคือสิ่งที่หลีกเลี่ยงปัญหาทั้งสองนี้ได้ และก็เป็นเส้นทางที่ implement ให้ถูกต้องได้ยากกว่า จึงเป็นเหตุผลที่ข้อควรระวังด้านล่างนี้ควรรู้ไว้ก่อนที่รายงานจะถูกส่งออกจริง
ทำไมต้องแปลผล metafile แทนที่จะ flatten เป็นบิตแมป
HotPDF ยึดการนำเข้า EMF และ WMF ไว้บนเส้นทางเวกเตอร์ เพราะ metafile ของ Windows คือลำดับการเรียกวาดของ GDI ที่ถูกบันทึกไว้ ไม่ใช่ภาพ และการเล่นซ้ำ (replay) การเรียกเหล่านั้นให้เป็น PDF path, text และ shading operator คือสิ่งที่ทำให้ผลลัพธ์สเกลได้เหมือนส่วนอื่นของหน้ากระดาษ THPDFPage.ShowMetafile และคู่หูของมัน ShowMetafileEx คือจุดเข้าที่แอปพลิเคชันเรียกใช้ ทั้งสองส่ง metafile ต่อไปให้ THPDFWmf คลาสที่เดินผ่านทุก record ของ GDI และแปลมัน ความแตกต่างนี้ไม่ใช่เรื่องเด็ดขาดสมบูรณ์ และ HotPDF ก็ไม่ได้แสร้งทำเป็นอย่างนั้น record ของ metafile ที่เป็นข้อมูล raster จริงๆ เช่น bitmap blit แบบ StretchDIBits จะถูกฝังเป็น PDF Image XObject จริงผ่าน AddImage และ ShowImage ซึ่งเป็นคู่การเรียกเดียวกับที่ภาพอื่นใดบนหน้าใช้ แทนที่จะถูกบังคับให้เป็น path operator ซึ่งไม่สามารถแสดงภาพถ่ายได้ เส้น พื้นที่เติมสี และข้อความยังคงเป็นเวกเตอร์ ส่วนพิกเซลที่เป็นพิกเซลอยู่แล้วในต้นฉบับก็ยังคงเป็นพิกเซลในเอาต์พุต การเรียกใช้แบบง่ายที่สุดต้องการแค่ metafile ที่โหลดแล้วเท่านั้น
var
Pdf: THotPDF;
Chart: TMetafile;
begin
Pdf := THotPDF.Create(nil);
Chart := TMetafile.Create;
try
Chart.LoadFromFile('quarterly-revenue.emf'); // exported from TChart or GDI+
Pdf.FileName := 'quarterly-report.pdf';
Pdf.BeginDoc;
Pdf.CurrentPage.ShowMetafile(Chart);
Pdf.EndDoc;
finally
Chart.Free;
Pdf.Free;
end;
end;
ตัวแปลผลแปลงพิกัด GDI ให้เป็นพื้นที่หน้า PDF ได้อย่างไร
HotPDF ตอบคำถามนี้ด้วยการเดินผ่าน record stream ของ metafile เพียงรอบเดียว แทนที่จะ implement GDI ซ้ำเป็นครั้งที่สอง THPDFWmf.Analyse อ่าน header ของ metafile ผ่านฟังก์ชัน Win32 GetEnhMetaFileHeader รีเซ็ต state การวาดภายในของมัน แล้วเรียก EnumEnhMetafile ซึ่งเป็น enumeration API ตัวเดียวกับที่โปรแกรมดู metafile จะใช้ ดังนั้นทุก record ประเภท EMR_* จะไปถึง THPDFWmf.ExecuteRecord ตามลำดับที่ถูกบันทึกไว้จริง GDI แสดงพิกัดแบบบนลงล่างในหน่วย device หรือ logical ที่เลือกโดย mapping mode ของ metafile เอง ส่วนหน้า PDF เป็นแบบล่างขึ้นบนในหน่วย point ของ user-space ซึ่งเป็นระบบพิกัดที่ครอบคลุมในโมเดลการวาดแบบ canvas ของ HotPDF สำหรับ path และ fill ตัวจัดการ record แต่ละตัวแก้ความไม่ตรงกันนี้ผ่าน ScaleX และ ScaleY ซึ่งเรียก ProjectX และ ProjectY เพื่อเล่นซ้ำสูตร window-to-viewport ของ GDI เองสำหรับ mapping mode แบบ anisotropic และ isotropic ดังนั้นรูปทรงที่ถูกบันทึกไว้กว้างห้า logical unit จะไปลงเอยที่ความกว้างที่ถูกต้องในหน่วย PDF point ไม่ว่า window และ viewport extent ที่แอปพลิเคชันต้นทางตั้งไว้จะเป็นเท่าใดก็ตาม
gradient fill แบบ GDI กลายเป็น PDF shading pattern ได้อย่างไร
record EMR_GRADIENTFILL จะกลายเป็น PDF Type 2 axial shading pattern จริง (ISO 32000-1 §8.7.4.5) ทุกครั้งที่ GDI บันทึกมันไว้ในโหมดสี่เหลี่ยมสองแบบใดแบบหนึ่ง THPDFWmf.VEMRGradientFill อ่าน layout ของ record นั้นตรงจาก raw byte buffer โดยเดินตามโครงสร้าง MS-EMF §2.3.1.6 คือ array ของมุมสี RGBA 16 บิต ตามด้วยรายการสี่เหลี่ยมที่แต่ละอันอ้างอิงถึงสอง vertex เหล่านั้น สำหรับ GRADIENT_FILL_RECT_H สีจะไล่จากซ้ายไปขวาตามแนวกลางแนวนอนของสี่เหลี่ยม ส่วน GRADIENT_FILL_RECT_V สีจะไล่จากบนลงล่างตามแนวกลางแนวตั้ง ไม่ว่าแบบไหน สีของมุมทั้งสองและพิกัดสี่เหลี่ยมที่ project แล้วจะถูกส่งตรงเข้า THotPDF.RegisterAxialGradient ซึ่งคืนชื่อ pattern กลับมา แล้วหน้ากระดาษก็วาดสี่เหลี่ยมและเติมสีผ่าน pattern นั้น (SetFillPattern) แทนการเรียก SetRGBFillColor แบบสีเดียว ดังนั้น header แบบมีแถบสีของสเปรดชีตหรือพื้นที่กราฟแบบ gradient จึงคง blend ของมันไว้ แทนที่จะยุบเหลือสีเฉลี่ยเดียว
โหมด Gouraud triangle คือช่องว่างที่ยอมรับตรงๆ เมื่อฟิลด์ ulMode ของ record รายงานเป็น GRADIENT_FILL_TRIANGLE VEMRGradientFill จะรู้จักมัน บันทึก log ว่าโหมด triangle ยังไม่ได้ implement และข้ามสี่เหลี่ยมนั้นไปแทนที่จะเดาประมาณด้วยสองสี การ interpolation แบบต่อ vertex ต่อพิกเซลข้าม mesh สามเหลี่ยมที่มีรูปแบบอิสระ ไม่สามารถลดทอนให้เหลือ axial หรือ radial shading แบบสองจุดหยุดสีได้ และการแสดงมันอย่างถูกต้องจะหมายถึงการสร้าง PDF Type 4 หรือ Type 5 mesh shading ซึ่งเป็นตระกูล shading เดียวกับที่ตัว renderer หน้าของ HotPDFก็ปล่อยว่างไม่วาดเช่นกันเมื่ออ่าน PDF กลับมา สอง code path ที่ไม่เกี่ยวข้องกันมาบรรจบที่ขอบเขตเดียวกัน mesh shading คือช่องว่างทั้งฝั่งเขียนและฝั่งอ่าน และไดอะแกรมต้นทางที่ใช้ Gouraud triangle เพื่อสร้างแสงเรืองแบบ radial ที่นุ่มนวลจะ fallback ไปเป็นแปรงทึบสีสุดท้ายที่ใช้อยู่ ไม่ใช่ภาพประมาณค่าที่ถูก render
Hatch brush กลายเป็น tiling pattern ไม่ใช่สีเทาแบนราบ
hatch brush ของ GDI ยังคงลวดลายของมันไว้ใน PDF เพราะ THPDFWmf.SetBrushColor ตรวจสอบ CurrentBrush.lbStyle ว่าเป็น BS_HATCHED หรือไม่ ก่อนที่จะ fallback ไปเติมสีทึบ โดยส่งกรณีนี้ไปที่ SetHatchBrushPattern แทน method นั้นเขียน PDF content stream ขนาด 8x8 หน่วยของ operator เส้นแบบ stroke ได้แก่ m, l และ S ซึ่งเลือกตาม GDI hatch style คือเส้นแนวนอนหรือแนวตั้งเส้นเดียวสำหรับ HS_HORIZONTAL และ HS_VERTICAL, เส้นทแยงขนานสามเส้นสำหรับ HS_FDIAGONAL และ HS_BDIAGONAL และการรวมแนวนอนกับแนวตั้งหรือทแยงทั้งสองทิศสำหรับ HS_CROSS และ HS_DIAGCROSS THotPDF.RegisterTilingPattern ลงทะเบียน content stream นั้นเป็น colored tiling pattern (PaintType 1, ISO 32000-1 §8.7.3.1) ที่มี XStep และ YStep ขนาด 8 หน่วย และหน้ากระดาษเติมสีผ่าน SetFillPattern แบบเดียวกับ axial shading ผังพื้น CAD หรือแบบวิศวกรรมที่อาศัย hatch fill เพื่อแยกแยะวัสดุ จะคงภาษาภาพนั้นไว้ใน PDF แทนที่จะสูญเสียทุกพื้นที่ให้กลายเป็นสีเทาเดียวกันหมด
ไม่ใช่ทุกแปรงจะได้รับการปฏิบัติแบบนั้น และช่องว่างตรงนี้ก็ควรรู้ไว้ก่อนที่การนำเข้า CAD จะถูกส่งจริง EMR_CREATEDIBPATTERNBRUSHPT ซึ่งเป็น record สำหรับ pattern brush แบบภาพบิตแมปที่กำหนดเอง ไม่ใช่หนึ่งใน hatch style สำเร็จรูปหกแบบของ GDI จะลงทะเบียนแค่ handle ของมัน เพื่อให้ record SELECTOBJECT และ DELETEOBJECT ที่ตามมายังคงสอดคล้องกัน HotPDF ยังไม่มี pipeline PDF Pattern resource สำหรับภาพ tile แบบอิสระ ดังนั้นการเลือกแปรงนั้นจะตกไปเป็น fallback สีทึบแทนลวดลายต้นฉบับ ถ้าพื้นที่เติมสีออกมาแบนราบทั้งที่ต้นฉบับใช้ลวดลายภาพซ้ำอย่างชัดเจน แปรงต้นทางแทบจะแน่นอนว่าเป็น custom DIB pattern ไม่ใช่ hatch มาตรฐาน และนั่นคือกรณีเดียวที่ควรตรวจสอบด้วยมือก่อน การตั้งค่าการนำเข้าสำหรับภาพวาดแบบนั้นยังคงผ่าน options object ตัวเดียวกัน
var
Pdf: THotPDF;
Drawing: TMetafile;
Options: THPDFEmfOptions;
begin
Pdf := THotPDF.Create(nil);
Drawing := TMetafile.Create;
Options := THPDFEmfOptions.Create;
try
Drawing.LoadFromFile('floor-plan.emf');
Options.Assign(Pdf.EmfOptions); // start from the document-wide defaults
Options.Redraw := False; // interpret the original EMF bytes, no GDI re-record pass
Options.ShowNullBrush := True; // keep explicitly unfilled CAD regions visible
Options.UseFrame := True; // clip output to the frame the EMF header declares
Pdf.FileName := 'floor-plan.pdf';
Pdf.BeginDoc;
Pdf.CurrentPage.ShowMetafileEx(Drawing, Options);
Pdf.EndDoc;
finally
Options.Free;
Drawing.Free;
Pdf.Free;
end;
end;
อะไรป้องกัน metafile ที่ผิดรูปแบบไม่ให้ทำให้หน้ากระดาษเสียหาย
คำตอบของ HotPDF คือ gate เดียวที่อยู่ด้านบนสุดของ ExecuteRecord แทนที่จะเป็นการตรวจสอบเชิงป้องกันที่ต้องทำซ้ำในตัวจัดการ record แต่ละตัวจากทั้งหมดประมาณแปดสิบตัว วงเล็บ path ของ GDI ที่เปิดโดย EMR_BEGINPATH และปิดโดย EMR_ENDPATH หรือ EMR_ABORTPATH ถูกติดตามด้วย property ส่วนตัวชื่อ PathContinue ซึ่งหนุนหลังด้วยฟิลด์ FPathContinue ในขณะที่วงเล็บนั้นยังเปิดอยู่ ExecuteRecord จะยอมให้ผ่านเฉพาะ record ที่สร้าง path เท่านั้น ได้แก่ variant ของ move, line, polyline, polygon, polybezier และ polydraw บวกกับ CLOSEFIGURE และชุดเล็กๆ ของ record ประเภท transform และ DC-state เช่น SETWORLDTRANSFORM, SAVEDC และ RESTOREDC record ประเภทอื่นใดที่มาถึง ExecuteRecord ขณะที่วงเล็บยังเปิดอยู่ เช่น EXTTEXTOUT ที่หลงเข้ามา หรือ bitmap blit จะถูกทิ้งจากศูนย์กลางด้วย Exit เดียวทันทีที่มันมาถึง
gate นี้มีอยู่เพราะวงเล็บ path ใน metafile ที่เขียนขึ้นด้วยมือ สร้างจากเครื่องมือ หรือเพียงแค่เสียหาย ไม่มีการรับประกันว่าจะมีแค่สิ่งที่ไฟล์ที่มีรูปแบบถูกต้องจะใส่ระหว่าง record เปิดและปิดของมัน record แสดงข้อความที่ตกลงมาระหว่าง EMR_BEGINPATH กับ EMR_ENDPATH หากไม่มี gate จะสร้างมลพิษให้กับ geometry ของ path ที่กำลังสร้างอยู่ หรือไม่ก็ปล่อย PDF text-showing operator ออกมากลางลำดับที่ควรจะเป็นการสร้าง path ล้วนๆ และความล้มเหลวทั้งสองแบบเป็นชนิดที่ปรากฏขึ้นจากอินพุตที่ผิดรูปแบบตัวใดตัวหนึ่งจากเครื่องมือของบุคคลที่สาม ไม่ใช่อะไรที่ test suite ปกติจะครอบคลุมถึง การรวมศูนย์การตรวจสอบไว้ที่ ExecuteRecord หมายความว่าตัวจัดการ VEMR* แต่ละตัวไม่จำเป็นต้องป้องกันตัวเองจากการถูกเรียกผิดเวลาอีกต่อไป gate เป็นผู้ตัดสินใจเรื่องนั้นเพียงครั้งเดียวก่อนการ dispatch แทนที่จะต้องตัดสินใจแปดสิบครั้งหลังจากนั้น
การวางกราฟเวกเตอร์ไว้ข้างข้อความและภาพในหน้าเดียวกัน
หน้ารายงานแทบไม่เคยมีแค่กราฟอย่างเดียว และ ShowMetafile ประกอบเข้ากับ page operator อื่นๆ ของ HotPDF ได้เหมือนการเรียกวาดภาพอื่นใดทุกประการ หัวเรื่องที่วาดด้วย TextOut กราฟแท่งที่เติมด้วย hatch ซึ่งนำเข้าเป็น EMF และโลโก้ที่วางด้วย ShowImage ล้วนสามารถลงเอยบนหน้าเดียวกันใน content stream เดียวกันได้ โดยแต่ละอย่างยังคงความเที่ยงตรงแบบเนทีฟของตัวเอง ซึ่งเป็นรูปแบบการจัดวางที่ครอบคลุมในคู่มือของ HotPDF เรื่องการจัดวางข้อความ ฟอนต์ และภาพในรายงาน
Pdf.CurrentPage.SetFont('Arial', [fsBold], 14);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Q2 Regional Sales');
Pdf.CurrentPage.ShowMetafile(RegionChart); // hatch-filled bars, still vector
Pdf.CurrentPage.ShowImage(LogoIndex, 450, 760, 90, 30, 0);
ตัวแปลผล EMF และ WMF, axial shading pattern ที่มันลงทะเบียนไว้สำหรับ gradient fill และการแม็ป tiling pattern สำหรับ hatch brush ที่อธิบายในบทความนี้ ล้วนมาพร้อมกับHotPDF Componentรุ่นมาตรฐานสำหรับ Delphi และ C++Builder ซึ่งเป็นไลบรารี VCL แบบเนทีฟที่ไม่ต้องพึ่งพา DLL ภายนอกใดๆ เลยสำหรับส่วนนี้