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

นำเข้าเวกเตอร์ EMF บน Free Pascal ด้วย PDFlibPas

PDFlibPas แปลง enhanced metafile เป็นเนื้อหาหน้า PDF จริง ๆ ทีละ record แทนที่จะแปลงเป็นภาพ raster ซึ่งเป็นสิ่งที่ทำให้แผนภูมิหรือภาพวาด CAD ที่นำเข้ามาคมชัดทุกระดับซูม ตัวแปลงนี้มีขนาดราว 6500 บรรทัดและถูกเขียนขึ้นบนพื้นฐานของ VCL เมื่อไลบรารีได้เป้าหมาย Free Pascal มันจึงถูกจัดประเภทว่าพอร์ตไม่ได้และถูก stub ออกไป การจัดประเภทนั้นผิด และความผิดแบบที่เป็นอยู่เป็นบทเรียนที่ใช้ประโยชน์ได้จริงเรื่องวิธี audit dependency ก่อนตัดสินใจว่าจะเขียนใหม่รอบมัน

พื้นผิว VCL ที่แท้จริงของ 6500 บรรทัดเหล่านั้นปรากฏว่าเล็กมาก: คลาส bitmap หนึ่งตัวที่ใช้เพื่อ pixel format, การบันทึก stream, handle, canvas และ scanlines คลาส metafile หนึ่งตัวที่ใช้เพื่อ width, height และ handle และชนิดสีที่มาพร้อมค่าคงที่สองตัว ทุกอย่างเหล่านี้มีหน่วยกราฟิกของไลบรารีเองจัดเตรียมไว้ให้แล้วทั้งหมด ซึ่งหน่วยนั้นก็มีอยู่เพื่อให้บิลด์ที่ไม่มี VCL มีสิ่งทดแทนเสียอยู่แล้ว ตัวแปลงไม่เคยติดที่ VCL เลยแม้แต่นิดเดียว มันติดที่ยูนิต Windows ของ Free Pascal ต่างหาก

แบ่งตามแกนที่โค้ดพึ่งพาจริง ๆ

การเปลี่ยนแปลงจึงไม่ใช่การ reimplement มันคือเงื่อนไขเดียว: เปลี่ยนจาก "compile stub เมื่อบิลด์โดยไม่มี VCL" เป็น "compile stub เมื่อบิลด์ไม่ใช่สำหรับ Windows" นั่นคือแกนที่ถูกต้อง และการพูดเหตุผลออกมาทำให้ความต่างเห็นชัด enhanced metafile เป็นคอนเทนเนอร์ของ Windows ตัวแปลงเป็น parser สำหรับ record ของ Windows GDI ตั้งแต่หัวจรดเท้า ว่าแอปพลิเคชันโฮสต์ใช้ VCL หรือชุด widget อื่น หรือไม่ใช้ widget เลยนั้นไม่เกี่ยวกับความสามารถในการตีความ record เหล่านั้น แต่ว่าเป้าหมายเป็น Windows หรือไม่ต่างหากที่เกี่ยวโดยตรง

ผลที่ตามมาจากการเลือกแกนที่ถูกได้มาฟรี ๆ บิลด์ C++Builder ซึ่ง undefine สัญลักษณ์ platform Windows ไว้ในไลบรารีนี้ ยังคงใช้ stub แบบ throw และทำงานเหมือนเดิมทุกประการ macOS ยังคงใช้ stub ซึ่งถูกต้องแล้วเพราะไม่มี record GDI ให้ parse อยู่จริง บิลด์ Delphi VCL ไม่ถูกแตะต้อง และบิลด์ Windows ที่ใช้ widget set อื่นกลับได้การนำเข้าเวกเตอร์ EMF มาโดยปริยาย โดยที่ไม่มีใครต้อง implement มันเลย เงื่อนไขที่ตรงกับ dependency จริงเปลี่ยนงานด้าน platform ให้กลายเป็นการแก้บรรทัดเดียว ส่วนเงื่อนไขที่ผูกกับแกนที่ผิดเปลี่ยนมันให้กลายเป็นการเขียนใหม่ที่ไม่มีวันถูกจัดคิวลงแผน

เงื่อนไขการนำเข้า EMF ถูกจัดแกนใหม่จากการเป็นบิลด์ VCL ไปสู่ platform Windows โดยเก็บ stub ไว้ที่อื่นและให้บิลด์ Windows ที่ไม่มี VCL ได้การนำเข้าเวกเตอร์
การจัดแกนเงื่อนไข stub ใหม่ไปที่ platform Windows รักษาพฤติกรรมบิลด์เดิมทุกแบบ และให้เป้าหมาย Windows ที่ไม่มี VCL ได้การนำเข้าเวกเตอร์ EMF มาโดยปริยาย

ช่องว่างของ Free Pascal คือการประกาศ ไม่ใช่ตรรกะ

สิ่งที่หายไปจริง ๆ คือการประกาศ Win32 ที่ยูนิต Windows ของ Delphi มีให้แต่ยูนิตของ Free Pascal ไม่มี การรวบรวมพวกมันเข้ายูนิต compatibility เดียวแทนการกระจายเงื่อนไขไปทั่วตัวแปลงทำให้ parser ยังอ่านได้ราบรื่น รายการนี้น่าศึกษาเพราะมันแสดงให้เห็นว่าความครอบคลุมของ header ระหว่าง RTL ทั้งสองเป็นแบบไม่สม่ำเสมอแค่ไหน: ค่าคงที่ชนิด record ของ metafile 113 ตัว, flag ของ text-output แบบ extended สองตัว, ค่าคงที่ gradient fill mode สามตัว, ชนิด pointer ของ handle-table, alias สำหรับ record gradient vertex และ primitive และอีกสามชนิด record ที่ Free Pascal ไม่ประกาศให้เลย ครอบคลุมการ blend แบบ alpha, การ blit แบบโปร่งใส และโหมดจัดการสี

รายการเหล่านี้ไม่มีอะไรน่าตื่นเต้นเป็นรายตัว แต่ทุกตัวต้องถูกต้องก่อน parser จะ compile ผ่าน และยูนิต compatibility เป็นที่พักที่เหมาะสมที่สุด เพราะมันถูก diff กับเอกสาร header ได้ในฐานะหน่วยเดียวทั้งก้อน

การประกาศ Win32 ที่ยูนิต Windows ของ Free Pascal ไม่มี ถูกรวบรวมเข้ายูนิต compatibility เดียวสำหรับตัวแปลงเวกเตอร์ EMF เป็น PDF
ค่าคงที่ record, flag, alias และอีกสามชนิด record ที่หายไปอยู่รวมกันในยูนิต compatibility เดียวที่ diff กับเอกสาร header ได้ทั้งก้อน

ตัวที่วาดภาพผิดแบบเงียบ ๆ

สองรายการในการประกาศเหล่านั้นไม่ได้แค่หายไป แต่มีอยู่จริงและผิดสำหรับการใช้งานนี้ และนี่คือส่วนที่สมควรจดจำแม้คุณจะไม่เคยแตะ metafile เลยก็ตาม

Free Pascal ประกาศ record การสร้าง brush โดยฝังโครงสร้าง brush ระดับ run-time เอาไว้ข้างใน และประกาศ record ปากกาแบบ extended โดยฝังโครงสร้างปากกา run-time เช่นกัน ทั้งสองโครงสร้าง run-time ประกาศ member ที่ชื่อ hatch เป็นจำนวนเต็มขนาด pointer เพราะในการเรียก GDI ที่ยังมีชีวิต member ตัวนี้อาจแบก handle มาด้วย แต่ metafile เก็บรูป 32 บิตไว้เสมอ เพราะ layout ของ record เป็นส่วนหนึ่งของรูปแบบไฟล์แบบ serialised และไม่เปลี่ยนตาม bitness ของ process

บนบิลด์ 32 บิตสองรูปแบบนี้เห็นพ้องกันและไม่มีอะไรเกิดขึ้น บน Win64 member ขนาด pointer กินแปดไบต์ที่ที่ไฟล์มีสี่ไบต์ ทุก field ที่อยู่หลัง member hatch จึงถูกอ่านจาก offset ที่ผิด ไม่มี exception ไม่มี parse error ไม่มีคำเตือนใด ๆ metafile เพียงแค่แสดงผลผิด: สีมาจากไบต์ที่ผิด ความกว้างปากกามาจากไบต์ที่ผิด และภาพที่ได้ดูเหมือนบั๊กการแสดงผลมากกว่าบั๊ก layout ของ struct Delphi จัดเตรียมตัวแปรของสองโครงสร้างนี้ที่ระบุความกว้าง 32 บิตชัดเจนไว้เพื่อเหตุผลนี้โดยเฉพาะ และยูนิต compatibility ก็ประกาศซ้ำในแนวทางเดียวกัน

layout ไบต์ของ record brush ใน EMF แสดง field hatch ขนาด pointer ที่ดัน field ถัดไปเลื่อนไปสี่ไบต์บน Win64 เทียบกับ layout 32 บิตแบบตายตัว
record แบบ serialised เก็บ hatch ขนาด 4 ไบต์เสมอ โครงสร้าง run-time ขนาด pointer จึงอ่าน field ทุกตัวถัดไปผิดอย่างเงียบ ๆ บน Win64
// ผิดบน Win64: Hatch ขนาดเท่า pointer ขณะที่ไฟล์เก็บ 32 บิต
// และทุก field ถัดไปเลื่อนไปสี่ไบต์โดยไม่มี error ใด ๆ
type
  TLogBrushRuntime = record
    lbStyle: UINT;
    lbColor: COLORREF;
    lbHatch: ULONG_PTR;      // 8 ไบต์ใน process 64 บิต
  end;

// ถูกต้อง: layout แบบ serialised ความกว้างตายตัวไม่ขึ้นกับ bitness
type
  TLogBrush32 = record
    lbStyle: UINT;
    lbColor: COLORREF;
    lbHatch: DWORD;          // 4 ไบต์เสมอ ตามที่เก็บใน metafile
  end;

กฎทั่วไปก็คือ: โครงสร้างใด ๆ ที่ปรากฏทั้งเป็นอาร์กิวเมนต์ API ระดับ run-time และเป็น layout field แบบ serialised ต้องมีสองการประกาศ โดยตัวที่ serialised ต้องใช้ชนิดความกว้างคงที่ตลอดทั้งโครง การใส่ member ขนาด pointer ลงในรูปแบบไฟล์คือบั๊กที่รอ build แบบ 64 บิตอยู่เสมอ

ความต่างของ signature ให้ซ่อนไว้ใน wrapper ไม่ใช่ที่ทุก call site

ความต่างที่เหลือเป็นการลงลายมือชื่อที่ไม่ตรงกันธรรมดา และวิธีรับมือคือ wrapper แบบส่งต่อ แทนการเขียนเงื่อนไขที่ call site แต่ละจุด ฟังก์ชันรวม transform รับ pointer ภายใต้ Free Pascal ที่ที่ Delphi รับพารามิเตอร์แบบ reference ตัว wrapper จึงรับแบบ reference แล้วส่ง address ต่อไป มันยัง copy อาร์กิวเมนต์ต้นทางทั้งสองเข้าตัวแปร local ก่อนด้วย เพราะตัวแปลงมี call site ที่ matrix ปลายทางเป็นตัวแปรเดียวกับต้นทางพร้อมกัน และการส่ง address เดียวกันสองครั้งให้ฟังก์ชันที่เขียนไปพร้อมอ่านจะให้ transform ที่ผิดแบบละเอียดอ่อน ซึ่งปรากฏอาการเฉพาะเมื่อเนื้อหาถูกหมุนเท่านั้น

function CombineTransformCompat(var Dest: TXForm;
  const A, B: TXForm): BOOL;
var
  SrcA, SrcB: TXForm;
begin
  // copy ก่อน: caller ส่ง Dest เป็น A หรือ B ได้อย่างถูกต้องตามสิทธิ์
  SrcA := A;
  SrcB := B;
{$IFDEF FPC}
  Result := Windows.CombineTransform(@Dest, @SrcA, @SrcB);
{$ELSE}
  Result := Windows.CombineTransform(Dest, SrcA, SrcB);
{$ENDIF}
end;

ชนิด rectangle และ point เป็นอีกกรณีหนึ่ง Free Pascal มอง record rectangle และ point ของ metafile เป็นชนิดต่างหากจากชนิดกราฟิกทั่วไป ทำให้ต้องมีการ cast ชัดเจนระหว่าง record ที่ layout เหมือนกันเปล่า ๆ ถึงแปดจุด ทั้งสองคอมไพเลอร์ยอมรับรูป cast นี้ จุดเหล่านั้นจึงไม่ต้องพกเงื่อนไขใด ๆ เลย ซึ่งสมควรแลกกับความไม่สวยสักหน่อย

สิ่งที่เปลี่ยนไปสำหรับการ deploy บน Free Pascal

การนำเข้าเวกเตอร์ EMF ทำงานได้บน Windows ภายใต้ Free Pascal โดยให้เนื้อหาหน้าเหมือนกับบิลด์ Delphi: path คือ path, gradient คือเนื้อหาแบบ pattern, ข้อความคือข้อความ นอก Windows เส้นทาง raster ยังเป็นคำตอบ และนั่นคือข้อจำกัดของรูปแบบไฟล์ ไม่ใช่ของการพอร์ต สถานะพิกัดและการคลิปที่ตัวแปลงส่งเข้าไปอธิบายไว้ในบทความ content stream CTM และการติดตาม clipping และ primitive เวกเตอร์ที่มันปล่อยออกมาอยู่ในกราฟิกเวกเตอร์, shader และ gradient

ถ้ากำลัง audit codebase ของตัวเองหาโอกาสแบบเดียวกันนี้ แบบฝึกหัดที่ใช้ได้ผลคือแบบที่จุดเริ่มของเรื่องนี้: จดสมาชิกที่คุณใช้จริง ๆ จาก framework ที่คิดว่าตัวเองพึ่งพาอยู่ คำตอบมักสั้นกว่าที่รายการ import บ่งบอกอยู่มาก และข้อจำกัดจริงมักอยู่ที่อื่นสิ้นเชิง เส้นทางการนำเข้าที่อิง device context โดยทั่วไปอธิบายไว้ในบทความ print preview และ device context และความครอบคลุมด้าน platform และ toolchain ระบุไว้บนหน้าผลิตภัณฑ์losLab PDF Developer Library