Bài viết kỹ thuật

Chia sẻ từ điển ký hiệu JBIG2 giữa các trang trong Delphi

Một hợp đồng scan năm mươi trang lặp lại cùng một bảng chữ cái trên mỗi trang, nhưng một bộ mã hóa JBIG2 xây dựng một từ điển ký hiệu cho mỗi ảnh sẽ huấn luyện lại bảng chữ cái đó năm mươi lần riêng biệt. HotPDF, thành phần PDF Delphi và C++Builder gốc, thay vào đó có thể tích lũy một từ điển ký hiệu dùng chung trên toàn tài liệu và nâng nó thành một stream /JBIG2Globals duy nhất ở cấp tài liệu, để mỗi stream JBIG2 riêng của từng trang chỉ tham chiếu ID ký hiệu thay vì lưu trữ bản sao riêng của bảng chữ cái

Bài viết này cố tình giữ phạm vi hẹp và chỉ nói về cách HotPDF xây dựng cơ chế chia sẻ giữa các trang này nội bộ ra sao — những kiến thức nền về JBIG2, so sánh với CCITT, và sự đánh đổi giữa Lossless và LossyLevel đã có trong bài viết đồng hành về nén bilevel JBIG2 gốc trong Delphi, bài viết này giả định bạn đã đọc bài đó

Vì sao nén JBIG2 theo từng trang vẫn lặp lại cùng một chi phí?

Câu trả lời là không có gì mang trạng thái qua các lệnh gọi. Mỗi lần bộ mã hóa của HotPDF xây dựng một từ điển ký hiệu cho một ảnh, từ điển đó chỉ có phạm vi trong đúng một lệnh gọi AddImage: lượt so khớp hình dạng bắt đầu lại từ số không, mỗi glyph trên trang được phân loại là mới, và các bitmap kết quả được mã hóa số học và lưu trữ mới toàn bộ. Đưa cùng một bộ mã hóa qua năm mươi trang cùng dùng một kiểu chữ và nó vui vẻ lặp lại toàn bộ lượt huấn luyện đó năm mươi lần, vì từ góc nhìn của nó, mỗi trang là một ảnh không liên quan tình cờ trông giống nhau. UseSymbolDictionary theo từng trang đã vượt trội hơn một lượt mã hóa generic-region phẳng với biên độ lớn trên một trang đơn, nhưng nó chạm trần khá xa dưới mức mà một bản scan nhiều trang thực sự có thể đạt được

HotPDF chia sẻ một từ điển ký hiệu duy nhất giữa các trang như thế nào?

Bật AccumulateGlobalsAcrossPages trên THPDFJBIG2Options và HotPDF giữ một từ điển ký hiệu sống trong bộ nhớ suốt vòng đời của tài liệu thay vì loại bỏ nó sau mỗi ảnh. Các glyph của mỗi trang tiếp theo được kiểm tra đối chiếu với từ điển đang chạy đó trước khi bất cứ thứ gì được mã hóa lại: một hình dạng đã tồn tại sẵn được tái sử dụng qua ID ký hiệu của nó, và chỉ một hình dạng chưa ai từng thấy mới được thêm vào và mã hóa vào từ điển. Việc so sánh tái sử dụng cùng logic dung sai mà LossyLevel áp dụng trên một trang đơn — một bản scan hơi nhiễu của cùng một chữ cái vẫn tính là khớp — nên bộ tích lũy không âm thầm phình to thành một mục từ điển cho mỗi biến thể mức pixel của cùng một glyph. Việc trích xuất diễn ra trước tiên và nạp cho phép so sánh đó: HotPDF duyệt bitmap của mỗi trang và lấy ra các hình dạng liên thông qua flood fill trên các pixel đen, cùng ý tưởng như việc lần theo các vết mực bằng tay, và chính các hình dạng đã trích xuất đó, không phải các khối pixel thô, được so sánh với từ điển đang chạy

Từ điển dùng chung nằm bên trong một stream /JBIG2Globals như thế nào

Từ điển đã tích lũy được ghi thành một đoạn từ-điển-ký-hiệu duy nhất bên trong stream /JBIG2Globals, giữ ở một số đoạn cố định để mỗi trang đều có thể trỏ vào cùng một đích. Bên trong tổ chức JBIG2 nhúng mà ISO 32000-1 §7.4.7 định nghĩa, một đoạn text-region có thể đặt tên một đoạn khác làm nguồn ký hiệu của nó thông qua trường đoạn-được-tham-chiếu trong header của đoạn, và đó chính xác là cơ chế HotPDF dựa vào: stream globals mang một từ điển ký hiệu lớn duy nhất, và stream JBIG2 riêng của mỗi trang thu gọn xuống thành một đoạn thông-tin-trang cộng với một đoạn text-region có danh sách được-tham-chiếu trỏ ngược về đoạn globals. Thứ từng là một bitstream tự chứa cho mỗi trang trở thành một danh sách ngắn các vị trí và ID ký hiệu, và mỗi trang được xây dựng theo cách này tham chiếu đến cùng một đối tượng /JBIG2Globals gián tiếp giống hệt nhau thay vì một bản sao của nó. Bộ hồi quy riêng của HotPDF kiểm tra chính xác điều đó: mã hóa một tài liệu ngắn nơi mỗi trang có bố cục glyph khác nhau, nạp lại nó, và đếm xem có bao nhiêu tham chiếu đối tượng /JBIG2Globals khác biệt xuất hiện trong file — một tài liệu, một tham chiếu đối tượng, bất kể bao nhiêu trang đã đóng góp ký hiệu vào đó

Bật chế độ tích lũy từ điển ký hiệu giữa các trang

Công tắc này nằm trên cùng bản ghi tùy chọn được nói đến trong bài viết đồng hành, và nó cần bốn thiết đặt đồng thuận với nhau trước khi việc tích lũy thực sự hoạt động

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;

Sự kết hợp đó không phải trang trí tùy chọn. Đường mở rộng bộ mã hóa ngoài được mô tả trong bài viết về nén bilevel — cái mà bạn đăng ký qua RegisterJBIG2EncoderBackend để đạt tỉ lệ nén cấp sản xuất — được xây dựng xoay quanh việc mã hóa theo từng ảnh, và các bản demo tích lũy cùng các test hồi quy riêng của HotPDF luôn kết hợp AccumulateGlobalsAcrossPages với UseExternalEncoder := False. Hãy coi đó là một yêu cầu bắt buộc chứ không phải một gợi ý: chia sẻ giữa các trang là một tính năng của bộ mã hóa gốc, và một backend ngoài đã đăng ký đơn giản là không nằm trong đường xử lý xây dựng từ điển dùng chung

Một bản scan nhiều trang thực sự nhỏ đi bao nhiêu?

Câu trả lời trung thực bắt đầu từ những gì không tạo ra khác biệt trước tiên. Một bản phát hành trước đó đã thêm một cache theo nội dung địa chỉ cho các stream /JBIG2Globals — một lượt tra cứu được đánh khóa bằng một hash FNV-1a 64-bit của các byte stream, để hai ảnh tình cờ tạo ra dữ liệu globals giống hệt nhau từng byte có thể chia sẻ một đối tượng PDF. Đo trên đầu ra thực tế, cache đó gần như không giúp được gì, vì cơ chế phát hiện trùng lặp toàn ảnh sẵn có của HotPDF đã gộp các ảnh giống hệt từng byte trước cả khi cache có cơ hội chạy. Bài học rút ra là việc khử trùng lặp ở cấp stream chỉ đem lại lợi ích một khi hai ảnh trang thực sự khác nhau vẫn có thể chia sẻ một từ điển đang lớn dần, chính là điều mà việc tích lũy thực sự giữa các trang mang lại

Với trường hợp khó hơn đó, ước tính kỹ thuật riêng của HotPDF đặt mức tiết kiệm thêm vào khoảng 30 đến 60 phần trăm nhỏ hơn so với chỉ khử trùng lặp ở cấp stream đạt được, cho một bản scan nhiều trang điển hình được xây dựng từ một font lặp lại — con số này thay đổi theo mức độ từ vựng thị giác của tài liệu thực sự lặp lại, vì một trang toàn sơ đồ độc nhất không cho từ điển gì để tái sử dụng. Hãy coi đó là một mục tiêu thiết kế chứ không phải một cam kết cho bất kỳ đầu vào cụ thể nào, và đo trên chính tài liệu của bạn thay vì tin vào một con số duy nhất. Bản demo JBIG2Benchmark đi kèm với HotPDF tồn tại chính vì mục đích đó: nó mã hóa cùng một bản scan nhiều trang theo bốn cách khác nhau và in ra kích thước file kết quả cho mỗi cấu hình, để phép so sánh chạy trên chính hỗn hợp bản scan của bạn thay vì một bản scan tổng hợp

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.

Giới hạn của việc tích lũy giữa các trang

Từ điển đã tích lũy bị giới hạn ở mức 4096 ký hiệu, cùng trần mà bộ mã hóa gốc theo từng ảnh đã áp đặt trên một trang đơn. Vượt qua giới hạn đó giữa tài liệu và HotPDF không ném ra ngoại lệ hay hủy lượt chạy: bộ tích lũy từ chối glyph mới, và trang đưa glyph đó vào tự động rơi về mã hóa độc lập theo từng ảnh, nên tài liệu vẫn ra đúng — bạn chỉ ngừng nhận được khoản tiết kiệm giữa các trang cho bất kỳ trang nào vượt qua trần đó. Một cơ chế bảo vệ thứ hai theo dõi tổng kích thước thay vì số lượng ký hiệu: một khi tổng chiều rộng ký hiệu kết hợp của từ điển đã tích lũy vượt qua 131071 pixel, HotPDF tự động đổ lô hiện tại ra đĩa và bắt đầu một nhóm globals mới, thay vì để một cấu trúc trong bộ nhớ lớn lên vô hạn định. Không giới hạn nào trong hai giới hạn này cần code gì từ phía bạn, vì cả hai đều là các cơ chế dự phòng tự động chứ không phải ngoại lệ bạn cần bắt lấy

Tuân thủ PDF/A là thiết lập duy nhất tắt hẳn toàn bộ cơ chế thay vì chỉ giới hạn nó. HotPDF âm thầm thay CCITT Group 4 cho JBIG2 ngay khi PDFACompliance không rỗng, trên mọi trang, độc lập với AccumulateGlobalsAcrossPages hay bất cứ thứ gì khác trên JBIG2Options — một lựa chọn tuân thủ có chủ đích, không phải một lỗi, nhưng điều đó có nghĩa là một hồ sơ lưu trữ và việc chia sẻ ký hiệu giữa các trang loại trừ lẫn nhau ở thời điểm hiện tại. Dù bạn chọn cấu hình nào, hãy giải mã lại những gì bạn đã ghi trước khi tin tưởng nó: nạp lại file bằng LoadFromFile và kéo từng trang qua ExtractLoadedImage, hàm này tự giải quyết globals dùng chung cho bạn theo đúng cách bất kỳ trình đọc tuân thủ chuẩn nào cũng làm, và so sánh kết quả với các bitmap nguồn của bạn

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;

Việc chia sẻ từ điển giữa các trang chỉ đụng đến phía ảnh bilevel của một tài liệu. Nếu cùng pipeline đó cũng phát sinh các trang văn bản được tạo ra cạnh các bản scan — trang bìa, trang mục lục, một lớp văn bản OCR — thì object stream và xref stream tấn công nửa còn lại của ngân sách kích thước file bằng cách nén cấu trúc tài liệu mà các trang đó thêm vào. Chia sẻ globals JBIG2 giữa các trang đi kèm sẵn trong HotPDF Component dành cho Delphi và C++Builder, cùng với các tùy chọn JBIG2 theo từng ảnh và phần còn lại của pipeline nén