Teknisk artikel

Reducer PDF-filstørrelse i Delphi: Skrifttyper, billeder, LZW

For at reducere PDF-filstørrelsen i Delphi tilbyder losLab PDF Library tre API'er, der tackler de tre største kilder til overflødige data: SubsetEmbeddedFonts omskriver hvert integreret TrueType-skrifttypeprogram til kun de tegn (glyphs), dokumentet rent faktisk renderer, DownsampleImages nedsampler rasterbilleder, der overskrider en mål-DPI, og NormalizeLZWStreams erstatter ældre LZWDecode-komprimering med FlateDecode. Hver af dem returnerer antallet af objekter, den har ændret, så et nul fortæller dig, at handlingen var uden virkning frem for en lydløs fejl

Hvorfor er min sammenføjede PDF større end dens kildefiler?

En sammenføjet eller programmatisk genereret PDF er normalt for stor af en af tre årsager: Fuldt integrerede skrifttyper, billeder med en opløsning langt over deres visningsopløsning, og strømme, der stadig er komprimeret med det forældede LZW-filter. ISO 32000-1 §9.9 tillader en producent at integrere hele skrifttypeprogrammet, og de fleste producenter gør netop det, da det er den sikre standardindstilling. En komplet Arial FontFile2 fylder flere hundrede kilobytes; integrer den i et dusin kildefiler, sammenføj dem, og du bærer rundt på et dusin kopier af tegnkonturer for tegn, som ingen har skrevet. Sammenføjningen i sig selv skaber ikke spildet, den koncentrerer det blot i en enkelt fil, hvor det samlede resultat endelig bliver synligt

Billeder er den anden synder. En scanning med en bredde på 4800 pixels placeret i en ramme på en kvart side leverer ca. 40 gange flere pixeldata, end en 300 DPI printpipeline kan udnytte. Den tredje er mere stille: Strømme filtreret med LZWDecode. ISO 32000-1 §7.4.4 specificerer både LZWDecode og FlateDecode og bemærker, at Flate normalt komprimerer mindst lige så godt; i praksis er Flate-output konsekvent mindre for de samme data, og LZW overlever primært i filer, der har passeret gennem værktøjer fra 1990'erne på et eller andet tidspunkt i deres historik. Resten af denne artikel gennemgår de tre losLab PDF Library-kørsler, der løser hvert problem, og kombinerer dem derefter i én enkelt pipeline

Delmængdedannelse af skrifttyper med SubsetEmbeddedFonts

SubsetEmbeddedFonts mindsker enhver integreret TrueType-skrifttype i et indlæst dokument til de tegn, som dokumentet rent faktisk bruger, og den behøver ingen argumenter, da den udleder bevaringslisten fra selve indholdsstrømmene. Internt gennemgår kørslen hver sides indholdsstrøm med GetTextRuns, indsamler de tegnkoder, der refereres til under hver skrifttyperessource, opbygger en bevaringsliste og overdrager det oprindelige skrifttypeprogram to Windows FontSub-motoren (CreateFontPackage) for at producere en delmængde. Det omskrevne program erstatter FontFile2-strømmen på stedet, og BaseFont-navnet får et LOSABC+-tag, hvilket er den konvention med seks store bogstaver og et plustegn, som ISO 32000-1 §9.6.4 definerer for delmængder af skrifttyper. Dette præfiks er også det, der gør kaldet idempotent: Kør kørslen to gange, og skrifttyper, der allerede er behandlet, genkendes og springes over, hvorfor det er sikkert at integrere det i et batchjob, der kan besøge filer igen

var
  Lib: TPDFlib;
  Fonts: Integer;
begin
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile('merged-report.pdf', '') = 1 then
    begin
      Fonts := Lib.SubsetEmbeddedFonts;
      // Fonts = number of FontFile2 programs rewritten;
      // 0 means nothing embedded, or everything already subsetted
      Lib.SaveToFile('merged-report-subset.pdf');
    end;
  finally
    Lib.Free;
  end;
end;

To implementeringsdetalser er værd at kende, fordi de forklarer API'ens begrænsninger. For det første er kørslen målrettet FontFile2, så den dækker integrerede TrueType-programmer; skrifttyper, der er integreret som Type 1 eller ren CFF, efterlades urørte i stedet for at risikere noget. For det andet afhænger den af FontSub, hvilket gør SubsetEmbeddedFonts kun tilgængelig på Windows. En finere detalje fra implementeringen: Hvorvidt en skrifttype er kvalificeret, besluttes ved faktisk at opløse referencekæden FontDescriptorFontFile2, ikke ved at stole på en heuristik med et integreret-flag, da skrifttyper i et indlæst dokument aldrig har gennemgået den bogføring på oprettelsessiden, der sætter sådanne flag. Hvis den opløste strøm eksisterer, er skrifttypen en kandidat; hvis ikke, springes den over uden fejl

Det ærlige kompromis: En delmængdeskrifttype indeholder kun de tegn (glyphs), der var til stede på tidspunktet for delmængdedannelsen. Hvis et efterfølgende værktøj, eller din egen kode, senere tilføjer tekst i den samme skrifttype, vil ethvert tegn uden for delmængden mangle kontur og blive renderet som et manglende tegn. Udfør delmængdedannelsen som det sidste indholdsændrende trin, aldrig før et redigeringstrin. Den samme forsigtighed gælder, hvis du planlægger at trække skrifttypen ud igen senere til genbrug; artiklen om udtrækning af tekst, billeder og skrifttyper med PDFlibPas dækker, hvad et udtrukket delmængdeprogram kan og ikke kan give dig

Hvordan beslutter DownsampleImages, hvilke billeder der skal mindskes?

DownsampleImages(MaxDPI, Quality, Filter) nedsampler kun de billeder, som den med sikkerhed kan kalde oversamplede, ved hjælp af et bevidst konservativt DPI-estimat. Et PDF-billede XObject gemmer pixeldimensioner, men ingen pålidelig fysisk opløsning, og et eventuelt DPI-tag fra kildebilledet overlever sjældent en indlæs-rediger-gem-cyklus. Derfor estimerer kørslen SrcDPI = PixelWidth / 8.5, hvilket i praksis svarer til at spørge: Hvis dette billede strakte sig over hele bredden af en Letter-side, hvad ville dets opløsning så være? Kun billeder, hvis estimat overstiger MaxDPI, behandles. Skævheden er tilsigtet: Et billede, der er placeret småt på siden, har en reel DPI, der er højere end estimatet, så kørslen under-trigger frem for at forringe en trykkvalitetsressource, den ikke kan måle

Quality fra 1 til 100 vælger JPEG-genkodningskvaliteten, mens 0 beholder outputtet som tabsfri Flate i PNG-stil; Filter vælger nedsamplingskernen, 0 for et boksgennemsnit og 1 for bilinear. Til scannet kontorpapirarbejde er DownsampleImages(150, 75, 1) et fornuftigt udgangspunkt; til alt, der kan genoptrykkes, skal du hæve MaxDPI til 300 or helt springe kørslen over. Nedsampling er det eneste tabsgivende trin ud af de tre, så det bør placeres bag en indstilling, som dine brugere kan slå fra

Konvertering af ældre LZW-strømme med NormalizeLZWStreams

NormalizeLZWStreams er den nemme gevinst: Den dekomprimerer tabsfrit enhver LZWDecode-strøm og genkomprimerer den med FlateDecode på stedet og returnerer antallet af konverterede strømme. Den håndterer både en enkelt /Filter /LZWDecode-post og LZW, der optræder i et filterkædearray, hvor kun LZW-leddet udskiftes, og resten af kæden bevares. Predictor-parametre (Predictor, Columns, Colors, BitsPerComponent) læses fra strømmens DecodeParms og sendes videre til dekomprimeringsenheden, så predictor-kodede billeddata konverteres korrekt frem og tilbage. Da begge filtre er bit-præcise codecs, er de afkodede bytes identiske før og efter; kun beholderkomprimeringen ændrer sig, og derfor er denne kørsel sikker at køre betingelsesløst på alle filer

På et dokument uden LZW-strømme returnerer kaldet blot 0 og rører intet, hvilket bibliotekets regressionssuite tester eksplicit: En nyoprettet Flate-kun fil skal rapportere nul konverteringer. Denne no-op garanti er vigtig, når kørslen indgår i en pipeline, der behandler tusindvis af forskelligartede filer, nogle fra 2024 og nogle fra 1998

Den komplette pipeline til størrelsesoptimering i Delphi

De three kørsler kombineres i en enkelt indlæs-optimer-gem funktion, og rækkefølgen betyder mindre, end man skulle tro, fordi de opererer på adskilte objekttyper: Skrifttyper, billede-XObjects og strømfiltre. At køre delmængdedannelsen først er stadig det mest ryddelige valg, da det er den kørsel, der har en redigeringsrækkefølge-begrænsning

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 -> subset
    Images  := Lib.DownsampleImages(150, 75, 1); // >150 DPI -> JPEG q75, bilinear
    Streams := Lib.NormalizeLZWStreams;        // LZWDecode -> FlateDecode
    Result := Lib.SaveToFile(Dst) = 1;
    // Log Fonts/Images/Streams: three zeros mean the file was already lean
  finally
    Lib.Free;
  end;
end;

Verificer pipelinen på samme måde, som biblioteket verificerer sig selv: En rundtur (round-trip). Regressionsmålingerne for v3.130 opretter et dokument, gemmer det, genindlæser det, kører optimeringen, gemmer igen og kontrollerer derefter tre ting: Outputtet er mindre, de returnerede tællinger svarer til forventningerne, og en genindlæsning af den optimerede fil kan stadig fortolkes og renderes. At genskabe denne opret-optimer-genindlæs løkke mod et udvalg af dine egne produktionsfiler, og sammenligne udtrukket tekst før og efter, er en investering på en time, der fanger integrationsfejl, længe før en kunde åbner en beskadiget faktura

// Round-trip check: the optimized file must still load cleanly
Lib := TPDFlib.Create;
try
  Assert(Lib.LoadFromFile('merged-report-opt.pdf', '') = 1);
  Assert(Lib.GetPageCount > 0);
finally
  Lib.Free;
end;

Hvor passer pipelinen ind i en sammenføjningsarbejdsgang? Efter sammenføjningen, ikke under den. At sammenføje først og optimere det enkelte resultat betyder, at hver integreret skrifttype delmængdebehandles én gang mod foreningen af alle brugte tegn, i stedet for pr. kildefil. Hvis sammenføjningsbåndbredden er flaskehalsen, tilbyder PDFlibPas en hurtig vej på byte-niveau, der undgår fuld objektfortolkning, beskrevet i artiklen om hurtig PDF-sammenføjning med byte-referenceskiftning; og for input, der er for store til at kunne være fuldt ud i hukommelsen, dækker direkte filadgang til sammenføjning og opdeling for store PDF-filer streaming-vejen. Begge parres naturligt med en afsluttende optimeringskørsel på det sammenføjede output

Hvad de tre kørsler ikke vil gøre

losLab PDF Librarys optimeringstrio udelukker bevidst alt, hvad der ændrer dokumentets semantik. SubsetEmbeddedFonts forener ikke duplikerede skrifttyper på tværs af sammenføjede kilder to ét program, den mindsker hver uafhængigt; deduplikering er en anden og mere risikabel transformation. DownsampleImages vil springe over et billede, hvis konservative DPI-estimat forbliver under tærsklen, selv når et menneske kan se, at det er for stort til sin ramme. Og ingen af kørslerne rører ved dokumentstrukturen, så en fil, der er pustet op af tusindvis af forældreløse objekter, kræver en genskrivningslagring frem for disse kørsler på strømniveau. Inden for disse grænser fjerner kombinationen af delmængdedannelse af skrifttyper, nedsampling af billeder og LZW-til-Flate-normalisering de tre klassiske kilder til PDF-opsvulmning med ét forudsigeligt API-kald hver. De tre funktioner leveres som en del af losLab PDF Library til Delphi, C# og VB.NET sammen med sammenføjnings-, udtræknings- og rendering-API'erne diskuteret ovenfor