För att minska PDF-filstorleken i Delphi tillhandahåller losLab PDF Library tre API:er som angriper de tre största källorna till onödig storlek: SubsetEmbeddedFonts skriver om varje inbäddat TrueType-typsnittsprogram till de glyfer som dokumentet faktiskt renderar, DownsampleImages nedsamplar rasterbilder som överskrider ett mål-DPI, och NormalizeLZWStreams ersätter äldre LZWDecode-komprimering med FlateDecode. Varje funktion returnerar antalet objekt den har ändrat, så en nolla visar att steget inte utförde någon åtgärd snarare än ett tyst misslyckande
Varför är min sammanfogade PDF större än dess källfiler?
En sammanfogad eller programmatiskt genererad PDF är vanligtvis för stor av en av tre anledningar: helt inbäddade typsnitt, bilder samplade långt över sin visningsupplösning och strömmar som fortfarande är komprimerade med det äldre LZW-filtret. ISO 32000-1 §9.9 tillåter en producent to bädda in hela typsnittsprogrammet, och de flesta producenter gör exakt det eftersom det är det säkra standardvalet. En komplett Arial FontFile2 uppgår till hundratals kilobyte; bädda in den i ett dussin källfiler, sammanfoga dem, och du bär på ett dussin kopior av glyfkonturer för tecken som ingen har skrivit. Sammanfogningen i sig skapar inte spillet, den bara koncentrerar det till en enda fil där summan till slut blir synlig
Bilder är den andra boven i dramat. En 4800 pixlar bred skanning placerad i en ram som täcker en fjärdedels sida skickar ungefär 40 gånger mer pixeldata än vad en utskriftspipeline på 300 DPI kan använda. Den tredje är tystare: strömmar filtrerade med LZWDecode. ISO 32000-1 §7.4.4 specificerar både LZWDecode och FlateDecode och noterar att Flate vanligtvis komprimerar minst lika bra; i praktiken är Flate-utdata konsekvent mindre på samma data, och LZW överlever mest i filer som passerade genom verktyg från 1990-talet vid någon tidpunkt i sin historia. Resten av den här artikeln går igenom de tre stegen i losLab PDF Library som fixar varje problem, och kombinerar dem sedan i en pipeline
Typsnittsdelmängdsbildning (subsetting) med SubsetEmbeddedFonts
SubsetEmbeddedFonts krymper varje inbäddat TrueType-typsnitt i ett inläst dokument till de tecken som dokumentet faktiskt använder, och det behöver inga argument eftersom det härleder spara-listan från själva innehållsströmmarna. Internt går steget igenom varje sidas innehållsström med GetTextRuns, samlar in teckenkoderna som refereras under varje typsnittsresurs, bygger en spara-lista och lämnar över det ursprungliga typsnittsprogrammet till Windows FontSub-motor (CreateFontPackage) för att producera en delmängd. Det omskrivna programmet ersätter FontFile2-strömmen på plats, och BaseFont-namnet får en LOSABC+-tagg, den konvention med sex versaler och ett plustecken som ISO 32000-1 §9.6.4 definierar för delmängdstypsnitt. Det prefixet är också det som gör anropet idempotent: kör steget två gånger så identifieras och hoppas redan delmängdsbildade typsnitt över, vilket gör det säkert att koppla in det i ett batchjobb som kan besöka 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 = antal omskrivna FontFile2-program;
// 0 betyder inget inbäddat, eller att allt redan är delmängdsbildat
Lib.SaveToFile('merged-report-subset.pdf');
end;
finally
Lib.Free;
end;
end;
Två implementeringsdetaljer är värda att veta eftersom de förklarar API:ets gränser. För det första riktar steget in sig på FontFile2, så det täcker inbäddade TrueType-program; typsnitt inbäddade som Type 1 eller ren CFF lämnas orörda för att undvika risker. För det andra förlitar det sig på FontSub, vilket gör SubsetEmbeddedFonts till en funktion som endast fungerar på Windows. En finare punkt från implementeringen: huruvida ett typsnitt kvalificerar sig avgörs genom att faktiskt lösa referenskedjan FontDescriptor → FontFile2, inte genom att lita på en heuristik för inbäddade flaggor, eftersom typsnitt i ett inläst dokument aldrig gick igenom den bokföring på skapandesidan som sätter sådana flaggor. Om den lösta strömmen finns är typsnittet en kandidat; om inte, hoppas det över utan fel
Den ärliga kompromissen: ett delmängdstypsnitt innehåller endast de glyfer som finns vid tidpunkten för delmängdsbildningen. Om ett efterföljande verktyg, eller din egen kod, senare lägger till text med samma typsnitt, kommer alla tecken utanför delmängden att sakna kontur och renderas som en saknad glyf. Gör delmängdsbildning som det sista innehållsändrande steget, aldrig före ett redigeringssteg. Samma försiktighet gäller om du planerar att hämta ut typsnittet senare för återanvändning; artikeln om extrahering av text, bilder och typsnitt med PDFlibPas täcker vad ett extraherat delmängdsprogram kan och inte kan ge dig
Hur beslutar DownsampleImages vilka bilder som ska krympas?
DownsampleImages(MaxDPI, Quality, Filter) nedsamplar endast bilder som den med säkerhet kan kalla översamplade, med hjälp av en medvetet konservativ DPI-uppskattning. Ett PDF-bild-XObject lagrar pixeldimensioner men ingen pålitlig fysisk upplösning, och eventuella DPI-taggar från källbilden överlever sällan en inläsnings-redigerings-sparningscykel. Så steget uppskattar SrcDPI = PixelWidth / 8.5, vilket i princip innebär frågan: om denna bild spände över hela bredden på en Letter-sida, vad skulle dess upplösning vara? Endast bilder vars uppskattning överstiger MaxDPI berörs. Denna snedvridning är avsiktlig: en bild som är placerad liten på sidan har ett verkligt DPI som är högre än uppskattningen, så steget under-triggars hellre än att försämra en utskriftskvalitetstillgång den inte kan mäta
Quality från 1 till 100 väljer JPEG-omkodningskvalitet, medan 0 behåller utdata som förlustfri PNG-liknande Flate; Filter väljer nedsamplingskärna, 0 för box-medelvärde och 1 för bilinjär. För skannade kontorsdokument är DownsampleImages(150, 75, 1) en rimlig startpunkt; för allt som kan skrivas ut igen, höj MaxDPI till 300 or hoppa över steget helt. Nedsampling är det enda förlustdrivande steget av de tre, så det bör ligga bakom en inställning som dina användare kan stänga av
Konvertera äldre LZW-strömmar med NormalizeLZWStreams
NormalizeLZWStreams är den enkla vinsten: den dekomprimerar förlustfritt varje LZWDecode-ström och komprimerar den igen med FlateDecode på plats, och returnerar antalet konverterade strömmar. Den hanterar både en enskild /Filter /LZWDecode-post och LZW som förekommer inuti en filterkedjearray, där endast LZW-länken ersätts och resten av kedjan bevaras. Prediktorparametrar (Predictor, Columns, Colors, BitsPerComponent) läses från strömmens DecodeParms och skickas vidare till dekomprimeraren, så att prediktorkodad bilddata går fram och tillbaka korrekt. Eftersom båda filtren är bit-exakta kodekar är de dekomprimerade byten identiska före och efter; endast behållarens komprimering ändras, vilket är anledningen till att detta steg är säkert att köra villkorslöst på varje fil
På ett dokument utan LZW-strömmar returnerar anropet helt enkelt 0 och rör ingenting, vilket bibliotekets regressionssvit testar explicit: en nyskapad fil med endast Flate måste rapportera noll konverteringar. Den garantin för att ingen åtgärd vidtas är viktig när steget ingår i en pipeline som bearbetar tusentals heterogena filer, vissa från 2024 och andra från 1998
Den kompletta storleksoptimeringspipelinen i Delphi
De tre stegen kombineras till en enda load-optimize-save-funktion, och ordningen spelar mindre roll än man kan förvänta sig eftersom de arbetar på helt skilda objekttyper: typsnitt, bild-XObjects och strömfilter. Att köra delmängdsbildning först är dock fortfarande det snyggaste valet, eftersom det är steget med redigeringsordningsbegränsningar
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 -> delmängd
Images := Lib.DownsampleImages(150, 75, 1); // >150 DPI -> JPEG q75, bilinjär
Streams := Lib.NormalizeLZWStreams; // LZWDecode -> FlateDecode
Result := Lib.SaveToFile(Dst) = 1;
// Logga Fonts/Images/Streams: tre nollor betyder att filen redan var optimerad
finally
Lib.Free;
end;
end;
Verifiera pipelinen på det sätt som biblioteket verifierar sig själv: round-trip. Regressions-testerna för v3.130 skapar ett dokument, sparar det, läser in det igen, kör optimeringen, sparar igen och hävdar sedan tre saker: utdata är mindre, det returnerade antalet matchar förväntningarna och en ny inläsning av den optimerade filen kan fortfarande tolkas och renderas. Att återskapa den create-optimize-reload-loopen mot ett urval av dina egna produktionsfiler, och jämföra extraherad text före och efter, är en investering på en timme som fångar upp integrationsfel långt innan en kund öppnar en trasig faktura
// Round-trip-kontroll: den optimerade filen måste fortfarande kunna läsas in utan problem
Lib := TPDFlib.Create;
try
Assert(Lib.LoadFromFile('merged-report-opt.pdf', '') = 1);
Assert(Lib.GetPageCount > 0);
finally
Lib.Free;
end;
Var passar pipelinen in i ett sammanfogningsarbetsflöde? Efter sammanfogningen, inte under den. Genom att sammanfoga först och optimera det slutgiltiga resultatet delmängdsbildas varje inbäddat typsnitt en gång mot unionen av alla använda tecken, istället för per källfil. Om genomströmningen vid sammanfogning är flaskhalsen erbjuder PDFlibPas en snabbväg på bytenivå som undviker fullständig objekttolkning, beskriven i artikeln om snabb PDF-sammanslagning med byte-referensskiftning; och för indata som är för stora för att rymmas helt i minnet täcker direktåtkomstsammanslagning och delning för stora PDF-filer den strömmande vägen. Båda alternativen passar naturligtvis ihop med ett sista optimeringssteg på de sammanfogade utdata
Vad de tre stegen inte kommer att göra
losLab PDF Librarys optimeringstrio utesluter medvetet allt som ändrar dokumentsemantiken. SubsetEmbeddedFonts förenar inte duplicerade typsnitt över sammanfogade källor till ett enda program, den krymper varje typsnitt oberoende; deduplicering är en annan, more riskfylld transformering. DownsampleImages kommer att ignorera en bild vars konservativa DPI-uppskattning förblir under tröskelvärdet även när en människa kan se att den är för stor för sin ram. Och inget av stegen rör dokumentstrukturen, så en fil som har blivit onödigt stor på grund av tusentals föräldralösa objekt behöver en omskrivningstyp av sparning snarare än dessa steg på strömnivå. Inom dessa gränser tar kombinationen av typsnittsdelmängdsbildning, bildnedsampling och LZW-till-Flate-normalisering bort de tre klassiska källorna till onödig PDF-storlek med ett förutsägbart API-anrop vardera. De tre funktionerna levereras som en del av losLab PDF Library för Delphi, C# och VB.NET, tillsammans med API:erna för sammanfogning, extrahering och rendering som diskuterats ovan