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

HotPDF บน Free Pascal และ Lazarus: เพดานการรองรับ Win64

คำตอบสั้น ๆ สำหรับตั๋วสนับสนุนฉบับนั้นคือ ได้ แต่มีเงื่อนไข HotPDF 2.730.0 บิลด์บน Free Pascal 3.2.2 และ Lazarus 4.6 สำหรับ Win64 ได้ และเส้นทางหลักของ create, load และ save ทำงานได้ สิ่งที่ไม่ตามมาคือทุกอย่างที่พาดพิงไปถึงออบเจกต์ codec เนทีฟแบบลิงก์คงที่หรือ anonymous method ของ Delphi

คำถามมักมาถึงในรูปแบบเดียวกัน: ทีมหนึ่งมาตรฐานลง Lazarus เพื่อเครื่องมือข้ามแพลตฟอร์ม หรือรับช่วงโค้ดเบส Free Pascal และอยากได้คอมโพเนนต์ PDF ตัวเดิมที่ลิขสิทธิ์ไว้กับ Delphi การพอร์ตไลบรารี Delphi ที่โตมาแล้วไม่ค่อยได้เป็นแค่เรื่องไวยากรณ์ ส่วนที่น่าสนใจคือการพอร์ตเผยให้เห็นว่าไลบรารีถูกผูกกับ toolchain เดียวไว้ตรงไหนอย่างเงียบ ๆ และในกรณีนี้การผูกอยู่สองจุดเจาะจงมาก: ABI ของไฟล์ออบเจกต์ของ codec ที่แถมมาด้วย และฟีเจอร์คอมไพเลอร์ที่ซ่อนอยู่หลังสัญลักษณ์เวอร์ชัน

ตารางความสามารถเทียบบิลด์ Delphi ของ HotPDF กับบิลด์ Free Pascal 3.2.2 และ Lazarus 4.6 บน Win64 แสดงว่าเส้นทางเอกสารส่วนใดแบ่งกันใช้ และ API ด้าน codec, การบีบอัด, การเรนเดอร์ขนาน และ anonymous method ส่วนใดไปจบที่ stub ที่ raise
เส้นทางหลัก create, load และ save เหมือนกันทั้งสองบิลด์ ช่องว่างทั้งหมดอยู่ที่ codec แบบลิงก์คงที่และ API ของ anonymous method เท่านั้น

Free Pascal 3.2.2 ต้องการอะไรก่อน HPDFDoc จะคอมไพล์ผ่าน

HotPDF คอมไพล์ภายใต้ Free Pascal ได้ในโหมด Delphi เท่านั้น และต่อเมื่อไดเรกทอรียูนิต LCL ของ Lazarus อยู่บน search path ด้วย ทั้งสองเงื่อนไขไม่มีทางต่อรอง HotPDF.inc สลับคอมไพเลอร์ด้วย {$MODE DELPHI} กับ {$H+} ภายในบล็อก {$IFDEF FPC} และปฏิเสธทุกอย่างที่เก่ากว่าด้วย {$FATAL} เมื่อ FPC_FULLVERSION ต่ำกว่า 30202 การติดตั้ง 3.0.x จึงล้มเหลวแบบดัง ๆ ไม่ใช่ผลิตยูนิตที่พังออกมาเงียบ ๆ แพ็กเกจรันไทม์ Lazarus HotPDFLaz.lpk บันทึกส่วนที่เหลือเอาไว้: LCL เป็น required package และ -Mdelphi เป็น custom option

เงื่อนไข LCL ทำให้คนที่แค่อยากได้เอาต์พุต console แปลกใจ แต่มันเป็นเรื่องโครงสร้าง HPDFFPCCompat ส่งมอบชนิด VCL ของ Delphi ที่ Free Pascal ไม่มีสิ่งใดเทียบเท่า โดยแมป TMetafile กับ TMetafileCanvas ลงบนคลาส bitmap กับ canvas ของ LCL และตั้ง alias TRichEdit ให้เป็น TMemo ขณะที่ HPDFDoc ตั้ง alias TPNGObject ให้เป็น Graphics.TPortableNetworkGraphic จงถือว่าพวกนี้เป็น shim ตอนคอมไพล์ ไม่ใช่ความเท่าเทียมของฟีเจอร์: คลาส metafile ที่พากันด้วย bitmap ทำให้ยูนิตยังคอมไพล์ผ่าน ไม่ได้ทำให้เส้นทาง metafile ทำงานเหมือนบน Delphi แม้แต่ smoke test ที่ไม่แตะ GUI ยังต้องดึง Interfaces เข้ามา และ build script ส่ง -Fu สำหรับ lcl\units\x86_64-win64 กับไดเรกทอรีเอาต์พุตของ lazutils

ทำไม D2009+ จึงมาทำหน้าที่เป็นประตูเวอร์ชันไม่ได้

มันน่าหลงใหลที่จะถือว่าบิลด์ Free Pascal เป็นคอมไพเลอร์ยุคใหม่แล้วก็ define สัญลักษณ์ฟีเจอร์ Delphi ล่าสุดให้เลย HotPDF ไม่ทำแบบนั้น และเหตุผลควรพูดกันตรง ๆ: D2009+ ไม่ได้แปลว่า Unicode string เท่านั้น มันยังเป็นประตูของยูนิตที่ public API ถูกเขียนด้วย anonymous method อีกด้วย Free Pascal 3.2.2 ไม่รองรับทั้ง anonymous method ของ Delphi และ API กลุ่มนั้น การยืมสัญลักษณ์จึงลากโค้ดที่คอมไพล์ไม่ได้เข้ามาโดยไม่จำเป็น uses clause ของ HPDFDoc จึงแบก tail เงื่อนไขแยกสองชุด และส่วนที่ซ้อนกันระหว่างสองชุดนั้นเกิดจากการออกแบบ ไม่ใช่ความบังเอิญ

uses
  // ...
  HPDFJavaScript,
  HPDFFormCalcGraph
{$IFDEF FPC}
  , HPDFFPCCodecStubs,
  HPDFCMS,
  HPDFWinCertSigner
{$ENDIF}
{$IFDEF D2009+}
  , HPDFXFARuntime,
  HPDFCMS,
  HPDFWinCertSigner,
  HPDFSignVerify,
  HPDFSignatureBatch
{$ENDIF};

ทำไม codec เนทีฟจึงไปไม่ผ่านตัว linker

เพราะมันคือออบเจกต์ Win64 COFF ที่ถูกปล่อยออกมาจาก toolchain ตัวใดตัวใดตัวหนึ่ง และ linker ของ Free Pascal บน Win64 ทั้งสองแบบไม่มีตัวไหนยอมกินมัน: ไม่ว่าจะ internal linker หรือเส้นทาง external GNU ld นี่คือปัญหา ABI ระดับไฟล์ออบเจกต์ ไม่ใช่ปัญหาภาษา Pascal และเงื่อนไขซอร์สมากแค่ไหนก็แก้ไม่ได้ ไลบรารีจึงเลือกทางที่ซื่อสัตย์ที่สุดที่ยังเหลืออยู่ ทุก directive {$L} ที่ดึงออบเจกต์ codec แบบ static เข้ามาถูกห่อด้วย {$IFNDEF FPC} บิลด์ Free Pascal จึงเพียงละทิ้งพวกมัน แล้ว HPDFFPCCodecStubs ก็ส่งมอบสัญลักษณ์ external ที่หายไปแต่ละตัวด้วย stub ที่ raise แทนการคืนค่า

// HPDFFPCCodecStubs.pas
function HPDFFPCNativeCodecUnavailable: PtrUInt;
begin
  raise ENotSupportedException.Create(
    'This native codec is not available in the Free Pascal build');
end;

function HPDFFPCStub_deflate: PtrUInt; cdecl;
  public name 'deflate';
begin
  Result := HPDFFPCNativeCodecUnavailable;
end;

ตาราง stub นั้นยาว และการอ่านมันบอกคุณแบบเป๊ะว่าความสามารถใดเป็นของเฉพาะ Delphi ในวันนี้: entry point deflate ของ zlib-ng กับ zopfli, การบีบอัดและคลายการบีบอัด libjpeg, codec OpenJPEG JPEG 2000, libtiff พร้อมตัวเตรียมงานแยกตามรูปแบบการบีบอัด, JBIG2 ทั้ง encode และ decode, entry point การแปลงสีของ Little-CMS และ primitives ฝั่ง AES สิ่งที่ซ่อนอยู่เบื้องหลังการออกแบบ stub สำคัญกว่ารายชื่อในลิสต์ สัญลักษณ์ที่หายไปตอนลิงก์ทำให้คุณเจอกำแพง undefined reference จากยูนิตที่คุณไม่เคยแตะด้วยซ้ำ ส่วน stub ที่ raise ENotSupportedException ให้บิลด์ที่รันได้ ข้อความที่บอกเหตุผล และ stack trace ที่ชี้ตรง call site มันยังหมายความว่าบิลด์ Free Pascal จะไม่ผลิตไบต์ผิด ๆ เงียบ ๆ ในจุดที่บิลด์ Delphi ผลิตไบต์ถูก จับทางผลกระทบลำดับสองด้วย: การรัน codec ภาพจากแหล่งไม่น่าเชื่อถือในโปรเซสแยก เป็นการตัดสินใจที่เกิดขึ้นได้เฉพาะบิลด์ Delphi เพราะบิลด์ Free Pascal ไม่มี decoder เนทีฟในโปรเซสให้ sandbox ตั้งแต่แรกแล้ว

บน Delphi ออบเจกต์ codec แบบ static ของ HotPDF ถูกลิงก์เข้ามาและทำงานแบบเนทีฟ ขณะที่บิลด์ Free Pascal Win64 ข้าม directive การลิงก์แล้วส่งทุกสัญลักษณ์ external ที่ขาดหายไปยัง stub ที่ raise exception พร้อมชื่อที่ call site
การข้าม directive ลิงก์และตั้ง stub ให้ทุกสัญลักษณ์ external เปลี่ยนกำแพง undefined reference ให้เป็นบิลด์ที่รันได้และบอกขีดจำกัดของตัวเอง

การบีบอัด: บรรทัดแรกที่ต้องเปลี่ยนคือ cmNone

ก่อนจะพอร์ตอย่างอื่น ตั้ง Compression เป็น cmNone ก่อน THPDFCompressionMethod มีค่าแค่สองค่า คือ cmNone กับ cmFlateDecode และค่าที่สองวิ่งตรงเข้า entry point deflate ที่เป็น stub ในบิลด์ Free Pascal พิสูจน์ object model หลักก่อนโดยปิดการบีบอัด แล้วจึงตัดสินใจว่ายังต้องการอะไรอีก นั่นคือลำดับที่ smoke test ที่แถมมาใช้: สร้างเอกสารหน้าเดียวแบบไม่บีบอัด โหลดกลับ แล้ว assert ว่าจำนวนหน้าที่ได้กลับมาเป็นหนึ่ง เอาต์พุตแบบไม่บีบอัดใหญ่กว่า และมันก็ยังเป็น PDF ที่สมบูรณ์ตามสเปกอยู่ดี

program HotPDFLazarusSmoke;

{$mode delphi}
{$H+}

uses
  Interfaces, SysUtils, HPDFDoc;

var
  Pdf, Reloaded: THotPDF;
  OutputFile: string;
  PageCount: Integer;
begin
  OutputFile := IncludeTrailingPathDelimiter(GetTempDir) +
    'HotPDF-FPC-Smoke.pdf';
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := OutputFile;
    Pdf.Compression := cmNone;   // cmFlateDecode ไปชนสัญลักษณ์ที่ถูก stub
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', [], 12);
    Pdf.CurrentPage.TextOut(72, 72, 0, 'HotPDF Free Pascal smoke test');
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;

  Reloaded := THotPDF.Create(nil);
  try
    PageCount := Reloaded.LoadFromFile(OutputFile);
    if PageCount <> 1 then
      raise Exception.CreateFmt('Expected one page, got %d', [PageCount]);
  finally
    Reloaded.Free;
  end;
end.

การเรนเดอร์หน้าแบบขนานกลายเป็นอะไร

มันยังคอมไพล์ ยังคืน bitmap ที่ถูกต้อง และหยุดเป็นแบบขนาน THotPDF.RenderLoadedPagesParallel กับ THotPDF.RenderLoadedPagesParallelOrdered ถูกสร้างบน TThread.CreateAnonymousThread พร้อม closure เป็น procedure แบบ inline ซึ่ง Free Pascal 3.2.2 เขียนไม่ได้ เส้นทาง Free Pascal จึงรัน fallback แบบ serial ที่ตัดสินใจได้: เดิน index ของหน้าตามลำดับ เรียก RenderLoadedPageToBitmap ทีละหน้า แล้วนับความสำเร็จ รูปร่าง API, ค่าที่คืน และอาร์เรย์เอาต์พุตไม่เปลี่ยน ซึ่งเป็นสิ่งที่ทำให้โค้ดเบสเดียวบิลด์ได้ทั้งสองทาง

การเรียกเรนเดอร์แบบขนานตัวเดียวกันของ HotPDF ทำงานบนเธรด worker ที่ซ้อนทับกันภายใต้ Delphi และเดิน index หน้าแบบเรียงลำดับภายใต้ Free Pascal โดยเรกคอร์ด pipeline info รายงานจำนวน worker เป็นหนึ่งแทนการปิดบัง fallback
เส้นทาง Free Pascal รักษารูปร่าง API กับอาร์เรย์เอาต์พุตไว้พร้อมรายงานจำนวน worker เป็นหนึ่ง โค้ดที่อ่านเรกคอร์ด info อยู่แล้วจึงเห็นความจริง
var
  Bitmaps: THPDFBitmapArray;
  Info: THPDFParallelRenderPipelineInfo;
  Rendered: Integer;
begin
  Rendered := Pdf.RenderLoadedPagesParallel([0, 1, 2, 3], 150, 4,
    Bitmaps, Info);
  // Delphi: Info.WorkerCount คือเท่าที่งบหน่วยความจำยอมให้
  // Free Pascal: Info.WorkerCount เป็น 1 เสมอ หน้าเรียงตาม index
  if Info.WorkerCount = 1 then
    LogSerialFallback(Rendered, Info.RequestedWorkerCount);

fallback นี้ไม่เงียบ ซึ่งเป็นส่วนที่ควรออกแบบรับมือ มันเติม THPDFParallelRenderPipelineInfo อย่างซื่อตรง: PageCount จากคำขอ, RequestedWorkerCount สะท้อนสิ่งที่คุณขอมา, WorkerCount ตั้งเป็น 1 และจำนวนที่เสร็จกับที่ส่งมอบตรงกับสิ่งที่กลับมาจริง โค้ดที่อ่าน Info อยู่แล้วเพื่อคำนวณขนาด progress bar หรืองบหน่วยความจำทำงานต่อได้และอ่านเจอความจริง แทนที่จะอ่านเจอสมมติฐาน ถ้าแผน throughput ของคุณพาดพิงไปถึง pipeline เรนเดอร์แบบขนานและโมเดล backpressure แผนนั้นคือแผนของฝั่ง Delphi บน Free Pascal จงกันงบไว้กับต้นทุน single-threaded ของ การเรนเดอร์หนึ่งหน้าลง bitmap คูณด้วยจำนวนหน้า

บิลด์ไหนที่ควรส่งมอบจริง

เลือกด้วยความสามารถ ไม่ใช่ด้วยความชอบ ถ้าเวิร์กโฟลว์ของคุณคือการประกอบเอกสาร การวาดข้อความกับเวกเตอร์ การกรอกฟอร์ม การโหลดและบันทึก บิลด์ Free Pascal บน Win64 ครอบคลุมหมด และควรตรวจสอบด้วยการปิดการบีบอัดก่อนจะเปิดอะไรขึ้นมา ถ้างานเกี่ยวกับภาพ JPEG หรือ JPEG 2000 หรือ TIFF หรือ JBIG2, การแปลงสี ICC, เอาต์พุตแบบบีบอัด หรือ throughput ที่พึ่งหลายคอร์ ยังอยู่กับ Delphi หรือ C++Builder ไปก่อน เส้นแบ่งถูกขีดด้วย ABI ระดับไฟล์ออบเจกต์และฟีเจอร์ภาษาที่ขาดหาย ทั้งคู่มองเห็นได้ในซอร์ส ไม่ได้ถูกฝังไว้ในตารางสนับสนุน และทั้งคู่ล้มเหลวด้วยข้อผิดพลาดที่ระบุชื่อ ไม่ใช่ผลลัพธ์ที่ผิด

แพ็กเกจ Free Pascal และ Lazarus มาในชุด distribution เดียวกับยูนิต Delphi และ C++Builder ไลเซนส์เดียวครอบคลุมทั้งคู่ และคุณทดสอบเส้นทาง Lazarus กับเอกสารจริงของตัวเองได้ก่อนจะตัดสินใจใช้จริง หน้าผลิตภัณฑ์ HotPDF Delphi PDF Component เก็บตารางรองรับคอมไพเลอร์ล่าสุดและเอกสาร API ฉบับเต็มเอาไว้