Műszaki cikk

PDF-fájlméret csökkentése Delphi-ben: betűtípusok, képek, LZW

A PDF-fájlméret csökkentéséhez Delphi-ben a losLab PDF Library három olyan API-t biztosít, amelyek a méretnövekedés három legnagyobb forrását célozzák meg: a SubsetEmbeddedFonts átírja az összes beágyazott TrueType betűtípus-programot a dokumentum által ténylegesen megjelenített karakterekre, a DownsampleImages újramintavételezi a cél-DPI-t meghaladó raszteres képeket, a NormalizeLZWStreams pedig a régi LZWDecode tömörítést FlateDecode-ra cseréli; mindegyik a módosított objektumok számát adja vissza, így a nulla jelzi, ha a lépés nem végzett műveletet, nem pedig néma hiba történt

Miért nagyobb az összefűzött PDF, mint a forrásfájljai?

Az összefűzött vagy programozottan előállított PDF általában három ok egyikéből adódóan túlméretezett: teljesen beágyazott betűtípusok, a megjelenítési felbontásuknál jóval nagyobb felbontással mintavételezett képek, és a régi LZW szűrővel tömörített adatfolyamok; az ISO 32000-1 §9.9 lehetővé teszi az előállító számára, hogy a teljes betűtípus-programot beágyazza, és a legtöbb előállító pontosan ezt teszi, mivel ez a biztonságos alapértelmezés; egy teljes Arial FontFile2 több száz kilobájtra rúghat; ágyazza be egy tucat forrásfájlba, fűzze össze őket, és máris egy tucatnyi másolatot hordoz a karakterkörvonalakból olyan karakterekhez, amelyeket senki sem gépelt be; maga az összefűzés nem hozza létre a hulladékot, csak egyetlen fájlba koncentrálja azt, ahol a teljes méret végül láthatóvá válik

A képek jelentik a második problémát; egy negyedoldalas keretbe elhelyezett, 4800 képpont szélességű szkennelés körülbelül 40-szer több képpontadatot tartalmaz, mint amennyit egy 300 DPI felbontású nyomtatási folyamat fel tud használni; a harmadik csendesebb: az LZWDecode-dal szűrt adatfolyamok; az ISO 32000-1 §7.4.4 az LZWDecode-ot és a FlateDecode-ot is meghatározza, és megjegyzi, hogy a Flate általában legalább olyan jól tömörít; a gyakorlatban a Flate kimenet ugyanazon adatokon következetesen kisebb, az LZW pedig leginkább olyan fájlokban maradt fenn, amelyek a történetük egy bizonyos pontján az 1990-es évekből származó eszközökön mentek keresztül; a cikk további részében bemutatjuk azt a három losLab PDF Library műveletet, amelyek megoldják az egyes problémákat, majd egyetlen feldolgozási folyamatban egyesítjük őket

Betűtípus részhalmaz-képzés a SubsetEmbeddedFonts segítségével

A SubsetEmbeddedFonts egy betöltött dokumentumban található összes beágyazott TrueType betűtípust a dokumentum által ténylegesen használt karakterekre zsugorít, és nem igényel argumentumokat, mert a megtartandó karakterek listáját magukból a tartalomfolyamokból származtatja; belsőleg a művelet végigmegy az egyes oldalak tartalomfolyamán a GetTextRuns segítségével, összegyűjti az egyes betűtípus-erőforrások alatt hivatkozott karakterkódokat, felépít egy megtartási listát, és átadja az eredeti betűtípus-programot a Windows FontSub motorjának (CreateFontPackage) a részhalmaz előállításához; az átírt program helyettesíti a helyi FontFile2 adatfolyamot, és a BaseFont név egy LOSABC+ előtagot kap, amelyet az ISO 32000-1 §9.6.4 a részhalmaz betűtípusokhoz hat nagybetű plusz egy pluszjel konvencióval határoz meg; ez az előtag teszi a hívást idempotenssé is: futtassa le a műveletet kétszer, és a már részhalmazként kezelt betűtípusok felismerésre és kihagyásra kerülnek, így biztonságosan beépíthető egy olyan kötegelt feladatba, amely újra feldolgozhatja a fájlokat

var
  Lib: TPDFlib;
  Fonts: Integer;
begin
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile('merged-report.pdf', '') = 1 then
    begin
      Fonts := Lib.SubsetEmbeddedFonts;
      // Fonts = az átírt FontFile2 programok száma;
      // A 0 azt jelenti, hogy nincs beágyazott tartalom, vagy már minden részhalmazként van tárolva
      Lib.SaveToFile('merged-report-subset.pdf');
    end;
  finally
    Lib.Free;
  end;
end;

Két megvalósítási részletet érdemes tudni, mert ezek magyarázzák az API határait; először is, a művelet a FontFile2-t célozza meg, így a beágyazott TrueType programokra terjed ki; a Type 1 vagy tiszta CFF betűtípusok érintetlenül maradnak a kockázatok elkerülése érdekében; másodszor, a FontSub-ra támaszkodik, ami a SubsetEmbeddedFonts-ot kizárólag Windows rendszeren teszi elérhetővé; a megvalósítás egy finomabb pontja: az, hogy egy betűtípus alkalmas-e, a FontDescriptorFontFile2 hivatkozási lánc tényleges feloldásával dől el, nem pedig a beágyazott jelző heurisztikájára hagyatkozva, mivel a betöltött dokumentumban lévő betűtípusok soha nem mentek keresztül az ilyen jelzőket beállító létrehozási oldali könyvelésen; ha a feloldott adatfolyam létezik, a betűtípus jelölt; ha nem, akkor hiba nélkül kihagyja

A valós kompromisszum: a részhalmaz betűtípus csak a részhalmaz létrehozásakor jelen lévő karaktereket tartalmazza; ha egy későbbi eszköz vagy a saját kódja később szöveget ad hozzá ugyanazzal a betűtípussal, a részhalmazon kívüli karaktereknek nem lesz körvonala, és hiányzó karakterként fognak megjelenni; a részhalmaz-képzést az utolsó tartalomváltoztatási lépésként végezze el, soha ne a szerkesztési szakasz előtt; ugyanez az óvatosság érvényes akkor is, ha a betűtípust később újra fel szeretné használni; a szövegek, képek és betűtípusok PDFlibPas segítségével történő kinyeréséről szóló cikk bemutatja, mit adhat és mit nem adhat vissza egy kinyert részhalmaz-program

Hogyan dönti el a DownsampleImages, hogy mely képeket zsugorítsa?

A DownsampleImages(MaxDPI, Quality, Filter) csak azokat a képeket mintavételezi újra, amelyeket magabiztosan túl-mintavételezettnek nevezhet, szándékosan konzervatív DPI-becslést használva; a PDF kép-XObject a pixelméreteket tárolja, de nem tartalmaz megbízható fizikai felbontást, és a forrásképből származó DPI-jelölés ritkán éli túl a betöltés-szerkesztés-mentés ciklust; így a lépés a SrcDPI = PixelWidth / 8.5 képlettel becsli meg a felbontást, azt kérdezve: ha ez a kép egy Letter méretű oldal teljes szélességét átfogná, mekkora lenne a felbontása? Csak azokat a képeket érinti, amelyek becsült értéke meghaladja a MaxDPI-t; az elfogultság szándékos: az oldalon kicsiben elhelyezett kép valódi DPI-je magasabb a becsültnél, így a művelet inkább alul-működik, mintsem lerontana egy nyomtatási minőségű elemet, amelyet nem tud megmérni

A Quality 1 és 100 között választja ki a JPEG újrakódolási minőségét, míg a 0 veszteségmentes PNG-stílusú Flate kimenetet tart fenn; a Filter a mintavételezési magot határozza meg, a 0 a doboz-átlagoláshoz, az 1 pedig a bilineárishoz való; a szkennelt irodai dokumentumok esetében a DownsampleImages(150, 75, 1) ésszerű kiindulópont; bármihez, amit újra lehet nyomtatni, emelje a MaxDPI értékét 300-ra, vagy hagyja ki teljesen ezt a lépést; a lefelé mintavételezés a három közül az egyetlen veszteséges lépés, így ennek egy olyan beállítás mögött van a helye, amelyet a felhasználók kikapcsolhatnak

Régi LZW adatfolyamok konvertálása a NormalizeLZWStreams segítségével

A NormalizeLZWStreams ingyenes nyereséget jelent: veszteségmentesen kitömöríti az összes LZWDecode adatfolyamot, és a helyén újra tömöríti azokat FlateDecode segítségével, visszaadva a konvertált adatfolyamok számát; kezeli mind az egyedi /Filter /LZWDecode bejegyzéseket, mind a szűrőlánc tömbben megjelenő LZW-t, ahol csak az LZW hivatkozás cserélődik ki, a lánc többi része pedig megmarad; a prediktor paramétereket (Predictor, Columns, Colors, BitsPerComponent) az adatfolyam DecodeParms részéből olvassa be, és továbbítja a kitömörítőnek, így a prediktorral kódolt képadatok helyesen mennek végbe; mivel mindkét szűrő bitpontos kodek, a dekódolt bájtok teljesen azonosak előtte és utána; csak a tároló tömörítése változik, ezért ezt a lépést minden fájlon biztonságosan futtathatjuk feltétel nélkül

Az LZW adatfolyamokat nem tartalmazó dokumentumok esetében a hívás egyszerűen 0 értéket ad vissza, és semmit sem módosít, amit a könyvtár regressziós tesztcsomagja kifejezetten ellenőriz: egy frissen létrehozott, csak Flate tömörítésű fájlnak nulla konverziót kell jelentenie; ez a garancia fontos, amikor a művelet egy olyan feldolgozási folyamatban van, amely több ezer különféle fájlt dolgoz fel, amelyek közül néhány 2024-ből, mások pedig 1998-ból származnak

A teljes méret-optimalizálási folyamat Delphi-ben

A három művelet egyetlen betöltés-optimalizálás-mentés függvényben egyesül, és a sorrend kevésbé számít, mint gondolná, mert egymástól független objektumtípusokon működnek: betűtípusokon, kép-XObject-eken és adatfolyam-szűrőkön; a részhalmaz-képzés elsőként történő futtatása továbbra is a legtisztább választás, mivel ez a szerkesztési sorrend korlátozásával járó lépés

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 -> részhalmaz
    Images  := Lib.DownsampleImages(150, 75, 1); // >150 DPI -> JPEG q75, bilineáris
    Streams := Lib.NormalizeLZWStreams;        // LZWDecode -> FlateDecode
    Result := Lib.SaveToFile(Dst) = 1;
    // Fonts/Images/Streams naplózása: a három nulla azt jelenti, hogy a fájl már tömör volt
  finally
    Lib.Free;
  end;
end;

Ellenőrizze a folyamatot úgy, ahogy a könyvtár ellenőrzi magát: kétirányú úton; a v3.130 regressziós tesztek létrehoznak egy dokumentumot, elmentik, újratöltik, lefuttatják az optimalizálást, újra elmentik, majd három dolgot állítanak fel: a kimenet kisebb, a visszaadott számok megfelelnek a várakozásoknak, és az optimalizált fájl újratöltése továbbra is értelmezhető és renderelhető; ennek a létrehozás-optimalizálás-újratöltés ciklusnak a reprodukálása a saját termelési fájljainak mintáján, és a kinyert szöveg összehasonlítása előtte és utána egy olyan egyórás befektetés, amely elkapja az integrációs hibákat jóval azelőtt, hogy az ügyfél megnyitna egy hibás számlát

// Kétirányú ellenőrzés: az optimalizált fájlnak továbbra is tisztán be kell töltődnie
Lib := TPDFlib.Create;
try
  Assert(Lib.LoadFromFile('merged-report-opt.pdf', '') = 1);
  Assert(Lib.GetPageCount > 0);
finally
  Lib.Free;
end;

Hol illeszkedik a folyamat az összefűzési munkafolyamatba? Az összefűzés után, nem pedig alatta; az összefűzés elvégzése, majd az egyetlen eredmény optimalizálása azt jelenti, hogy minden beágyazott betűtípus egyszer kerül részhalmazra az összes használt karakter uniójával szemben, ahelyett, hogy forrásfájlonként történne; ha az összefűzési áteresztőképesség a szűk keresztmetszet, a PDFlibPas olyan bájtszintű gyors utat kínál, amely elkerüli a teljes objektumértelmezést, amelyet a gyors PDF-összefűzés bájthivatkozás-eltolással című cikk ír le; a memóriában teljes egészében nem tartható bemenetek esetében pedig a nagy PDF-ek közvetlen elérésű összefűzése és felosztása mutatja be a streamelési utat; mindkettő természetes módon párosul az összefűzött kimenet végső optimalizálási lépésével

Amit a három lépés nem fog megtenni

A losLab PDF Library optimalizálási triója szándékosan kizár mindent, ami megváltoztatja a dokumentum szemantikáját; a SubsetEmbeddedFonts nem egyesíti az összefűzött források duplikált betűtípusait egyetlen programmá, hanem mindegyiket külön-külön zsugorítja; a duplikációk kiszűrése egy másik, kockázatosabb átalakítás; a DownsampleImages figyelmen kívül hagyja azt a képet, amelynek konzervatív DPI-becslése a küszöbérték alatt marad, még akkor is, ha egy ember meg tudná mondani, hogy túlméretezett a keretéhez képest; a lépések egyike sem érinti a dokumentum szerkezetét, így az árva objektumok ezrei miatt felduzzadt fájl újraírás-szerű mentést igényel ezen adatfolyam-szintű műveletek helyett; ezen korlátok között a betűtípus-részhalmazok képzése, a képek lefelé mintavételezése és az LZW-Flate normalizálás kombinációja egyetlen kiszámítható API-hívással távolítja el a PDF-méretnövekedés három klasszikus forrását; a három funkció a Delphi, C# és VB.NET rendszerekhez készült losLab PDF Library részeként érhető el, a fent említett összefűzési, kinyerési és renderelési API-kkal együtt