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

JBIG2 Encoder Backends กับ linker ของ Free Pascal

PDFlibPas เข้ารหัสภาพ bilevel เป็น JBIG2 ได้ผ่าน backend สองแบบ แบบหนึ่งคือ MMR encoder เนทีฟภาษา Object Pascal ที่อยู่ในไลบรารีเสมอ อีกแบบคือ external symbol-dictionary encoder ที่ให้ผลลัพธ์เล็กลงอย่างมีนัยสำคัญบนข้อความสแกน และแบบนี้เป็นทางเลือก: โปรเจกต์ต้อง link ยูนิต backend ตัวนี้เข้าไปก่อน มันจึงจะมีอยู่จริง ความต่างตรงนี้เป็นที่มาของความประหลาดใจที่พบบ่อยที่สุดกับฟีเจอร์นี้ จึงควรพูดไว้ก่อน: DefaultJBIG2EncodeOptions ร้องขอ external encoder เป็นค่าเริ่มต้น และเมื่อยูนิต backend ไม่ถูก link คำขอนั้นจะถอยไปเส้นทาง MMR ของ Pascal อย่างเงียบ ๆ

บน Delphi และ C++Builder external backend เป็นชุด static object ที่ build ไว้แล้ว บน Free Pascal มันต้องกลายเป็น DLL และเส้นทางจนถึงข้อสรุปนั้นเป็นเรื่องราวของ linker ที่มีประโยชน์กับทุกคนที่เคยพยายาม link ออบเจกต์ C++ เข้าโปรแกรม Free Pascal

การ register คือสัญญา

ยูนิต backend register ตัวเองจาก initialisation section ของมันผ่านการเรียก RegisterJBIG2EncoderBackend ผู้เรียกขอใช้มันได้ทั้งผ่านบิตใน options คือ PDF_JBIG2_OPTION_EXTERNAL_ENCODER ซึ่งมีค่าเป็น 4 หรือผ่านพารามิเตอร์ UseExternalEncoder ของ entry point ด้านภาพแบบ extended ยูนิต umbrella ของไลบรารีตั้งใจไม่ดึงยูนิต backend เข้ามาเอง เพราะการแบกชุดออบเจกต์ขนาดใหญ่ควรเป็นการตัดสินใจของแต่ละโปรเจกต์ ในแผนผัง C++Builder ยกตัวอย่างเช่น มันถูก include อย่างชัดเจนโดยโปรเจกต์ที่ต้องการเท่านั้น

ผลที่ตามผู้เรียกก็คือการร้องขอ external encoder เป็นเพียงความชอบ ไม่ใช่การรับประกัน และบิลด์ที่ลืมยูนิตจะให้ไฟล์ที่ใหญ่ขึ้นแทนที่จะให้ error ถ้าขนาดผลลัพธ์สำคัญพอที่จะร้องขอ encoder ที่ดีกว่า มันก็สำคัญพอที่จะเช็กว่าคุณได้มันมาจริง

ขั้นตอนคำขอเข้ารหัส JBIG2 ของ PDFlibPas ที่ความชอบต่อ external encoder ถอยไปเส้นทาง MMR ของ Pascal อย่างเงียบ ๆ เมื่อไม่มียูนิต backend
การร้องขอ external symbol-dictionary encoder เป็นเพียงความชอบ: link แล้วผลลัพธ์เล็กลง ไม่ link ก็วิ่งเส้นทาง MMR ของ Pascal อย่างเงียบ ๆ พร้อมไฟล์ที่ใหญ่ขึ้น
uses
  PDFlibrary,
{$IFDEF FPC}
  PDFlibJBIG2EncDLL;    // backend แบบ dynamic สำหรับ Free Pascal
{$ELSE}
  PDFlibJBIG2EncC;      // ชุด static object สำหรับ Delphi / C++Builder
{$ENDIF}

var
  Pdf: TPDFlib;
  ImageId: Integer;
begin
  Pdf := TPDFlib.Create(nil);
  try
    Pdf.NewDocument;
    Pdf.NewPage;
    // Interpolate, SymbolExtract, UseExternalEncoder, SkipBlackDots,
    // BlackDotSize, LossyLevel
    ImageId := Pdf.AddImageJBIG2FromFileEx('scan-page-1.tif',
      0, 1, 1, 0, 0, 0);
    if ImageId = 0 then
      raise Exception.Create('JBIG2 encoding failed');
    Pdf.SaveToFile('archive.pdf');
  finally
    Pdf.Free;
  end;
end;

การ compile ยูนิตใช้แค่สองบรรทัด สัญลักษณ์ต่างหากที่คืองานจริง

ทำให้ยูนิต backend เอง compile ผ่านบน Free Pascal ใช้การเปลี่ยนแปลงเพียงสองอย่าง: ตั้ง assembler dialect และเปลี่ยน constructor ที่สร้าง format-settings จาก record ให้เป็นตัวแปร default ระดับ global นั่นสะท้อนภาพได้ดีว่า Pascal ที่ตรงไปตรงมาพอร์ตระหว่างคอมไพเลอร์ทั้งสองได้ง่ายเพียงใด

ฝั่งสัญลักษณ์ต่างหากคืองานหนัก ชุดออบเจกต์อ้างอิงสัญลักษณ์ C ถึง 176 ตัว ในจำนวนนั้น 128 ตัวมี implementation ภาษา Pascal อยู่ในยูนิตแล้วและต้องแค่ผูกชื่อ export เข้าไป เพราะ Delphi ใช้ชื่อฟังก์ชันเป็นชื่อสัญลักษณ์ ขณะที่ Free Pascal ต้องการการประกาศ public name อย่างชัดเจน อีก 27 ตัวใช้ร่วมกับ codec JPEG 2000 และต้องถูก export จากที่เดียวเท่านั้น เพราะการนิยามซ้ำสองที่ทำให้โปรแกรมที่ link ทั้งสองพัง ส่วนที่เหลืออีก 21 ตัวเป็น entry ระดับ platform และ C runtime คือฟังก์ชันไฟล์ Win32 สิบหกตัวบวกการเรียก standard library อีกหลายตัว และพวกมันถูกย้ายไปอยู่ในยูนิต compatibility ใหม่

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

สามเส้นทาง linking สามทางตัน

linker ภายในของ Free Pascal อ่านไฟล์ออบเจกต์เหล่านี้ไม่ได้ เพราะมันถูกผลิตโดยคอมไพเลอร์ที่ปล่อย section แบบ associative COMDAT และ linker ภายในรายงานว่าไม่รองรับสิ่งนั้น นี่คือการปฏิเสธแบบหน้าตรง ไม่ใช่คำเตือน

การเปลี่ยนไปใช้ external linker ดูเหมือนจะเป็นคำตอบ linker binutils ที่แถมมากับ Free Pascal พังทั้งตัวระหว่างทำ section garbage collection กับ archive นี้ และ flag นั้นเป็นส่วนหนึ่งของชุดพารามิเตอร์คงที่ที่ Free Pascal ส่งให้เป้าหมาย Windows 64 บิต จึงลบออกจาก command line ไม่ได้ สวิตช์ที่เอกสารระบุไว้สำหรับกดมันถูกเมินในเส้นทางนี้ การเอา binutils รุ่นใหม่กว่ามากมาแทนก็ล้มแบบต่างออกไป: มันอ่าน link script ของ Free Pascal ไม่เข้าเลย ให้ผลลัพธ์ว่างเปล่าเมื่อไม่มี script และให้กำแพง relocation error เมื่อมี script

ขอบเขตหนึ่งที่ค้นพบระหว่างทางสมควรรู้ไว้แม้คุณไม่เคยเจอปัญหา linker ตัวนี้ external linker คำนวณพาธไฟล์ออบเจกต์เทียบกับไดเรกทอรีผลลัพธ์ของ executable ไม่ใช่ต้นไม้ซอร์ส ดังนั้นคำสั่ง include-object แบบ relative จึงใช้ได้เฉพาะเมื่อไดเรกทอรีผลลัพธ์บังเอิญเท่ากับ working directory ตอน compile ไลบรารีไม่สามารถสมมติเช่นนั้นกับโปรเจกต์ของผู้ใช้ได้ ซึ่งด้วยตัวมันเองก็เป็นเหตุผลให้เลือกไลบรารีแบบ link เข้ามามากกว่าออบเจกต์หลวม ๆ

สามเส้นทาง linker ที่ล้มเหลวสำหรับออบเจกต์ JBIG2 encoder ภาษา C++ บน Free Pascal และ DLL ที่เปิด entry point C แบบ flat สองตัวจนแก้ปัญหาได้
section แบบ COMDAT ทำให้ linker ภายในและ external ทั้งสองตัวล้ม ผลลัพธ์จึงเป็น encoder C++ ที่ส่งมอบเป็น DLL เดียวที่ยูนิต backend ผูกแบบ dynamic

ทำไมการเปลี่ยนคอมไพเลอร์ C++ ไม่ช่วย

ไอเดียถัดไปที่ชัดที่สุดคือ build ฝั่ง C++ ใหม่ด้วยคอมไพเลอร์ที่ Free Pascal อ่านออบเจกต์ของมันได้ มันไม่ได้ผลเช่นกัน และเหตุผลอยู่ที่รากฐาน ไม่ใช่เรื่องของสวิตช์ translation unit ภาษา C++ ขนาดเล็กที่มี template หนึ่งอัน เมื่อคอมไพล์ด้วยการปิดฟีเจอร์ code-generation ทุกอย่าง ยังคงปล่อย weak external symbols ออกมา เพราะการ instantiate ของ template และ inline ผลิตพวกมันขึ้นมาโดยโครงสร้างของภาษา Free Pascal ปฏิเสธชั้นสัญลักษณ์แบบนี้ทั้งหมด ทิศตรงข้ามก็ล้มเหมือนกัน: linker ภาษา C++ กระแสหลักกินออบเจกต์ของอีกคอมไพเลอร์ไม่ได้เพราะการจัดการ section แบบ COMDAT เช่นเดียวกัน

โค้ด C++ จึงส่งมอบไปยัง Free Pascal ในรูปออบเจกต์ไม่ได้ด้วยเส้นทางใดที่มีอยู่ แต่ส่งได้ในรูป DLL ซึ่งก็คือสิ่งที่เกิดขึ้น: encoder และ dependency ด้าน image processing ของมันถูก build รวมเป็นไลบรารีเดียวที่เปิด entry point C แบบ flat สองตัว และยูนิต backend ของ Free Pascal ผูกกับพวกมันแบบ dynamic แล้ว register ตัวเองเหมือนกับ backend แบบ static ทุกประการ เส้นทางของ Delphi และ C++Builder ไม่ถูกแตะแม้แต่นิด ซึ่งเป็นผลที่ถูกต้อง: ปัญหา portability บน toolchain หนึ่งไม่ควรไปกระทบ toolchain ที่ทำงานได้อยู่แล้ว

ขั้วสีคือสิ่งเดียวที่จะกัดคุณ

ระหว่าง bitmap bilevel ของ Windows กับ JBIG2 encoder มีความไม่ตรงกันของธรรมเนียมที่ระบบชนิดใดจับไม่ได้ scanline แบบหนึ่งบิตต่อพิกเซลของ device-independent bitmap ถือว่าบิตที่ตั้งไว้คือสีขาว encoder ถือว่าบิตที่ตั้งไว้คือสีดำ ส่ง scanline ไปแบบไม่แก้อะไรแล้วคุณจะได้สตรีม JBIG2 ที่ถูกต้องสมบูรณ์ของภาพเนกาทีฟของหน้าคุณ

ธรรมเนียมขั้วสีของ DIB หนึ่งบิตกับ JBIG2 ที่บิตที่ตั้งไว้คือขาวใน scanline แต่ดำใน encoder แก้ด้วยการกลับค่าทุกไบต์
ไบต์เดียวกัน ความหมายตรงข้าม: ไม่กลับค่าทุกไบต์แล้ว encoder จะให้สตรีม JBIG2 ที่ถูกต้องของภาพเนกาทีฟ
// DIB หนึ่งบิต: บิตที่ตั้งไว้คือขาว JBIG2 encoder: บิตที่ตั้งไว้
// คือดำ ให้กลับค่าทุกไบต์ก่อนส่งเข้าไป
for I := 0 to RowBytes - 1 do
  Row[I] := Row[I] xor $FF;

วิธีตรวจสอบสำคัญพอ ๆ กับตัวทางแก้ การเทียบความยาวสตรีมบีบอัดไม่บอกอะไรเลย เพราะภาพเนกาทีมีขนาดบีบอัดใกล้เคียงกัน การเปิดดูหน้าพิสูจน์ได้แค่ว่ามันไม่ได้กลับด้านอย่างโจ่งแจ้ง การเช็กที่เชื่อถือได้คือ render ผลลัพธ์ของทั้งสองเส้นทางการเข้ารหัส เนทีฟ Pascal และ external ออกเป็น PNG แล้วเทียบกันแบบไบต์ต่อไบต์: ทั้งสอง encoder เป็น lossless บนภาพต้นฉบับเดียวกัน อะไรที่ไม่ตรงกันเป๊ะคือบั๊กในตัวใดตัวหนึ่ง การเทียบนี้ตอนนี้เป็น regression test ถาวรแล้ว และมันคือรูปแบบ assertion ที่คุ้มค่าที่จะสร้างขึ้นทุกครั้งที่ implementation สองตัวควรให้ผลตรงกันเป๊ะ

เลือก backend แบบไหนดี

สำหรับเนื้อหา bilevel ทั่วไป ไม่ว่าจะเป็น halftone แบบ dither ภาพ line art หรือกราฟิกผสม MMR encoder เนทีฟของ Pascal ใช้ได้ดีและไม่มีต้นทุนด้าน deployment สำหรับข้อความสแกน ซึ่งเป็นกรณีที่ JBIG2 ถูกออกแบบมาเพื่อ external symbol-dictionary encoder ต่างหากที่คือที่ที่การลดขนาดอยู่ เพราะมันแยกรูปทรง glyph ที่ซ้ำเข้า dictionary แทนที่จะเข้ารหัสทุกครั้งที่ปรากฏใหม่ ถ้าคุณผลิตคลังเอกสารสแกน ความต่างนี้ใหญ่พอที่จะเปลี่ยนแผนการจัดเก็บได้

คำถามระดับต้นน้ำ คือภาพ bilevel ถูกผลิตขึ้นอย่างไรในตอนแรก สำคัญกับขนาดผลลัพธ์ไม่ยิ่งหย่อนกัน การ render ขาวดำแบบอิง region อธิบายไว้ในบทความ monochrome region rendering และกลยุทธ์ขนาดระดับเอกสารทั้งฉบับอยู่ในการปรับขนาดไฟล์ PDF และ font subsetting สำหรับชุดสแกนที่มีหน้าซ้ำ ๆ การ deduplicate มักชนะการบีบอัดที่ดีขึ้น ซึ่งเป็นหัวข้อของการ deduplicate ภาพเชิงการรับรู้ ความพร้อมของ toolchain และ backend ในแต่ละ platform ระบุไว้บนหน้าผลิตภัณฑ์losLab PDF Developer Library