П'ятдесятисторінковий скан-контракт повторює той самий алфавіт на кожній сторінці, але кодувальник JBIG2, що будує один словник символів на кожне зображення, перенавчає цей алфавіт п'ятдесят окремих разів. HotPDF, рідний компонент PDF для Delphi та C++Builder, натомість може накопичувати один спільний словник символів на весь документ і підняти його до єдиного потоку рівня документа /JBIG2Globals, тож власний потік JBIG2 кожної сторінки просто посилається на ідентифікатори символів замість зберігання власної копії алфавіту
Ця стаття навмисно вузька й охоплює лише те, як HotPDF внутрішньо будує цей спільний доступ між сторінками — основи JBIG2, порівняння з CCITT та компроміси Lossless проти LossyLevel уже описані в супутній статті про рідне бітове стиснення JBIG2 у Delphi, яку ця стаття вважає прочитаною
Чому потосторінкове стиснення JBIG2 усе одно повторює ту саму вартість?
Відповідь у тому, що ніщо не переносить стан між викликами. Щоразу, коли кодувальник HotPDF будує словник символів для одного зображення, цей словник обмежений одним конкретним викликом AddImage: прохід зіставлення форм починається з нуля, кожен гліф на сторінці класифікується як новий, а результатні растрові зображення заново арифметично кодуються й зберігаються. Подайте тому самому кодувальнику п'ятдесят сторінок, набраних тим самим шрифтом, і він охоче повторить весь цей прохід навчання п'ятдесят разів, бо з його точки зору кожна сторінка — незв'язане зображення, яке просто виглядає схожим. Потосторінковий UseSymbolDictionary уже значно перевершує пласке кодування загального регіону на одній сторінці, але він упирається в стелю задовго до тієї межі, яку справжній багатосторінковий скан лишає невикористаною
Як HotPDF ділить один словник символів між сторінками?
Увімкніть AccumulateGlobalsAcrossPages у THPDFJBIG2Options, і HotPDF тримає один словник символів живим у пам'яті протягом усього життя документа замість того, щоб відкидати його після кожного зображення. Гліфи кожної наступної сторінки перевіряються проти цього накопичувального словника, перш ніж щось буде перекодовано заново: форма, що вже існує, повторно використовується за своїм ідентифікатором символу, і лише форма, якої ніхто ще не бачив, додається й кодується в словник. Порівняння повторно використовує ту саму логіку допуску, яку LossyLevel застосовує на одній сторінці — трохи шумний скан тієї самої літери все одно рахується збігом, — тож накопичувач мовчки не роздувається до одного запису словника на кожну варіацію того самого гліфа на рівні пікселів. Вилучення відбувається першим і живить це порівняння: HotPDF обходить растрове зображення кожної сторінки й видобуває зв'язні форми через заливку (flood fill) проти чорних пікселів, ту саму ідею, що й обведення чорнильних плям вручну, і саме ці вилучені форми, а не сирі блоки пікселів, порівнюються з накопичувальним словником
Як спільний словник розташовується всередині потоку /JBIG2Globals
Накопичений словник записується як один сегмент словника символів усередині потоку /JBIG2Globals, утримуваний під фіксованим номером сегмента, щоб кожна сторінка могла посилатися на ту саму ціль. Усередині організації вбудованого JBIG2, яку визначає ISO 32000-1 §7.4.7, сегмент текстового регіону може назвати інший сегмент своїм джерелом символів через поле посилання на сегмент у заголовку сегмента, і саме на цей механізм спирається HotPDF: потік globals несе один великий словник символів, а власний потік JBIG2 кожної сторінки скорочується до сегмента інформації про сторінку плюс сегмента текстового регіону, чий список посилань вказує назад на сегмент globals. Те, що раніше було самодостатнім бітовим потоком на сторінку, стає коротким списком позицій та ідентифікаторів символів, і кожна сторінка, побудована так, посилається на той самий непрямий об'єкт /JBIG2Globals, а не на його копію. Власне регресійне покриття HotPDF перевіряє саме це: закодуйте короткий документ, де кожна сторінка має інший розклад гліфів, перезавантажте його й порахуйте, скільки різних посилань на об'єкт /JBIG2Globals з'являється у файлі, — один документ, одне посилання на об'єкт, незалежно від того, скільки сторінок надали символи до нього
Увімкнення накопичення словника символів між сторінками
Перемикач розташований на тому самому записі опцій, описаному в супутній статті, і потребує узгодження чотирьох налаштувань, перш ніж накопичення справді увімкнеться
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;
Це поєднання — не необов'язкова прикраса. Шов зовнішнього кодувальника, описаний у статті про бітове стиснення, — той, що реєструється через RegisterJBIG2EncoderBackend заради промислової якості стиснення, — побудований навколо кодування по одному зображенню, і власні демонстрації накопичення й регресійні тести HotPDF завжди поєднують AccumulateGlobalsAcrossPages із UseExternalEncoder := False. Ставтеся до цього як до жорсткої вимоги, а не пропозиції: спільний доступ між сторінками — це можливість рідного кодувальника, і зареєстрований зовнішній бекенд просто не є частиною шляху, що будує спільний словник
Наскільки справді меншим стає багатосторінковий скан?
Чесна відповідь починається з того, що спершу не зрушило голку. Попередній випуск додав кеш за вмістом для потоків /JBIG2Globals — пошук за 64-бітним хешем FNV-1a байтів потоку, щоб два зображення, які випадково дали побайтово ідентичні дані globals, могли поділити один об'єкт PDF. Виміряний проти реального результату, цей кеш майже не допоміг, бо наявне виявлення дублікатів цілих зображень HotPDF уже згортало побайтово ідентичні зображення ще до того, як кеш узагалі отримував шанс спрацювати. Урок був у тому, що дедуплікація на рівні потоку окупається лише тоді, коли два справді різні зображення сторінок усе одно можуть поділяти один словник, що зростає, — саме це й дає справжнє накопичення між сторінками
Для цього складнішого випадку власна інженерна оцінка HotPDF ставить додаткову економію приблизно на 30-60 відсотків меншою, ніж досягає сама лише дедуплікація на рівні потоку, для типового багатосторінкового скана, побудованого з одного повторюваного шрифту, — діапазон рухається залежно від того, наскільки справді повторюється візуальний словниковий запас документа, оскільки сторінка, повна унікальних діаграм, не дає словнику нічого повторно використати. Ставтеся до цього як до цільового показника дизайну, а не гарантії для будь-якого конкретного входу, і вимірюйте власні документи, а не довіряйте одному числу. Демонстрація 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.
Де накопичення між сторінками впирається у свої межі
Накопичений словник обмежений 4096 символами — тією самою стелею, яку потосторінковий рідний кодувальник уже застосовує на одній сторінці. Перетніть цю межу посеред документа, і HotPDF не піднімає виняток і не перериває виконання: накопичувач відхиляє новий гліф, і сторінка, що його ввела, автоматично відкочується до незалежного потосторінкового кодування, тож документ усе одно виходить коректним — ви просто перестаєте отримувати економію між сторінками для тих сторінок, що перевищили стелю. Другий запобіжник стежить за загальним розміром, а не кількістю символів: щойно сукупна ширина символів накопиченого словника перевищує 131071 піксель, HotPDF автоматично скидає поточний пакет на диск і починає нову групу globals, замість того щоб дозволити одній структурі в пам'яті рости необмежено. Жодне з цих обмежень не потребує коду з вашого боку, оскільки обидва — автоматичні відкати, а не винятки, які треба перехоплювати
Відповідність PDF/A — єдине налаштування, що вимикає весь механізм повністю, а не просто обмежує його. HotPDF тихо підміняє JBIG2 на CCITT Group 4, щойно PDFACompliance непорожнє, на кожній сторінці, незалежно від AccumulateGlobalsAcrossPages чи будь-чого іншого в JBIG2Options, — навмисний вибір відповідності, не помилка, але це означає, що архівний профіль і спільний доступ до символів між сторінками сьогодні взаємно виключні. Яку б конфігурацію ви не обрали, декодуйте те, що записали, перш ніж довіряти цьому: завантажте файл назад через LoadFromFile і пропустіть кожну сторінку через ExtractLoadedImage, що розв'язує спільні globals за вас так само, як це зробив би будь-який читач, що відповідає специфікації, і порівняйте результат із вихідними растровими зображеннями
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;
Спільний доступ до словника між сторінками торкається лише бітового боку зображень документа. Якщо той самий конвеєр також випускає згенеровані текстові сторінки поряд зі сканами — обкладинки, покажчики, шар тексту OCR, — потоки об'єктів та потоки xref атакують іншу половину бюджету розміру файлу, стискаючи структуру документа, яку додають ці сторінки. Спільні globals JBIG2 між сторінками постачаються як частина компонента HotPDF для Delphi та C++Builder, поряд з потосторінковими опціями JBIG2 та рештою конвеєра стиснення