Техническа статия

Споделяне на речници от символи JBIG2 между страници в Delphi

Договор от петдесет сканирани страници повтаря една и съща азбука на всяка страница, но кодер на 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 обхожда растерното изображение на всяка страница и извлича свързаните форми чрез заливане, приложено върху черните пиксели, по същата идея като ръчно проследяване на петна от мастило, и именно тези извлечени форми, а не необработени блокове пиксели, се сравняват с текущия речник

Как споделеният речник стои в поток /JBIG2Globals

Натрупаният речник се записва като един сегмент на речник от символи вътре в потока /JBIG2Globals, поддържан с фиксиран номер на сегмент, така че всяка страница да сочи към една и съща цел. Вградената организация на JBIG2, дефинирана в ISO 32000-1 §7.4.7, позволява на сегмент за текстов регион да назове друг сегмент като източник на символи чрез полето referred-to-segment в заглавката на сегмента и точно на този механизъм разчита HotPDF: потокът с глобалните данни съдържа един голям речник от символи, а собственият JBIG2 поток на всяка страница се свива до сегмент с информация за страницата плюс сегмент за текстов регион, чийто списък referred-to сочи обратно към глобалния сегмент. Това, което преди е било самостоятелен битов поток за всяка страница, се превръща в кратък списък от позиции и идентификатори на символи, а всяка страница, изградена по този начин, препраща към един и същ косвен обект /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 хеш стойност на байтовете на потока, така че две изображения, които случайно създават битово идентични глобални данни, да могат да споделят един 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 записва текущата група на диска и автоматично започва нова група с глобални данни, вместо да позволява на една структура в паметта да расте без ограничение. Нито едно от тези ограничения не изисква код от ваша страна, защото и двете са автоматични резервни пътища, а не изключения, които трябва да прихващате

Съответствието с PDF/A е единствената настройка, която изключва целия механизъм, вместо само да го ограничава. HotPDF тихо заменя JBIG2 с CCITT Group 4 в момента, в който PDFACompliance стане непразен, на всяка страница, независимо от AccumulateGlobalsAcrossPages или от всичко останало в JBIG2Options — преднамерен избор за съответствие, а не дефект, но това означава, че архивен профил и споделяне на символи между страници днес са взаимно изключващи се. Независимо до каква конфигурация стигнете, декодирайте написаното, преди да му се доверите: заредете файла отново с LoadFromFile и извлечете всяка страница чрез ExtractLoadedImage, което разрешава споделените глобални данни по същия начин, по който би го направил всеки съвместим четец, след което сравнете резултата с изходните растерни изображения

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 потоците атакуват другата половина от бюджета за размера на файла, като компресират структурата на документа, която тези страници добавят. Глобалните данни на JBIG2 между страници се доставят като част от HotPDF Component за Delphi и C++Builder, наред с опциите за JBIG2 на ниво изображение и останалата част от конвейера за компресиране