Технічна стаття

HotPDF CompressDocument: компактні підмножини шрифтів Delphi

HotPDF THotPDF.CompressDocument — це один перемикач, який змушує BeginDoc продукувати найменший безвтратний PDF, на який спроможний компонент: FlateDecode на максимальному рівні, xref-потік з object streams, субсетинг шрифтів і компактні шрифтові підмножини, що перенумеровують збережені гліфи за явним /CIDToGIDMap. Потім EndDoc повертає ваші власні налаштування. Тестовий документ на три сторінки з Arial і SimSun скоїв із 10.2 МБ до 20 КБ за ідентичного рендерингу

Що CompressDocument насправді вмикає?

CompressDocument замінює шість налаштувань писаря плюс ліміт object stream на один документ і після цього повертає їх усі. На BeginDoc, ще до того, як версія PDF усталюється, HotPDF записує ваші значення і ставить Compression у cmFlateDecode, CompressionLevel у clMaximum, вмикає EnableFontSubsetting і CompactFontSubsetting, а також активує UseXRefStream разом із UseObjectStreams (ISO 32000-1 §7.5.7 і §7.5.8). Object streams потребують PDF 1.5, тож старіший Version підіймається до 1.5, коли не замкнений. PDF/A-1 забороняє обидві структури, тож PDF/A-1-документ зберігає класичну таблицю перехресних посилань і отримує лише Flate та шрифтову роботу. Зображення лишаються рівно такими, як ви їх вбудували

Діаграма життєвого циклу HotPDF CompressDocument у Delphi: BeginDoc записує власні значення писаря, замінює шість налаштувань, зокрема Compression і UseObjectStreams, на один документ, а EndDoc повертає кожне позичене значення в своєму найзовнішньому finally, тоді як властивість CompressDocument сама лишається True
Шість налаштувань писаря і ліміт object stream позичаються рівно на один документ і повертаються, коли виконується EndDoc, тож невдалий звіт ніколи не залишає компонент застряглим на максимальному стисненні
var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.AutoLaunch := False;
    Pdf.FileName := 'invoice-2026-1042.pdf';
    Pdf.CompressDocument := True;    // застосовується BeginDoc, скасовується EndDoc
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', [], 12);
    Pdf.CurrentPage.TextOut(40, 40, 0, 'Invoice 2026-1042');
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Відновлення відбувається в найзовнішньому finally з EndDoc, тож виняток посередині звіту не залишає довгоживучий компонент застряглим на максимальному стисненні для наступної задачі. Властивість CompressDocument сама лишається True; повертаються лише шість налаштувань, які вона позичила. З версією поводяться обережніше. HotPDF скасовує власне підняття до 1.5, лише якщо документ усе ще закінчується на 1.5, тож коли інша фіча штовхнула файл до 1.6 під час прогону (скажімо, вбудований шрифт OpenType), старша версія лишається — рівно так, як було б без стиснення

Чому шрифтові підмножини без компакції досі великі?

Класична TrueType-підмножина відкидає контури, які ви ніколи не малюєте, але лишає кожен glyph ID там, де він був, і саме та нумерація тримає її важкою. Потік вмісту показує CID, що дорівнюють початковим GID, тож підмножина мусить тримати зсув loca і запис hmtx для кожного слота аж до найвищого гліфа, який вона утримує, порожній він чи ні. Для латинського шрифту той наклад — шум. Для CJK-шрифту на кшталт SimSun, чиї ідеографи сидять глибоко в дуже великій таблиці гліфів, два китайські символи тягнуть за собою таблиці, розмірені на весь шрифт. Правила замикання шрифтової підмножини для шейпованих гліфів вирішують, які гліфи виживуть; компакція — про те, скільки коштують ті, хто вижив

CompactFontSubsetting перенумеровує збережені гліфи в щільний діапазон, починаючи з нуля, і пише потік /CIDToGIDMap на CIDFont, який ISO 32000-1 §9.7.4.2 визначає як таблицю двобайтових GID, індексованих CID. Та таблиця — і весь трюк. Потоки вмісту, масив ширин /W і ToUnicode CMap усі тримають початкові CID, тож нічому з уже написаного не треба змінюватися; у мапу переїжджає лише пошук від CID до гліфа. У тесті, що мотивував фічу, SimSun із двома символами скоїв із 24.8 КБ шрифтових даних до 3.1 КБ

Порівняння розрідженої шрифтової підмножини HotPDF, яка тримає записи loca та hmtx для кожного початкового glyph ID аж до найвищого утриманого GID, із виводом CompactFontSubsetting, що щільно перенумеровує збережені гліфи з нуля і відображає CID через потік CIDToGIDMap, тоді як потоки вмісту, /W і ToUnicode лишаються незмінними
Перенумерація виносить ціну з шрифтової програми в один маленький потік-мапу — два символи SimSun скоїли з 24.8 КБ до 3.1 КБ, не торкнувшись жодного байта вже написаного вмісту

У компакції тверді межі, і вона деградує тихо, а не падає. HotPDF будує компактні підмножини лише для Type 0 TrueType-шрифтів — як тих, що задані через SetFont з увімкненим субсетингом, так і шрифту, зареєстрованого через RegisterUnicodeTTF. Простий TrueType-шрифт знаходить свої гліфи через cmap усередині шрифтової програми, яку перенумерація зламала б, тож він лишає розріджену підмножину. OpenType-CFF-шрифти теж не мають компактного шляху. Компактна збірка, що зазнала невдачі, відкатується до розрідженої підмножини замість підняття винятку. Властивість усталено вимкнена, тож наявний вивід лишається байт-у-байт ідентичним, тоді як під PDF/A зареєстрований Unicode-шрифт завжди отримує компактну підмножину

Pdf.EnableFontSubsetting := True;
Pdf.CompactFontSubsetting := True;   // працює і без CompressDocument
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('SimSun', [], 12);
Pdf.CurrentPage.TextOut(40, 40, 0, WideString('Total: '#$4E2D#$6587));
Pdf.EndDoc;

Як пакувальний писар стискає структуру файлу?

Коли шрифти й потоки вже малі, найбільшою рештою витрат стають словники та дані перехресних посилань, тож писар object stream, що стоїть за CompressDocument, підтягує й їх. Посібник з object streams та інкрементальних оновлень покриває сам формат контейнера; компресійний шлях додає зверху чотири вдосконалення:

  • Компактний синтаксис за ISO 32000-1 §7.2.2: пробіл пишеться лише між двома токенами, які інакше злилися б як звичайні символи, тож /Type /Page стає /Type/Page
  • Поля xref-потоку беруть будь-яку ширину, яку дозволяє §7.5.8.2, тож файл до 16 МБ зберігає кожен зсув у 3 байтах замість 4
  • До 250 об'єктів іде в кожен object stream замість звичних 100, якщо ви не задали власний ліміт через ConfigureAdaptiveObjectStreamPacking
  • Коли файл не зашифрований, Catalog і словник Info теж пакуються в object streams; зашифрований вивід тримає їх на верхньому рівні

Компактний синтаксис прийшов із пасткою, яку варто знати, якщо ви розширюєте писар. Підписання заповнює підпис після запису файлу, шукаючи в байтах літеральні заповнювачі /ByteRange ( і /Contents <, а компактне написання перетворило б їх на /ByteRange( і /Contents<, які пошук ніколи не знайде. Словники підписів (Type Sig чи DocTimeStamp, FT Sig) і словник шифрування тому зберігають розклад із пробілами. Суміжний дефект уражав збірки до v2.766.41: кожне збереження з object streams, CompressDocument включно, починалося з двох рядків заголовка %PDF-, тож оновіться, якщо строгий валідатор позначає ваш вивід

Чи можна стиснути PDF, що вже завантажений?

Так, через перевантаження з опціями CompressLoadedDocument(Options, Info), яке проганяє ті самі безвтратні кроки по наявному файлу. З THPDFLoadedDocumentCompressionOptions.Default воно прибирає невживані ресурси сторінок, зливає ідентичні шрифти й форми, робить субсетинг вбудованих шрифтів з увімкненими компактними підмножинами, перестискує нефільтровані, Flate, LZW, ASCII та RunLength потоки Flate, коли результат менший, і змушує наступне збереження користуватися object streams. HighRatioFlate усталено вимкнений, а object streams пропускаються для PDF/A-1 та інкрементальних збережень. Перевантаження CompressLoadedDocument без параметрів — старіший, вужчий виклик, який лише Flate-стискає нестиснуті потоки

Потік кроків HotPDF CompressLoadedDocument у Delphi: виклик прибирає невживані ресурси сторінок, зливає ідентичні шрифти й форми, робить субсетинг вбудованих шрифтів компактними підмножинами, перестискує потоки Flate лише коли результат менший, і вмикає object streams для наступного збереження, тоді як поля підписів тригерять RefusedBySignaturePolicy і лишають файл недоторканим
Кожен крок переписує байти, які покриває підпис, тож увесь документ відхиляється, якщо ви явно не дозволите інвалідацію — Info.BytesSaved тоді сумує лише ресурсну, шрифтову і потокову роботу
var
  Doc: THotPDF;
  Options: THPDFLoadedDocumentCompressionOptions;
  Info: THPDFLoadedDocumentCompressionInfo;
begin
  Doc := THotPDF.Create(nil);
  try
    Doc.AutoLaunch := False;
    Doc.LoadFromFile('quarterly-report.pdf');
    Options := THPDFLoadedDocumentCompressionOptions.Default;
    Doc.CompressLoadedDocument(Options, Info);
    if Info.RefusedBySignaturePolicy then
      Writeln(Format('Left untouched: %d signature fields', [Info.SignatureCount]))
    else
    begin
      Writeln(Format('Compact fonts: %d, stream bytes saved: %d',
        [Info.Fonts.CompactSubsetFontCount, Info.BytesSaved]));
      Doc.SaveLoadedDocument('quarterly-report-compact.pdf');
    end;
  finally
    Doc.Free;
  end;
end;

На завантажувальному шляху важать два кордони. Кожен крок переписує байти, які покриває підпис, тож документ із полями підписів відхиляється як ціле: виклик повертає 0, ставить RefusedBySignaturePolicy і нічого не змінює, якщо ви не поставили AllowSignatureInvalidation, після чого Info.SignaturesInvalidated скаже, чим ви пожертвували. Компакція тут також консервативніша, ніж на шляху створення. HotPDF компактує лише шрифтові програми, що вживаються виключно шрифтами CIDFontType2 з Identity /CIDToGIDMap, де CID дорівнює GID, і пропускає програми з наявним потоком-мапою, /CIDSet чи таблицями кольорових гліфів на кшталт COLR, sbix, CBDT чи SVG, бо компактна перебудова втратила б кольорові шари. Зауважте також, що Info.BytesSaved сумує лише кроки ресурсів, шрифтів і потоків; виграш object stream проявляється, коли файл записується

Яких результатів варто очікувати на практиці?

Виграші слідкують за тим, скільки в файлі нестиснутої структури й роздутих шрифтових даних, а не за кількістю сторінок. Тристорінковий зразок з Arial і SimSun стиснувся з 10.2 МБ до 20 КБ при генерації з CompressDocument і з 10.2 МБ до 19.8 КБ, коли нестиснутий оригінал завантажили й прогнали через CompressLoadedDocument, з ідентичним рендерингом в обох випадках. PDF, який уже компактний, ледь ворухнеться: у регресному наборі такі файли економили в межах від -0.07% до +0.06% свого початкового розміру. Файли, важкі на фото, мало що виграють, бо жоден зі шляхів не торкається даних зображень

Якщо ви генеруєте ті самі CJK-звіти щовечора, спарте компактні підмножини з постійним шрифтовим кешем підмножин на диску, щоб субсетингова робота не повторювалася щопрогону, і порівнюйте стиснуті виводи за вмістом об'єктів, а не за байтами, адже одна зміна поля пере-Флейтить цілий object stream. Повні довідники властивостей і записів — на сторінці продукту HotPDF Delphi PDF component