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

HotPDF บน Free Pascal: Deflate, AES และขีดจำกัด codec

HotPDF compile และรันได้ภายใต้ Free Pascal 3.2.2 ร่วมกับ Lazarus และสรุปการพอร์ตนี้อย่างตรงไปตรงมาได้ในสองประโยค การสร้างเอกสาร การโหลด การบันทึก การบีบอัด การคลายการบีบอัด การเข้ารหัสและการถอดรหัสล้วนทำงานบน backend ภาษา Pascal ล้วน ๆ แอปพลิเคชัน Lazarus จึงผลิตและอ่าน PDF จริงได้โดยไม่มี dependency ภาษา C ส่วน image codec แบบ native เสริมไม่ทำงาน เพราะออบเจกต์ Win64 ที่ build ไว้ใช้รูปแบบ COFF ที่ linker ของ Free Pascal ทั้งสองตัวกินไม่ได้ บน toolchain นั้น entry point เหล่านั้นจึง resolve ไปเป็น stub ที่ล้มเหลวแบบ fail closed

แผนที่ความสามารถของ HotPDF บน Free Pascal: backend deflate, AES และเอกสารภาษา Pascal ที่ทำงานได้ เคียงข้าง stub ของ image codec ที่ล้มเหลวแบบ fail closed
ฟีเจอร์เอกสาร การบีบอัด และการเข้ารหัสรันบน backend ภาษา Pascal ล้วน ขณะที่ image codec แบบ native resolve ไปเป็น stub ที่ล้มเหลวแบบ fail closed

การเดินจากจุด "compile ผ่าน" ไปถึงจุด "ใช้งานได้" ต้องใช้ชุดทางแก้เจาะจง และทุกข้อล้วนเป็นกับดักที่จะตามหา codebase Delphi อื่น ๆ ที่กำลังย้ายไป Free Pascal เช่นกัน ข้อเหล่านี้สมควรจดไว้ตามลำดับที่มันเจ็บ

ทำไมยูนิตที่ compile ผ่านจึงไม่พิสูจน์อะไร

เพราะยูนิต Pascal สามารถอ้างถึงสัญลักษณ์ที่จะไม่ทำอะไรมีประโยชน์เลย ทั้งที่ยังทำให้คอมไพเลอร์พอใจ ณ จุดที่ยูนิตไลบรารีทั้ง 113 ตัว build ผ่านสะอาดภายใต้ Free Pascal handler ของคอนเทนเนอร์ archive ทำงานได้จริง ตรวจสอบด้วย smoke test ที่เปิด CBZ แล้วแปลงเป็น PDF ส่วน XFA form flattening ไม่ทำงานเลย เพราะการ flatten ต้อง inflate สตรีม packet /XFA ที่ถูกบีบอัด และ deflate entry point ยังเป็น stub อยู่ ไม่มีอะไรในผลลัพธ์ build แยกสองกรณีนี้ออกจากกัน

กฎที่ได้มาสั้น ๆ ก่อนเขียนลง release note ว่าฟีเจอร์ใดใช้ได้บน toolchain ใหม่ ให้เขียน runtime probe ที่ใช้ฟีเจอร์นั้นตั้งแต่ต้นจนจบบน toolchain นั้นเสียก่อน ความครอบคลุมระดับ compile เป็นเงื่อนไขเบื้องต้น ไม่ใช่หลักฐาน ภาพรวมว่าการพอร์ตครอบคลุมอะไรอยู่ในบันทึกการรองรับ Free Pascal และ Lazarus Win64

raise ที่อยู่ใน stub แบบ cdecl ไม่ไปถึงผู้เรียก

ข้อนี้สมควรได้หัวข้อเป็นของตัวเอง เพราะอาการหลอกลวงมาก ยูนิต stub เปิด entry point ภาษา C เสมือนไลบรารี static หนึ่งตัว stub จึงมีหน้าตาแบบนี้

// ดูสมเหตุสมผล แต่ไม่ใช่
function inflate(Strm: Pointer; Flush: Integer): PtrUInt; cdecl;
  public name 'inflate';
begin
  raise ENotSupportedException.Create('codec unavailable');
end;

บน Free Pascal สำหรับ Win64 exception นั้นไม่เดินทางไปถึงผู้เรียก ไม่มี handler แบบ try..except ตัวใดเห็นมัน เพราะการ unwind ข้ามขอบเขต cdecl ที่ประกาศแบบนี้ไม่ได้พา frame exception ของ Pascal ไปด้วย process จบด้วย exit code 217 จากฝั่งแอปพลิเคชันไม่มี error ไม่มีข้อความ ไม่มีบรรทัด log เหลือเพียงโปรแกรมที่หายไปเฉย ๆ ซึ่งเลวร้ายกว่าคำตอบที่ผิดอย่างมาก เพราะคำตอบที่ผิดยังพอจัดการได้

เหตุใด exception ที่ raise ใน stub แบบ cdecl จึงจบ process ของ Free Pascal ด้วย exit code 217 และการ gate ที่ entry point ภาษา Pascal แก้ปัญหาอย่างไร
frame exception ของ Pascal ไม่สามารถ unwind ข้ามขอบเขต cdecl ได้ process จึงตายอย่างเงียบ ๆ ทางแก้คือ gate ก่อนที่ stub จะถูกแตะ

ทางแก้ที่น่าล่อคือให้ stub คืนรหัสความล้มเหลวแทน ซึ่งสำหรับ inflate ถูกแล้ว เพราะ zlib มีรูปแบบค่าคืนกลับ error ที่ชัดเจน แต่เป็นทางแก้ที่ผิดในกรณีทั่วไป: stub ของ jpeg_read_header ที่คืนศูนย์คือการบอกผู้เรียกให้ดำเนินต่อกับโครงสร้างที่ไม่มีใครกำหนดค่าเริ่มต้น ทางแก้ที่ทนทานคือ gate ที่ entry point ภาษา Pascal แทนที่จะอยู่ข้างใน stub ที่มีรูปร่างแบบ C โดยใช้ธรรมเนียมการล้มเหลวที่ API นั้นมีอยู่แล้ว

function TryDecodeJPEG(const Data: TBytes; out Bitmap: TBitmap): Boolean;
begin
{$IFDEF FPC}
  // ปฏิเสธก่อนที่ stub จะถูกแตะ โดยใช้ธรรมเนียมความล้มเหลวของ API นี้
  // แทนการปล่อย exception ข้าม cdecl
  Bitmap := nil;
  Result := False;
  Exit;
{$ENDIF}
  Result := DecodeJPEGNative(Data, Bitmap);
end;

paszlib ไม่ใช่ zlib และความต่างกินพื้นที่ถึงสองคลาสเอกสาร

implementation deflate ภาษา Pascal ที่มีบน Free Pascal รองรับการครอบกรอบสองแบบ: wrapper zlib และ raw deflate มันไม่รองรับการครอบแบบ gzip ซึ่ง zlib เลือกผ่านค่า windowBits ช่วง 16 ถึง 31 และไม่รองรับโหมดตรวจจับอัตโนมัติที่ค่า 32 ถึง 47 เลือก HotPDF ต้องใช้ทั้งสองอย่าง เส้นทางนำเข้า SVG ที่ปลอดภัยร้องขอค่า 31 และตัวโหลดมีบันได fallback ที่ร้องขอค่า 47 เมื่อการครอบกรอบของสตรีมไม่ชัดเจน ข้ามข้อใดข้อหนึ่งไปแล้วตระกูลเอกสารทั้งตระกูลจะเปิดไม่ได้ พร้อม decode error ที่ชี้ไปที่สตรีมแทนที่จะชี้ไปที่การครอบกรอบที่หายไป

ความครอบคลุม windowBits ของ paszlib เทียบ zlib: ช่วงการครอบกรอบ gzip และ auto-detect ที่หายไปสำหรับการนำเข้า SVG และ fallback ของตัวโหลด HotPDF
paszlib รองรับ wrapper zlib และ raw deflate แต่ HotPDF ยังต้องใช้ windowBits 31 และ 47 ชั้น shim จึงต้องจัดการการครอบกรอบ gzip เอง

มีความไม่เข้ากันอีกชั้นที่คมกว่า record z_stream ที่ paszlib ประกาศไม่มี layout หน่วยความจำเหมือนตัวภาษา C: field msg เป็น short string แทนที่จะเป็น pointer และ total_in กับ total_out เป็น 64 บิต ขณะที่ C ABI ใช้ machine word ทั้งสอง บันทึกของผู้เรียกจึงส่งผ่านตรง ๆ ไม่ได้ แนวจัดการที่ใช้ได้คือเก็บสถานะ paszlib ไว้หลัง pointer state ที่ record สาธารณะกันพื้นที่ไว้แล้ว และ copy field สาธารณะเข้าออกรอบการเรียกทุกครั้ง CRC ของ gzip และ trailer ความยาวแปดไบต์ถูกจัดการอยู่ในชั้น shim เดียวกัน ซึ่งเป็นที่ที่เหมาะสมที่สุดสำหรับพวกมัน เพราะชั้นนั้นเป็นเจ้าของการตัดสินใจเรื่องการครอบกรอบอยู่แล้ว

ส่ง dynamic array เข้าพารามิเตอร์ var แบบ untyped

นี่คือบั๊กที่มีโอกาสสูงสุดที่จะนั่งรออยู่ในโค้ดคุณอยู่ตอนนี้ เมื่อคุณส่ง dynamic array เข้าพารามิเตอร์ var แบบ untyped สิ่งที่ปลายทางได้รับคือ address ของตัวแปรอาร์เรย์ ซึ่งคือ address ของ pointer ไม่ใช่ address ของ payload การอ่านลงไปที่นั้นจึงเขียนทับตัวแปรเองและทุกอย่างที่นั่งอยู่ข้าง ๆ

var
  FBuffer: TBytes;
begin
  SetLength(FBuffer, 65536);

  // ผิด: ส่ง address ของตัวแปร FBuffer ออกไป
  FStream.Read(FBuffer, Length(FBuffer));

  // ถูกต้อง: ส่ง address ของไบต์ payload ตัวแรกออกไป
  FStream.Read(FBuffer[0], Length(FBuffer));
end;

บน Delphi รูปที่ผิดมักปรากฏว่าใช้ได้ เพราะสิ่งที่มันทำให้เสียหายคือช่อง stack ข้างเคียงที่ไม่มีใครอ่านต่อ บน Free Pascal บรรทัดเดียวกัน segmentation fault ที่การใช้ครั้งแรก สิ่งที่ทำให้มันจับตาเห็นยากคือ static array ไม่มีปัญหานี้ เพราะตัวแปร static array คือ payload ของมันเอง ทั้งสองรูปจึงถูกต้องได้ในไฟล์เดียวกันขึ้นกับการประกาศที่อยู่ห่างออกไปสองสามร้อยบรรทัด

คอนเทนเนอร์ ZIP โดยไม่มี System.Zip

Free Pascal ไม่มีสิ่งเทียบเท่ากับยูนิต zip ของ RTL และทางเลือกที่มีมีทั้ง API คนละผิวและไม่รองรับการเข้ารหัสแบบดั้งเดิมที่รูปแบบคอนเทนเนอร์รุ่นเก่ายังใช้อยู่ ตัวอ่านขนาดเล็กในไลบรารีจึงสั้นกว่าการปรับให้เข้ากับมัน มีรายละเอียดของรูปแบบไฟล์สองข้อที่กินเวลาและทำผิดง่าย

ข้อแรกคือไบต์ตรวจสอบของ encryption header ไบต์ที่สิบสองของมันตามปกติคือไบต์บนของ CRC แต่เมื่อบิต 3 ของ general-purpose flag ถูกตั้ง ซึ่งหมายความว่าขนาดอยู่ใน data descriptor ที่แนบท้ายและ CRC ยังไม่เป็นที่รู้จัก ไบต์ตรวจสอบจะมาจากไบต์บนของเวลาแก้ไขแทน ใช้รูป CRC เพียงอย่างเดียวแล้ว archive ทุกไฟล์ที่เขียนในโหมด streaming จะปฏิเสธรหัสผ่านที่ถูกต้อง ข้อที่สองคือ extra field แบบ ZIP64: field 64 บิตทั้งสามของมันปรากฏตามลำดับคงที่ แต่จะถูกเขียนก็ต่อเมื่อ field 32 บิตที่สอดคล้องกันเต็มขีดเท่านั้น การอ่านที่ offset คงที่จึงใช้ได้กับ archive ที่คุณทดสอบแล้วพังกับไฟล์ถัดไป ให้ parse ตามตำแหน่งโดยดูว่า field 32 บิตตัวใดเต็มขีด

ของสะดวกหนึ่งอย่างที่ควรรู้: สตรีมคลายการบีบอัดของ Free Pascal รับอาร์กิวเมนต์ constructor ตัวที่สองเพื่อข้าม header zlib ซึ่งเป็นสิ่งที่ entry ของ ZIP ต้องการพอดี เพราะมันเก็บ raw deflate เส้นทางนี้ไม่แตะ zlib shim ของไลบรารีเลย จึงไม่ได้รับผลจาก backend C ที่หายไป

ความโปร่งใสของ glyph สีภายใต้ LCL

การอ่านช่อง alpha ของ glyph สีที่ถูกแรสเตอร์เป็นรายละเอียดกราฟิกเดียวที่ไม่มีการแปลตรง ๆ คลาส PNG ของ LCL ไม่มี accessor ระดับ scanline ที่เปิดเผย alpha และการ assign PNG เข้า bitmap จะทิ้งมันไป emoji สีจึงมาถึงในสภาพทึบแสงเต็มตัวและถูก composite พร้อมกล่องดำด้านหลัง เส้นทางที่ใช้ได้คือภาพระดับ interface: สร้างมันจาก PNG แล้วอ่านพิกเซลผ่าน accessor แบบสี โดยจำไว้ว่าส่วนประกอบของมันกว้าง 16 บิตและต้องเลื่อนลงแปดบิตจึงกลายเป็นไบต์ พื้นผิวนี้ยังใช้ลำดับแถวแบบ top-down ตามธรรมชาติ การกลับด้าน Height - 1 - Y ที่โค้ด scanline ของ VCL ต้องการจึงต้องถูกถอดออก ไม่ใช่พอร์ตตามไป

บันทึกระบบ build สองข้อก่อนคุณรายงานบั๊ก

การ rebuild แบบเต็มบางครั้งล้มด้วย undefined symbol ที่ชื่อลงท้ายด้วย suffix $crc ต่อด้วยค่าเลขฐานสิบหก suffix นั้นคำนวณจากชนิดพารามิเตอร์ และมันไม่ตรงกันเมื่อบิลด์หนึ่งคอมไพล์ยูนิตเดียวกันกับ interface สองเวอร์ชันในรอบเดียว การรัน build ซ้ำเคลียร์มันได้ signature ไม่ได้ผิด

ข้อที่สอง Free Pascal 3.2.2 ไม่มี anonymous methods ทุกจุดที่ไลบรารีใช้ closure ต่อสาย pipeline ขนาน บิลด์ Free Pascal จะใช้ fallback แบบ serial ที่ deterministic แทน ผลลัพธ์เหมือนกันทุกประการ แต่ throughput ไม่เหมือน ถ้าคุณพึ่งพาการ render หน้าแบบขนาน นั่นเป็นเหตุผลให้ยังอยู่กับ Delphi ชั่วขณะ และการออกแบบ pipeline อธิบายไว้ในบทความ parallel render pipeline สถานการณ์ image codec เป็นอีกจุดที่การเลือก toolchain เปลี่ยนความสามารถ ไม่ใช่แค่ความเร็ว การ deploy บน Lazarus จึงควรวางแผนเรื่องรูปแบบภาพไว้ให้เหมาะ ตารางต่อ toolchain ปัจจุบันอยู่บนหน้าผลิตภัณฑ์HotPDF Delphi PDF component