HotPDF THotPDF.CompressDocument е един-единствен ключ, с който BeginDoc произвежда най-малкия беззагубен PDF, който компонентата може да запише: FlateDecode на максимално ниво, cross-reference stream с object stream-и, font subsetting и компакти font subset-и, които преномерират запазените глифи зад изричен /CIDToGIDMap. EndDoc после връща вашите собствени настройки. Тристраничен тестов документ с Arial и SimSun падна от 10.2 MB на 20 KB с идентичен рендер
Какво всъщност включва CompressDocument?
CompressDocument презаписва шест настройки на writer-а, плюс тавана на object stream-ите, за един документ и възстановява всички след това. На BeginDoc, преди PDF версията да се уточни, HotPDF записва вашите стойности и задава Compression на cmFlateDecode, CompressionLevel на clMaximum, включва EnableFontSubsetting и CompactFontSubsetting и активира UseXRefStream плюс UseObjectStreams (ISO 32000-1 §7.5.7 и §7.5.8). Object stream-ите искат PDF 1.5, така че по-стара Version се вдига на 1.5, когато не е заключена. PDF/A-1 забранява и двете структури, така че PDF/A-1 документ си пази класическата cross-reference таблица и получава само Flate и font работата. Изображенията остават точно както сте ги вградили
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, така че изключение по средата на отчет не оставя дълго живееща компонента заклещена на максимална компресия за следващата задача. Самият property CompressDocument си остава True; само шестте назъмени настройки се връщат. С версията се обръща повече внимание. HotPDF отменя собственото си вдигане на 1.5 само ако документът все още завършва на 1.5, така че когато друга feature е бутнала файла на 1.6 по време на изпълнението (вграден OpenType font, да речем), по-високата версия остава — точно както би било без компресия
Защо font subset-ите са още големи без компакция?
Класически TrueType subset маха очертанията, които никога не чертаете, но пази всеки glyph ID там, където е бил, и точно тази номерация го държи тежък. Content stream-ът показва CIDs, равни на оригиналните GIDs, така че subset-ът е длъжен да пази loca отместване и hmtx запис за всеки слот до най-високия глиф, който е запазил — празен или не. За латински шрифт тази тежест е шум. За CJK шрифт като SimSun, чиито идеографи седят дълбоко в много голяма glyph таблица, два китайски знака дърпат след себе си таблици, оразмерени за целия шрифт. Правилата за subset closure на оформени глифи решават кои глифи оцеляват; компакцията е за това колко струват оцелялите
CompactFontSubsetting преномерира запазените глифи в плътен диапазон, започващ от нула, и записва /CIDToGIDMap stream върху CIDFont-а, който ISO 32000-1 §9.7.4.2 дефинира като таблица от двубайтови GIDs, индексирани по CID. Тази таблица е целият трик. Content stream-ите, масивът с ширини /W и ToUnicode CMap всички пазят оригиналните CIDs, така че нищо вече написано не трябва да се променя; само търсенето от CID към глиф се мести в map-а. В теста, породил feature-та, SimSun с два символа падна от 24.8 KB шрифтова дана на 3.1 KB
Компакцията има твърди граници и се влошава тихо, вместо да се проваля. HotPDF строи компакти subset-и само за Type 0 TrueType шрифтове — както тези, зададени чрез SetFont с включен subsetting, така и шрифта, регистриран чрез RegisterUnicodeTTF. Прост TrueType шрифт намира глифите си чрез cmap вътре в font програмата, което преномерирането би счупило, така че той пази разредения subset. OpenType-CFF шрифтове също нямат компактен път. Компактен build, който се провали, се връща към разредения subset вместо да вдига грешка. Property-то е изключено по подразбиране, така че съществуващият изход си остава байт-идентичен, докато под PDF/A регистрираният Unicode шрифт винаги получава компактен subset
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;
Как опакованият writer изстисква файловата структура?
Щом шрифтовете и stream-овете са дребни, речниците и cross-reference данните стават най-големият останал разход, така че object-stream writer-ът зад CompressDocument прорязва и тях. Ръководството за object stream-и и инкрементални актуализации покрива самия контейнерен формат; компресиращият път добавя четири доизпипвания отгоре:
- Компактен синтаксис по ISO 32000-1 §7.2.2: интервал се записва само между два токена, които иначе биха се слепили като обикновени символи, така че
/Type /Pageстава/Type/Page - Полетата на cross-reference stream-а приемат всяка ширина, която §7.5.8.2 позволява, така че файл под 16 MB пази всяко отместване в 3 байта вместо 4
- До 250 обекта влизат във всеки object stream вместо обичайните 100, освен ако не сте задали собствена таван чрез
ConfigureAdaptiveObjectStreamPacking - Когато файлът не е шифрован, Catalog-ът и Info речникът също се опаковат в object stream-и; шифрованият изход ги пази на най-високо ниво
Компактният синтаксис дойде с капан, който си струва да знаете, ако разширявате writer-а. Подписването попълва подписа след запис на файла, като търси в байтовете literal placeholder-ите /ByteRange ( и /Contents <, а компактното изписване би ги превърнало в /ByteRange( и /Contents<, които търсенето никога не намира. Signature речниците (Type Sig или DocTimeStamp, FT Sig) и encryption речникът затова пазят разреденото оформление. Сроден дефект засягаше build-ове преди v2.766.41: всяко object-stream записване, включително CompressDocument, започваше с два реда %PDF- header, така че надградете, ако строг валидатор флага вашия изход
Може ли да компресирате вече зареден PDF?
Да, чрез options overload-а CompressLoadedDocument(Options, Info), който пуска същите беззагубни стъпки върху съществуващ файл. С THPDFLoadedDocumentCompressionOptions.Default той маха неизползвани page ресурси, слива идентични шрифтове и форми, прави subset на вградените шрифтове с компакти subset-и включени, презаплита нефильтрирани Flate, LZW, ASCII и RunLength stream-ове с Flate, когато резултатът е по-малък, и кара следващият запис да ползва object stream-и. HighRatioFlate е изключен по подразбиране, а object stream-ите се прескачат за PDF/A-1 и инкрементални записвания. Parameterless overload-ът CompressLoadedDocument е по-старото, по-тясно извикване, което само Flate-компресира некомпресирани stream-ове
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;
Две граници имат значение на заредения път. Всяка стъпка пренаписва байтове, покрити от подпис, така че документ със signature полета се отказва като цяло: извикването връща 0, задава RefusedBySignaturePolicy и не променя нищо, освен ако не зададете AllowSignatureInvalidation, след което Info.SignaturesInvalidated ви казва на какво сте се отказали. Компакцията е и по-консервативна тук, отколкото на пътя на създаване. HotPDF компактира само font програми, ползвани единствено от CIDFontType2 шрифтове с Identity /CIDToGIDMap, където CID е равно на GID, и прескача програми със съществуващ map stream, /CIDSet или color glyph таблици като COLR, sbix, CBDT или SVG, защото компактният rebuild би изпуснал цветните слоеве. Обърнете внимание също, че Info.BytesSaved сумира само стъпките за ресурси, шрифтове и stream-ове; печалбата от object stream-ите се показва при записа на файла
Какви резултати да очаквате на практика?
Печалбите следват колко от файла е некомпресирана структура и раздутана шрифтова дана, не колко страници има. Тристраничната проба с Arial и SimSun се срина от 10.2 MB на 20 KB при генериране с CompressDocument и от 10.2 MB на 19.8 KB, когато некомпресираният оригинал беше зареден и прекаран през CompressLoadedDocument, с идентичен рендер и в двата случая. PDF, който вече е компактен, едва помръдва: в регресионния набор такива файлове записваха в рамките на -0.07% до +0.06% от оригиналния си размер. Фото-тежките файлове печелят малко, защото нито единият, нито другият път пипа image дана
Ако генерирате едни и същи CJK отчети всяка нощ, съчетайте компактите subset-и с устойчивия font subset кеш на диск, за да не се повтаря subsetting работата при всяко пускане, и diff-вайте компресирани изходи по обектно съдържание, а не по байтове, тъй като една променена клетка пре-Flate-ва целия object stream. Пълните референции на property-та и record-и са на продуктовата страница на HotPDF Delphi PDF компонента