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

Намаляване на размера на PDF файл в Delphi: Шрифтове, изображения, LZW

За намаляване на размера на PDF файл в Delphi, losLab PDF Library предоставя три API, които атакуват трите най-големи източника на излишно раздуване: SubsetEmbeddedFonts пренаписва всяка вградена програма за TrueType шрифт само до глифовете, които документът действително изобразява; DownsampleImages преизчислява растерните изображения, които надвишават целевата стойност за DPI; а NormalizeLZWStreams заменя старото компресиране LZWDecode с FlateDecode; Всеки метод връща броя на променените обекти, така че нулата ви показва, че операцията е била без ефект, а не че е имало скрит срив

Защо моят обединен PDF е по-голям от изходните му файлове?

Обединен или програмно генериран PDF обикновено е твърде голям поради една от три причини: напълно вградени шрифтове, изображения с разделителна способност много над тази на техния дисплей и потоци, които все още са компресирани със стария филтър LZW; ISO 32000-1 §9.9 позволява на създателя да вгради пълната програма за шрифт и повечето разработчици правят точно това, тъй като това е безопасното поведение по подразбиране; Една пълна програма FontFile2 за Arial достига стотици килобайти; вградете я в дузина изходни файлове, обединете ги и ще носите дузина копия на контурите на глифовете за символи, които никой не е написал; Самото обединяване не генерира излишния обем, то просто го концентрира в един файл, където общият размер накрая става видим

Изображенията са вторият нарушител; Сканирано изображение с ширина 4800 пиксела, поставено в рамка с размер една четвърт страница, доставя приблизително 40 пъти повече данни за пиксели, отколкото може да използва конвейер за печат с 300 DPI; Третият е по-тих: потоци, филтрирани с LZWDecode; ISO 32000-1 §7.4.4 дефинира както LZWDecode, така че и FlateDecode, и отбелязва, че Flate обикновено компресира поне толкова добре; на практика Flate компресираната информация винаги е по-малка при едни и същи данни, а LZW оцелява главно във файлове, преминавали през софтуер от 90-те години в някакъв момент от историята си; Останалата част от тази статия описва трите етапа на losLab PDF Library, които коригират всеки проблем, и след това ги комбинира в един конвейер

Извличане на подмножества от шрифтове със SubsetEmbeddedFonts

SubsetEmbeddedFonts свива всеки вграден TrueType шрифт в зареден документ до знаците, които документът действително използва, и не се нуждае от аргументи, тъй като извежда списъка за запазване от самите потоци от съдържание; Вътрешно методът обхожда потока от съдържание на всяка страница с GetTextRuns, събира кодовете на символите, препращани към всеки ресурс за шрифт, изгражда списък за запазване и предава оригиналната програма за шрифт на Windows FontSub модула (CreateFontPackage), за да произведе подмножество; Пренаписаната програма заменя потока FontFile2 на място, а името на BaseFont получава маркер LOSABC+ – конвенцията с шест главни букви и знак плюс, която ISO 32000-1 §9.6.4 определя за подмножества от шрифтове; Този префикс също така прави извикването идемпотентно: стартирайте го два пъти и вече обработените шрифтове се разпознават и пропускат, което прави безопасно използването му в пакетни задачи, които могат да преглеждат отново файлове

var
  Lib: TPDFlib;
  Fonts: Integer;
begin
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile('merged-report.pdf', '') = 1 then
    begin
      Fonts := Lib.SubsetEmbeddedFonts;
      // Fonts = брой пренаписани програми FontFile2;
      // 0 означава, че няма вградени програми или всичко вече е обработено
      Lib.SaveToFile('merged-report-subset.pdf');
    end;
  finally
    Lib.Free;
  end;
end;

Две подробности за имплементацията си струва да знаете, тъй като те обясняват границите на API; Първо, преминаването е насочено към FontFile2, така че обхваща вградените TrueType програми; шрифтовете, вградени като Type 1 или чисти CFF, се оставят недокоснати, за да се избегне риск; Второ, методът разчита на FontSub, което прави SubsetEmbeddedFonts достъпен само за Windows; По-фина подробност от имплементацията: дали даден шрифт отговаря на условията се решава чрез реално проследяване на веригата от препратки FontDescriptorFontFile2, а не чрез сляпо доверяване на евристичен флаг за вграждане, тъй като шрифтовете в зареден документ никога не са преминали през административните процеси от страната на създаването, които задават такива флагове; Ако проследеният поток съществува, шрифтът е кандидат; ако не – се пропуска без грешка

Честният компромис: подмножеството на шрифта съдържа само глифовете, налични по време на извличането; Ако инструмент по веригата или вашият собствен код по-късно добавят текст в същия шрифт, всеки символ извън подмножеството няма да има контур и ще се изобрази като липсващ глиф; Правете извличане на подмножество като последна стъпка в промяната на съдържанието, никога преди етап на редактиране; Същото предупреждение важи, ако планирате да извлечете шрифта по-късно за повторна употреба; статията за извличане на текст, изображения и шрифтове с PDFlibPas покрива какво може и какво не може да ви даде една извлечена програма с подмножество

Как DownsampleImages решава кои изображения да свие?

DownsampleImages(MaxDPI, Quality, Filter) преизчислява само тези изображения, които може уверено да определи като прекалено големи, като използва умишлено консервативна оценка за DPI; PDF изображение от тип XObject съхранява размери в пиксели, но няма надеждна физическа разделителна способност, а всеки маркер за DPI от изходното изображение рядко оцелява след цикъл зареждане-редактиране-запазване; Затова алгоритъмът оценява SrcDPI = PixelWidth / 8.5, на практика питайки: ако това изображение обхваща пълната ширина на страница Letter, каква би била неговата разделителна способност? Само изображения, чиято оценка надвишава MaxDPI, се променят; Това отклонение е умишлено: изображение, поставено в малък размер на страницата, има действителен DPI, по-висок от оценката, така че алгоритъмът се активира по-рядко, вместо да влоши качеството на важен актив за печат, който не може да измери

Параметърът Quality от 1 до 100 избира качеството на новото JPEG кодиране, докато 0 запазва изходния резултат като Flate без загуби от тип PNG; Filter избира ядрото за повторно изчисляване – 0 за средно аритметично (box average) и 1 за билинейно; За сканирани офис документи стойностите DownsampleImages(150, 75, 1) са разумна отправна точка; за всичко, което може да бъде отпечатано отново, вдигнете MaxDPI до 300 или пропуснете тази стъпка изцяло; Понижаването на разделителната способност е единствената стъпка със загуби от трите, така че нейното място е зад опция, която потребителите могат да изключат

Конвертиране на стари LZW потоци с NormalizeLZWStreams

NormalizeLZWStreams е безплатната печалба: той декомпресира без загуба всеки LZWDecode поток и го компресира обратно с FlateDecode на място, връщайки броя на конвертираните потоци; Той поддържа както единичен запис /Filter /LZWDecode, така и LZW, намиращ се в масив от филтри, където се заменя само LZW връзката, а останалата част от веригата се запазва; Параметрите на предсказателя (Predictor, Columns, Colors, BitsPerComponent) се четат от DecodeParms на потока и се предават на декомпресора, така че кодираните с предсказател данни за изображения се обработват коректно; Тъй като и двата филтри са точни на ниво бит кодеци, декодираните байтове са идентични преди и след; променя се само компресирането на контейнера, поради което е безопасно да стартирате тази стъпка безусловно за всеки файл

При документ без LZW потоци извикването просто връща 0 и не променя нищо, което регресионните тестове на библиотеката проверяват изрично: новосъздаден файл само с Flate трябва да отчете нула конвертирания; Тази гаранция за липса на действие има значение, когато стъпката се намира в конвейер, който обработва хиляди разнородни файлове, някои от 2024 г., а други от 1998 г

Пълният конвейер за оптимизация на размера в Delphi

Трите стъпки се комбинират в една единствея функция за зареждане, оптимизиране и запазване, а редът на изпълнение има по-малко значение, отколкото може да очаквате, тъй като те работят върху различни типове обекти: шрифтове, XObject изображения и филтри за потоци; Изпълнението на извличането на подмножества на първо място все пак е чистият избор, тъй като това е стъпката с ограничение в реда на редактиране

function OptimizePDF(const Src, Dst: string): Boolean;
var
  Lib: TPDFlib;
  Fonts, Images, Streams: Integer;
begin
  Result := False;
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile(Src, '') <> 1 then
      Exit;
    Fonts   := Lib.SubsetEmbeddedFonts;        // TrueType FontFile2 -> подмножество
    Images  := Lib.DownsampleImages(150, 75, 1); // >150 DPI -> JPEG q75, билинейно
    Streams := Lib.NormalizeLZWStreams;        // LZWDecode -> FlateDecode
    Result := Lib.SaveToFile(Dst) = 1;
    // Дневник на Fonts/Images/Streams: три нули означават, че файлът вече е бил оптимизиран
  finally
    Lib.Free;
  end;
end;

Проверете конвейера по начина, по който библиотеката проверява себе си: двупосочно (round-trip); Регресионните тестове на v3.130 създават документ, запазват го, зареждат го отново, стартират оптимизацията, запазват го отново и след това потвърждават три неща: изходният резултат е по-малък, върнатите бройки съвпадат с очакванията и повторното зареждане на оптимизирания файл все още се анализира и изобразява; Възпроизвеждането на този цикъл за създаване-оптимизиране-повторно зареждане спрямо извадка от вашите собствени производствени файлове и сравняването на извлечения текст преди и след е едночасова инвестиция, която улавя грешки при интегрирането много преди клиент да отвори повреден документ

// Двупосочна проверка: оптимизираният файл трябва да се зареди успешно
Lib := TPDFlib.Create;
try
  Assert(Lib.LoadFromFile('merged-report-opt.pdf', '') = 1);
  Assert(Lib.GetPageCount > 0);
finally
  Lib.Free;
end;

Къде се вписва конвейерът в работен процес по обединяване? След обединяването, а не по време на него; Обединяването първо и оптимизирането на единствения резултат означава, че всеки вграден шрифт се обработва веднъж спрямо обединението на всички използвани символи, вместо за всеки изходен файл поотделно; Ако скоростта на обединяване е тясното място, PDFlibPas предлага бърз път на ниво байтове, който избягва пълното анализиране на обекти, описан в статията за бързо обединяване на PDF с изместване на байтови препратки; а за входове, твърде големи, за да се поберат изцяло в паметта, статията за директен достъп за обединяване и разделяне на големи PDF файлове описва поточния път; И двата се съчетават естествено с крайна стъпка по оптимизиране на обединения резултат

Какво няма да направят трите стъпки

Триото за оптимизация на losLab PDF Library умишлено изключва всичко, което променя семантиката на документа; SubsetEmbeddedFonts не обединява дублиращи се шрифтове в един от различни източници, а свива всеки независимо; дедупликацията е различна, по-рискова трансформация; DownsampleImages ще подмине изображение, чиято консервативна оценка за DPI остава под прага, дори когато човек би могъл да каже, че е твърде голямо за своята рамка; И нито една от стъпките не докосва структурата на документа, така че файл, раздут от хиляди осиротели обекти, се нуждае от запазване с пренаписване, а не от тези стъпки на ниво поток; В тези граници комбинацията от извличане на подмножества от шрифтове, понижаване на разделителната способност на изображения и нормализиране от LZW към Flate премахва трите класически източника на излишно раздуване на PDF с по едно предвидимо извикване на API; Трите функции се доставят като част от losLab PDF Library за Delphi, C# и VB.NET, заедно с API за обединяване, извличане и изобразяване, обсъдени по-горе