Teknisk artikel

Linearisera PDF:er för Fast Web View med PDFium i Delphi

PDFium Component producerar Fast Web View-PDF:er (linjäriserade) från Delphi via TPdf.SaveAsLinearized. Metoden sparar det aktuella dokumentet med PDFium och skriver sedan om resultatet enligt ISO 32000-1 Annex F: förstasidesobjekt flyttas fram, två xref-sektioner skrivs, och sidoffset- och delade objekt-hinttabeller talar om för en visare vilka byteintervall den ska hämta för valfri sida

Skälet att någon bryr sig är nätverket. En katalog på 40 MB som serveras från objektlagring eller en dokumentportal bör visa sida ett efter de första kilobytesen, och att hoppa till sida 380 bör kosta ett par HTTP Range-förfrågningar, inte en fullständig nedladdning. Visare kan bara göra det när filen är linjäriserad. De flesta PDF:er som lämnar en redigeringspipeline är det inte, eftersom det naturliga sättet att skriva en PDF lägger xref-tabellen på slutet och utsprider sideobjekt där skrivaren råkade emittera dem

Varför kan PDFium läsa linjäriserade filer men inte skriva dem?

PDFium har en fullständig läsare för linjärisering och ingen skrivare för den. FPDFAvail_*-tillgänglighets-API:et detekterar linjäriseringsordlistan, avkodar hinttabeller och ber värden om exakt de byteintervall det behöver, vilket är vad progressiv nedladdning och avbrytningsarbetsflödet i PDFium Component bygger på. Det publika spar-ABI:t erbjuder å andra sidan bara en enkel kopiesparning och en sparning med mål-PDF-version. Ingenting i det ordnar om objekt eller emitterar hintströmmar

PDFium Component stänger det gapet utan att reimplementera vad PDFium redan gör bra. TPdf.SaveAsLinearized anropar först den befintliga SaveAs, så PDFium äger fortfarande den semantiska sparrowingen: redigerade sidor, formulärfältstillstånd och versionshantering går alla genom den kodväg som körts i åratal. Komponenten kör sedan ett Annex F-omstruktureringspass över den stabila mellanliggande PDF:en och skriver det linjäriserade resultatet till ditt mål. Att dela arbetet så här betyder att linjärisering aldrig behöver förstå en anteckning eller ett formulär, bara objekt, referenser och byteoffset

Anropa TPdf.SaveAsLinearized från Delphi

TPdf.SaveAsLinearized har två överladdningar, en för en TStream och en för ett filnamn, och båda tar samma valfria Option: TSaveOption- och PdfVersion: TPdfVersion-parametrar som SaveAs. Båda returnerar True vid framgång. Filöverladdningen skriver till en temporärfil i målkatalogen och ersätter destinationen atomärt, så en fallerad sparning lämnar aldrig en halvskriven PDF där en bra brukade ligga

var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'catalog-2026.pdf';
    Pdf.Active := True;
    // ... redigera sidor, fyll formulär, stämpla, vad pipelinen nu gör ...
    if not Pdf.SaveAsLinearized('catalog-2026-web.pdf', saNoIncremental, pv17) then
      raise Exception.Create('Linearized save failed');
  finally
    Pdf.Free;
  end;
end;

Parameterreglerna är strikta med flit. saIncremental kastar ett fel, eftersom en inkrementell uppdatering bifogar en ny revision på slutet av filen och omedelbart skulle förstöra den framladdade layouten. En målversion under PDF 1.2 avvisas också; pvUnknown behåller källversionen. Krypterad indata behöver saRemoveSecurity, eftersom skrivaren omnumrerar objekt och PDF:s objektnivå-krypteringsnycklar härleds ur objektnumret. Och eftersom linjärisering är en fullständig omskrivning ogiltigförklaras varje befintlig digital signatur. Måste du leverera signerat och linjäriserat, linjärisera först och signera efteråt

Hur skrivaren grupperar objekt för Fast Web View

Linjärisering är inte bara att flytta sida ett till toppen av filen. Skrivaren parsar den aktuella revisionen, oavsett om dess xref-data är en klassisk tabell, en hybridfil eller en xref-ström, och expanderar varje medlem i en aktiv objektström till ett fristående objekt. Den bygger sedan en referensgraf och sorterar objekten i fem grupper: dokumentnivåobjekt som behövs för att öppna filen, förstasidesobjekt, objekt privata för en senare sida, objekt delade av flera senare sidor, och allt annat

Fällan i det grafvandrandet är /Parent. Varje sidordlista pekar tillbaka uppåt i sidträdet, och en naiv traversering följer den pekaren till föräldernoden, sedan ned genom /Kids in i varje syskonsida och dess content streams. Ett steg senare innehåller förstasidesgruppen hela dokumentet, och filen är tekniskt linjäriserad men värdelös för range-först-åtkomst. PDFium Component ignorerar sidans /Parent-länk och sidträdsnoderna utanför sidan som samlas in, vilket är det som håller förstasidessektionen liten. På ett regressionsprov på 51 sidor med ett djupt sidträd är förstasidessektionen 5289 bytes, 0,48 % av den 1105354 byte stora utdatan

Objektströmmar och xref-strömmar i inmatan plattas ut till vanliga indirekta generation 0-objekt, varje referens skrivs om till de nya numren, och utdatan använder klassiska xref-sektioner. Det kostar en del filstorlek jämfört med komprimerade objektströmmar, men det förenklar hinttabellernas täckning och är layouten varje linjäriseringsmedveten visare hanterar. Spelar kompakthet större roll än Fast Web View för en given utdata har PDFium Component en separat objektström-skrivare för det jobbet

Vad hamnar i linjäriseringsordlistan och de två xref-sektionerna?

En linjäriserad fil skriven av PDFium Component bär två klassiska xref-sektioner och en linjäriseringsordlista i det allra första objektet. Den första xref-sektionen följer linjäriseringsordlistan och täcker dokumentnivå- och förstasidesobjekten, så en visare kan rendera sida ett enbart ur filens framsida. Huvud-xref-sektionen sitter på slutet och täcker allt annat

Linjäriseringsordlistan själv håller de tal en visare behöver innan den läser något annat: /L fillängden, /H hintströmmens offset och längd, /O första sidobjektets nummer, /E slutet på förstasidessektionen, /N sidantalet, och /T offseten av första posten i huvud-xref-tabellen. Alla de där beror på den slutliga layouten, och ordlistan sitter före layouten. Att skriva den med naturligbreddstal skapar en loop: en längd som växer från 999999 till 1000000 lägger till en byte, förskjuter varje följande offset, vilket kan ändra ett annat tals bredd, och så vidare

PDFium Component bryter loopen genom att reservera fasta 320 bytes för linjäriseringsordlistan. Varje objekt, hintströmmen, båda xref-sektionerna och trailer:na skrivs först; när alla offset är slutgiltiga fylls ordlistan i på plats och paddas till den reserverade bredden. En subtil detalj är /T: den måste peka på objekt 0-posten i huvud-xref-tabellen, inte på xref-nyckelordet som föregår den. Strikta validatorer flaggar nyckelordsoffseten fastän tillgivna visare öppnar filen ändå

Varför ignorerar hinttabellernas offset hintströmmen?

Hinttabellernas offset i en linjäriserad PDF tolkas som om hintströmmen inte fanns i filen. Annex F definierar det så för att hintströmmen kan sitta mellan förstasidessektionen och resten utan att sin egen storlek matar tillbaka in i talen den innehåller. Den praktiska konsekvensen för en skrivare är lätt att få fel: varje verklig offset som ligger efter hintströmmen måste reduceras innan den går in i sidoffset- eller delade objekt-hinttabellen

Frågan är reducerad med hur mycket. Det frestande svaret är längden på strömdata, eftersom det är delen som känns som hintinnehållet. Det korrekta svaret är längden på hela det hintindirekta objektet, inklusive N 0 obj, strömordlistan, nyckelorden stream och endstream, och endobj. Subtrahera bara datalängden och varje senare offset ligger fel med några dussin bytes. Sida ett renderas fortfarande eftersom den bor före hintströmmen, så buggen överlever en snabb visuell kontroll och dyker bara upp när en visare hoppar till en senare sida och begär ett intervall som börjar mitt i fel objekt

Båda hinttabellerna är bitpackade med mest signifikanta bit först, återigen enligt Annex F. Komponenten verifierar resultatet som en riktig klient skulle göra: ett glest intervalllager levererar bara de första 1 KiB av 51-sidorsprovet, och PDFium:s inbyggda hantering av hintar begär resten. Den sista sidan öppnas utan att hela filen någonsin funnits tillgänglig

Verifiera en linjäriserad PDF i Delphi

Den billigaste kontrollen är att fråga PDFium självt. TPdf.InspectAvailability kör tillgänglighets-API:et mot en ström utan att ladda dokumentet, och dess Linearization-fält rapporterar plnLinearized, plnNotLinearized eller plnUnknown

function IsFastWebView(const FileName: string): Boolean;
var
  Stream: TFileStream;
  Info: TPdfAvailability;
begin
  Stream := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
  try
    Info := TPdf.InspectAvailability(Stream);
    Result := Info.Linearization = plnLinearized;
  finally
    Stream.Free;
  end;
end;

Det bekräftar att PDFium känner igen layouten; det bevisar inte att hintarna är korrekta. För ett riktigt end-to-end-test, servera filen genom en intervallbegränsad transport och öppna en sen sida med TPdfProgressiveDocument, på samma sätt som guiden till att streama stora PDF:er på begäran med PDFium VCL gör. Renderas sidan efter att bara ha hämtat en liten andel av filen gör hinttabellerna sitt jobb. Att lägga till en andra oberoende linjäriseringskontroll till releasepipelinen är också värt det, eftersom visare skiljer sig i hur tillgivna de är om en felplacerad /T eller en lätt felaktig offset

Två begränsningar att hålla i minnet. Objektmaterialiseraren avvisar fortfarande arrayer på 2 GiB eller mer, så extremt stora dokument behöver en annan strategi för närvarande. Och linjärisering lönar sig bara när servern hedrar HTTP Range-förfrågningar; bakom en proxy som stjälper Accept-Ranges laddas en linjäriserad fil ner exakt som vilken annan som helst

FAQ

Håller TPdf.SaveAsLinearized digitala signaturer giltiga?

Nej. TPdf.SaveAsLinearized omnumrerar och skriver om varje objekt, så befintliga signaturer ogiltigförklaras. Linjärisera först, applicera sedan signaturen

Kan PDFium Component linjärisera en krypterad PDF?

Bara när du skickar saRemoveSecurity. Objektnivå-krypteringsnycklar beror på objektnummer, som skrivaren ändrar, så utdatan skrivs utan kryptering

Vilka PDF-versioner stöder linjäriserad utdata?

TPdf.SaveAsLinearized accepterar pvUnknown för att behålla källversionen eller vilket mål som helst från PDF 1.2 och uppåt, och avvisar saIncremental eftersom en bifogad revision skulle bryta layouten

Fast Web View-utdata, progressiv inläsning och tillgänglighets-API:et är alla en del av PDFium Component for Delphi and C++Builder, som wrappar PDFium för Delphi-, C++Builder- och Lazarus-projekt