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

การแชร์ Symbol Dictionary ของ JBIG2 ข้ามหน้าใน Delphi

สัญญาที่สแกนห้าสิบหน้าซ้ำอักขระชุดเดียวกันในทุกหน้า แต่ตัวเข้ารหัส JBIG2 ที่สร้าง symbol dictionary หนึ่งชุดต่อภาพหนึ่งภาพจะฝึกอักขระชุดนั้นซ้ำห้าสิบครั้งแยกกัน HotPDF คอมโพเนนต์ PDF แบบเนทีฟสำหรับ Delphi และ C++Builder สามารถสะสม symbol dictionary ที่ใช้ร่วมกันหนึ่งชุดตลอดทั้งเอกสารแทน และเลื่อนมันขึ้นเป็นสตรีมระดับเอกสารเดียวคือ /JBIG2Globals ทำให้สตรีม JBIG2 ของแต่ละหน้าแค่อ้างอิงถึง symbol ID แทนที่จะเก็บสำเนาอักขระชุดนั้นของตัวเอง

บทความนี้ตั้งใจจำกัดขอบเขตให้แคบ ครอบคลุมแค่ว่า HotPDF สร้างการแชร์ข้ามหน้านี้ภายในอย่างไร พื้นฐานของ JBIG2, การเปรียบเทียบกับ CCITT และการแลกเปลี่ยนระหว่าง Lossless กับ LossyLevel มีอยู่แล้วในบทความคู่กันเรื่องการบีบอัดแบบ bilevel ของ JBIG2 แบบเนทีฟใน Delphi ซึ่งบทความนี้สมมติว่าคุณได้อ่านมาแล้ว

ทำไมการบีบอัด JBIG2 แบบต่อหน้าถึงยังคงต้นทุนเดิมซ้ำๆ

คำตอบคือไม่มีอะไรพก state ข้ามการเรียกเลย ทุกครั้งที่ตัวเข้ารหัสของ HotPDF สร้าง symbol dictionary สำหรับภาพหนึ่งภาพ dictionary นั้นถูกจำกัดขอบเขตอยู่แค่การเรียก AddImage ครั้งนั้นครั้งเดียว รอบการจับคู่รูปทรงเริ่มจากศูนย์ ทุก glyph บนหน้าถูกจัดว่าเป็นของใหม่ และบิตแมปที่ได้จะถูก arithmetic-code และเก็บใหม่ทั้งหมด ป้อนตัวเข้ารหัสตัวเดียวกันด้วยห้าสิบหน้าที่ตั้งด้วย typeface เดียวกัน แล้วมันก็ยินดีทำรอบการฝึกทั้งหมดนั้นซ้ำห้าสิบครั้ง เพราะจากมุมมองของมัน แต่ละหน้าคือภาพที่ไม่เกี่ยวข้องกันซึ่งบังเอิญดูคล้ายกัน UseSymbolDictionary แบบต่อภาพเอาชนะการเข้ารหัสแบบ generic-region เรียบๆ ได้ด้วยส่วนต่างที่กว้างมากในหนึ่งหน้าอยู่แล้ว แต่มันก็ถึงเพดานก่อนที่จะถึงขีดจำกัดที่การสแกนหลายหน้าจริงยังทิ้งไว้บนโต๊ะ

HotPDF แชร์ symbol dictionary เดียวข้ามหน้าได้อย่างไร

เปิด AccumulateGlobalsAcrossPages บน THPDFJBIG2Options แล้ว HotPDF จะเก็บ symbol dictionary หนึ่งชุดไว้ในหน่วยความจำตลอดอายุของเอกสาร แทนที่จะทิ้งมันหลังแต่ละภาพ glyph ของทุกหน้าถัดไปจะถูกตรวจสอบกับ dictionary ที่กำลังทำงานอยู่นั้นก่อนที่อะไรจะถูกเข้ารหัสใหม่ รูปทรงที่มีอยู่แล้วจะถูกใช้ซ้ำโดยอ้างอิงจาก symbol ID ของมัน และมีแค่รูปทรงที่ไม่เคยเห็นมาก่อนเท่านั้นที่จะถูกเพิ่มเข้าไปและเข้ารหัสเข้า dictionary การเปรียบเทียบนำ tolerance logic เดียวกันกับที่ LossyLevel ใช้บนหน้าเดียวมาใช้ซ้ำ การสแกนที่มีสัญญาณรบกวนเล็กน้อยของตัวอักษรเดียวกันยังคงนับว่าตรงกัน ดังนั้นตัวสะสมจึงไม่บวมขึ้นเงียบๆ กลายเป็น dictionary entry หนึ่งรายการต่อความแปรผันระดับพิกเซลของ glyph เดียวกัน การสกัดเกิดขึ้นก่อนและป้อนเข้าการเปรียบเทียบนั้น HotPDF เดินผ่านบิตแมปของแต่ละหน้าและดึงรูปทรงที่เชื่อมต่อกันออกมาผ่าน flood fill กับพิกเซลสีดำ แนวคิดเดียวกับการตามรอยหยดหมึกด้วยมือ และรูปทรงที่สกัดออกมานั้นเอง ไม่ใช่บล็อกพิกเซลดิบ ที่ถูกนำไปเปรียบเทียบกับ dictionary ที่กำลังทำงานอยู่

dictionary ที่ใช้ร่วมกันอยู่ในสตรีม /JBIG2Globals อย่างไร

dictionary ที่สะสมไว้ถูกเขียนเป็น segment แบบ symbol-dictionary หนึ่งชุดภายในสตรีม /JBIG2Globals ซึ่งถือไว้ที่หมายเลข segment คงที่ เพื่อให้ทุกหน้าชี้ไปยังเป้าหมายเดียวกันได้ ภายในโครงสร้าง JBIG2 แบบฝังที่ ISO 32000-1 §7.4.7 กำหนดไว้ segment แบบ text-region สามารถระบุ segment อื่นเป็นแหล่ง symbol ของมันผ่านฟิลด์ referred-to-segment ใน segment header และนั่นคือกลไกที่ HotPDF พึ่งพาโดยตรง สตรีม globals ถือ symbol dictionary ชิ้นใหญ่ชิ้นเดียว และสตรีม JBIG2 ของแต่ละหน้าเองก็หดเหลือแค่ segment แบบ page-info บวก segment แบบ text-region ที่รายการ referred-to ของมันชี้กลับไปยัง segment globals สิ่งที่เคยเป็น bitstream ที่สมบูรณ์ในตัวเองต่อหน้าจึงกลายเป็นรายการสั้นๆ ของตำแหน่งและ symbol ID และทุกหน้าที่สร้างแบบนี้อ้างอิงถึง object /JBIG2Globals แบบ indirect เดียวกันเป๊ะ ไม่ใช่สำเนาของมัน การทดสอบ regression ของ HotPDF เองตรวจสอบสิ่งนี้ตรงๆ เข้ารหัสเอกสารสั้นๆ ที่แต่ละหน้ามี glyph layout ต่างกัน โหลดกลับมา แล้วนับว่ามี object reference ของ /JBIG2Globals ที่ต่างกันกี่ตัวปรากฏในไฟล์ หนึ่งเอกสาร หนึ่ง object reference ไม่ว่าจะมีกี่หน้าที่ร่วมให้ symbol กับมันก็ตาม

การเปิดใช้การสะสม symbol dictionary ข้ามหน้า

สวิตช์นี้อยู่ใน options record เดียวกับที่ครอบคลุมในบทความคู่กัน และต้องมีสี่การตั้งค่าที่สอดคล้องกันก่อนที่การสะสมจะทำงานได้จริง

var
  Pdf: THotPDF;
  Bmp: TBitmap;
  PageIdx, ImgIdx: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.JBIG2Options.Lossless := True;
    Pdf.JBIG2Options.UseSymbolDictionary := True;
    Pdf.JBIG2Options.UseGlobalSegments := True;
    Pdf.JBIG2Options.AccumulateGlobalsAcrossPages := True;  // opt-in, default False
    Pdf.JBIG2Options.UseExternalEncoder := False;            // accumulation needs the native path
    Pdf.JBIG2Options.UseNativeArithmeticFallback := True;
    Pdf.BeginDoc;
    for PageIdx := 0 to ScannedPages.Count - 1 do
    begin
      if PageIdx > 0 then
        Pdf.AddPage;
      Bmp := ScannedPages[PageIdx];             // 1-bit TBitmap for this page
      ImgIdx := Pdf.AddImage(Bmp, icJBIG2);
      Pdf.CurrentPage.ShowImage(ImgIdx, 0, 0, Bmp.Width, Bmp.Height, 0);
    end;
    Pdf.EndDoc;                                  // the shared /JBIG2Globals stream is finalized here
  finally
    Pdf.Free;
  end;
end;

การจับคู่นั้นไม่ใช่ของตกแต่งที่เลือกได้ ช่องต่อของตัวเข้ารหัสภายนอกที่อธิบายในบทความ bilevel-compression ตัวที่คุณลงทะเบียนผ่าน RegisterJBIG2EncoderBackend เพื่อให้ได้อัตราส่วนระดับ production ถูกสร้างขึ้นรอบการเข้ารหัสแบบต่อภาพ และตัวอย่างการสะสมและ regression test ของ HotPDF เองจับคู่ AccumulateGlobalsAcrossPages กับ UseExternalEncoder := False เสมอ ให้ถือว่านี่เป็นข้อกำหนดที่แข็งแรงมากกว่าเป็นแค่คำแนะนำ การแชร์ข้ามหน้าเป็นฟีเจอร์ของตัวเข้ารหัสเนทีฟ และ backend ภายนอกที่ลงทะเบียนไว้ก็ไม่ได้เป็นส่วนหนึ่งของเส้นทางที่สร้าง dictionary ที่ใช้ร่วมกันนั้นเลย

เอกสารสแกนหลายหน้าเล็กลงได้มากแค่ไหนจริงๆ

คำตอบที่ตรงไปตรงมาเริ่มจากสิ่งที่ไม่ได้ขยับตัวเลขมากนักก่อน รุ่นก่อนหน้าเพิ่ม cache แบบ content-addressed สำหรับสตรีม /JBIG2Globals คือการค้นหาโดย key ด้วยแฮช FNV-1a 64 บิตของไบต์ในสตรีม เพื่อให้ภาพสองภาพที่บังเอิญสร้างข้อมูล globals เหมือนกันไบต์ต่อไบต์สามารถแชร์ object PDF เดียวกันได้ เมื่อวัดกับเอาต์พุตจริง cache นั้นแทบไม่ช่วยอะไรเลย เพราะระบบตรวจจับภาพซ้ำทั้งภาพที่มีอยู่แล้วของ HotPDF ยุบภาพที่เหมือนกันไบต์ต่อไบต์ไปก่อนที่ cache จะมีโอกาสทำงานด้วยซ้ำ บทเรียนคือการลดความซ้ำซ้อนระดับสตรีมจะคุ้มค่าก็ต่อเมื่อภาพหน้าสองภาพที่ต่างกันจริงๆ ยังคงแชร์ dictionary ที่กำลังเติบโตได้ ซึ่งเป็นสิ่งที่การสะสมข้ามหน้าที่แท้จริงส่งมอบให้

สำหรับกรณีที่ยากกว่านั้น การประมาณการทางวิศวกรรมของ HotPDF เองระบุว่าการประหยัดเพิ่มเติมอยู่ที่ประมาณ 30 ถึง 60 เปอร์เซ็นต์ เล็กกว่าที่การลดความซ้ำซ้อนระดับสตรีมเพียงอย่างเดียวทำได้ สำหรับเอกสารสแกนหลายหน้าทั่วไปที่สร้างจากฟอนต์ที่ซ้ำกันหนึ่งแบบ ช่วงตัวเลขนี้ขยับตามว่าคำศัพท์ภาพของเอกสารซ้ำกันมากแค่ไหนจริงๆ เพราะหน้าที่เต็มไปด้วยไดอะแกรมที่ไม่ซ้ำกันเลยจะไม่มีอะไรให้ dictionary ใช้ซ้ำได้ ให้ถือว่านี่เป็นเป้าหมายการออกแบบ ไม่ใช่การรับประกันสำหรับอินพุตใดอินพุตหนึ่ง และวัดผลกับเอกสารของคุณเองแทนที่จะเชื่อตัวเลขเดียว ตัวอย่าง JBIG2Benchmark ที่มาพร้อม HotPDF มีไว้เพื่อจุดประสงค์นี้โดยเฉพาะ มันเข้ารหัสการสแกนหลายหน้าชุดเดียวกันสี่วิธีที่ต่างกัน และพิมพ์ขนาดไฟล์ที่ได้ของแต่ละการตั้งค่า ดังนั้นการเปรียบเทียบจะรันกับชุดการสแกนของคุณเองแทนที่จะเป็นชุดสังเคราะห์

procedure RunScenario(const Title: string; AccumulateGlobals: Boolean);
var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.JBIG2Options.Lossless := True;
    Pdf.JBIG2Options.UseSymbolDictionary := True;
    Pdf.JBIG2Options.UseGlobalSegments := True;
    Pdf.JBIG2Options.AccumulateGlobalsAcrossPages := AccumulateGlobals;
    Pdf.JBIG2Options.UseExternalEncoder := not AccumulateGlobals;
    // ... encode the same three-page scan here, then compare file sizes.
  finally
    Pdf.Free;
  end;
end;

begin
  RunScenario('Per-image lossless baseline', False);
  RunScenario('Cross-page accumulated globals', True);
end.

การสะสมข้ามหน้าเจอขีดจำกัดตรงไหน

dictionary ที่สะสมไว้ถูกจำกัดเพดานไว้ที่ 4096 symbol ซึ่งเป็นเพดานเดียวกับที่ตัวเข้ารหัสเนทีฟแบบต่อภาพบังคับใช้อยู่แล้วในหนึ่งหน้า ข้ามขีดจำกัดนั้นกลางเอกสารและ HotPDF จะไม่ยก exception หรือยกเลิกการทำงาน ตัวสะสมจะปฏิเสธ glyph ใหม่นั้น และหน้าที่แนะนำมันจะ fallback ไปเข้ารหัสแบบต่อภาพอิสระโดยอัตโนมัติ ดังนั้นเอกสารยังคงออกมาถูกต้อง คุณแค่หยุดได้รับการประหยัดข้ามหน้าสำหรับหน้าใดก็ตามที่ดันเกินเพดานไป ตัวป้องกันที่สองเฝ้าดูขนาดรวมแทนจำนวน symbol เมื่อความกว้างรวมของ symbol ใน dictionary ที่สะสมไว้ข้าม 131071 พิกเซล HotPDF จะ spill batch ปัจจุบันลงดิสก์และเริ่มกลุ่ม globals ใหม่โดยอัตโนมัติ แทนที่จะปล่อยให้โครงสร้างในหน่วยความจำหนึ่งเดียวเติบโตไม่มีขอบเขต ทั้งสองขีดจำกัดไม่ต้องการโค้ดใดๆ จากฝั่งคุณ เพราะทั้งคู่เป็น fallback อัตโนมัติ ไม่ใช่ exception ที่คุณต้องดักจับ

ความสอดคล้องกับ PDF/A เป็นการตั้งค่าเดียวที่ปิดกลไกทั้งหมดแทนที่จะแค่จำกัดเพดานมัน HotPDF แทนที่ JBIG2 ด้วย CCITT Group 4 อย่างเงียบๆ ทันทีที่ PDFACompliance ไม่ว่างเปล่า ในทุกหน้า ไม่ขึ้นกับ AccumulateGlobalsAcrossPages หรืออะไรอื่นใน JBIG2Options เลย เป็นทางเลือกด้านความสอดคล้องที่จงใจ ไม่ใช่บั๊ก แต่มันหมายความว่าโปรไฟล์สำหรับการเก็บถาวรกับการแชร์ symbol ข้ามหน้าไม่สามารถอยู่ร่วมกันได้ในปัจจุบัน ไม่ว่าคุณจะลงเอยที่การตั้งค่าแบบไหน ให้ decode สิ่งที่คุณเขียนก่อนที่จะเชื่อมัน โหลดไฟล์กลับมาด้วย LoadFromFile และดึงแต่ละหน้าผ่าน ExtractLoadedImage ซึ่งจะ resolve globals ที่ใช้ร่วมกันให้คุณแบบเดียวกับที่ reader ใดๆ ที่เป็นไปตามมาตรฐานจะทำ แล้วเปรียบเทียบผลลัพธ์กับบิตแมปต้นฉบับของคุณ

var
  Loaded: THotPDF;
  PageBmp: TBitmap;
  PageIdx: Integer;
begin
  Loaded := THotPDF.Create(nil);
  try
    Loaded.LoadFromFile('scanned-contract.pdf');
    for PageIdx := 0 to Loaded.PagesCount - 1 do
    begin
      PageBmp := Loaded.ExtractLoadedImage(PageIdx);   // resolves the shared globals for you
      try
        // Compare PageBmp against the source bitmap for this page.
      finally
        PageBmp.Free;
      end;
    end;
  finally
    Loaded.Free;
  end;
end;

การแชร์ dictionary ข้ามหน้าแตะแค่ฝั่งภาพ bilevel ของเอกสารเท่านั้น ถ้า pipeline เดียวกันยังสร้างหน้าข้อความที่สร้างขึ้นควบคู่ไปกับภาพสแกนด้วย เช่น หน้าปก หน้าดัชนี ชั้นข้อความ OCR object stream และ xref stream จัดการอีกครึ่งหนึ่งของงบขนาดไฟล์ ด้วยการบีบอัดโครงสร้างเอกสารที่หน้าเหล่านั้นเพิ่มเข้ามา globals ของ JBIG2 ข้ามหน้ามาพร้อมกับHotPDF Componentรุ่นมาตรฐานสำหรับ Delphi และ C++Builder ควบคู่ไปกับตัวเลือก JBIG2 แบบต่อภาพและ pipeline การบีบอัดที่เหลือ