Teknisk artikel

Redigera en PDF för hand i en textredigerare, reparera den sedan

HotPDF kan spara ett inläst dokument i en diagnostisk form avsedd att läsas och redigeras av en människa, och sedan reparera filen efter att du har redigerat den. SaveLoadedDocumentDiagnostic skriver dokumentet med deterministisk ordlisteordning, varje objekt som ett vanligt indirekt objekt, inga objektströmmar, ingen korsreferensström och ingen linjärisering. RepairDiagnosticDocument återuppbygger sedan byteoffseten, strömlängderna, korsreferenstabellen och trailern som din redigering ogiltigförklarade

Detta par existerar för ett arbetsflöde: att förstå eller fixa vad som faktiskt finns inuti en PDF. Varje utvecklare som har felsökt ett renderingsproblem har velat öppna filen, ändra en siffra i en ordlista, och titta på resultatet. I en normal PDF är det omöjligt, eftersom innehållet är komprimerat till objektströmmar och varje byte du infogar förskjuter offset som en korsreferenstabell registrerar exakt

Vad skiljer en diagnostisk sparning?

Sex inställningar ändras tillsammans. Ordlistenycklar skrivs i deterministisk ordning, så två sparningar av samma dokument ger jämförbara filer. Diagnostiskt utdataläge annoterar strukturen. Fullständig omskrivning tvingas fram, så ingenting förs över som en orörd inkrementell revision. Linjärisering är avstängd, eftersom en linjäriserad fil är organiserad för en webbvisare snarare än för en läsare. Korsreferensströmmar är avstängda, så tabellen är ren text. Och objektströmmar är avstängda, så inget objekt döljs inuti en komprimerad behållare

Resultatet är en fil vars sidhuvud bär en diagnostisk markör vid sidan av den vanliga %PDF--signaturen, och vars kropp du kan bläddra genom i valfri redigerare. Objekt visas ett efter ett, ordlistor är läsbara, och innehållsströmmar ligger där du kan hitta dem. Reparationsverktyget kontrollerar efter den markören och avvisar filer som saknar den, eftersom en normal PDF-fils kompakta struktur inte är något en byteintervallreparatör bör försöka tolka

Diagram som kontrasterar en vanlig produktions-PDF-sparning med en HotPDF-diagnostisk sparning som skriver klartextläsbara objekt och en klartexttabell för korsreferenser som du kan öppna i valfri textredigerare
En vanlig sparning döljer objekt inuti komprimerade strömmar medan den diagnostiska skrivaren lägger ut varje objekt ett per block med en klartext-korsreferenstabell. Utdatan är en fil du kan bläddra genom och ändra i valfri redigerare
uses
  HPDFDoc;

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('invoice.pdf') <= 0 then
      Exit;
    // Läsbar, ett objekt per block, ingenting dolt i objektströmmar
    Pdf.SaveLoadedDocumentDiagnostic('invoice-diagnostic.pdf');
  finally
    Pdf.Free;
  end;
end;

Varför förstör redigering filen, och vad måste återuppbyggas?

Tre saker i en PDF kodar byteplaceringar eller byteantal. Korsreferenstabellen mappar varje objektnummer till ett byteoffset. Värdet startxref i slutet av filen pekar mot tabellen. Och varje strömordlista bär en /Length som anger hur många byte strömkroppen upptar

Ändra ett tecken var som helst före filens slut och offseten efter det blir fel. Lägg till en rad i en innehållsström och dess /Length blir också fel. De flesta visare kommer att vägra filen, och de som återställer den gör det genom skanning, vilket döljer misstaget snarare än att åtgärda det. Reparation återuppbygger alla tre utifrån vad byten faktiskt nu säger

Diagram som visar hur en handredigering förskjuter byteoffset så att korsreferenstabellen, startxref-pekaren och strömlängderna blir inaktuella tills RepairDiagnosticDocument återuppbygger alla tre utifrån den faktiska layouten
Att lägga till byte flyttar varje efterföljande offset, så registrerade positioner, startxref-pekaren och deklarerade strömlängder slutar matcha verkligheten. Reparation återuppbygger var och en utifrån vad den redigerade filen faktiskt nu innehåller
var
  Options: THPDFDiagnosticRepairOptions;
  Report: THPDFDiagnosticRepairReport;
begin
  Options := THPDFDiagnosticRepairOptions.Default;
  Options.VerifyOutput := True;         // ladda resultatet innan det ersätts

  if THotPDF.RepairDiagnosticDocument('invoice-diagnostic.pdf',
       'invoice-fixed.pdf', Options, Report) then
    Writeln(Format('%d objects, %d streams, %d lengths corrected, ' +
      '%d xref rows rebuilt, %d pages verified',
      [Report.ObjectCount, Report.StreamCount,
       Report.RepairedStreamLengthCount, Report.RebuiltXRefEntryCount,
       Report.VerifiedPageCount]))
  else
    Writeln(Format('repair failed (%d): %s',
      [Ord(Report.Status), string(Report.Diagnostic)]));
end;

Kopiera byte, omserialisera inte

Designbeslutet som gör detta verktyg pålitligt är att det aldrig laddar dokumentet och skriver ut det på nytt. Det kopierar byteintervallen för objekt det har validerat, ändrar bara de /Length-värden som är fel, och återuppbygger tabellen, trailern och startxref runt dem

Att ladda och omserialisera skulle vara betydligt enklare att implementera och skulle tyst ändra saker. Okänd objektsyntax skulle normaliseras, strängundantag skulle skrivas om i skrivarens föredragna form, och redan kodade strömdata skulle kunna kodas om. För ett felsökningsverktyg är det oacceptabelt: du redigerade en siffra, och diffen bör visa en siffra, inte tusen tillfälliga formateringsändringar. Om du vill veta exakt vad som ändrats mellan två filer är det ett jämförelseproblem, som täcks i att jämföra två PDF-filer efter struktur och pixlar

Diagram som jämför HotPDF-reparation som kopierar validerade byteintervall ordagrant mot en ladda-och-omserialisera-väg som normaliserar syntax, skriver om strängundantag och kodar om strömdata
Reparation kopierar validerade intervall byte-för-byte och patchar bara de /Length-värden som är fel, så din handgjorda diff överlever intakt. Att ladda om och omserialisera skulle tyst skriva om syntax, undantag och kodningar du aldrig rörde

De två parsningsreglerna som förhindrar falska objekt

En byteintervallskanner möter en uppenbar fälla: text som ser ut som ett objektsidhuvud kan förekomma inuti en strömlast eller inuti en sträng. En naiv skanning skulle avge en korsreferensrad som pekar in i mitten av en innehållsström, och den resulterande filen skulle vara trasig på ett nytt och mer förvirrande sätt

HotPDF undviker detta genom att hoppa över hela byteintervallet för varje validerat objekt, med hjälp av dess registrerade slutoffset, så att ingenting inuti en objektkropp någonsin betraktas som en kandidat. Mellan objekt tillåts bara PDF-blanktecken och kommentarer; allt annat betyder att regionen inte är förstådd och dess byte får inte dras in i utdata

Strömlängder kräver lika stor omsorg. Den verkliga lastlängden exkluderar radslutsavgränsaren som en skrivare infogar före endstream. När den deklarerade längden landar exakt på markören accepteras den deklarationen. När den inte gör det utför reparatören en begränsad sökning efter paret endstream och endobj och subtraherar exakt en avgränsare, ett CR, ett LF eller ett CRLF. Att göra detta fel med en byte ger en fil som öppnas och renderar en sida med en vilsen operator i slutet av sin innehållsström

Vad trailern får behålla

Den återuppbyggda trailern är inte en kopia av den gamla. /Size, /Prev och /XRefStm beskriver alla den tidigare fillayouten och beräknas om eller släpps. Endast poster vars mål har validerats överlever: /Info, /ID och /Encrypt. Och /Root måste lösas till exakt ett Catalog-objekt — inte noll, och inte flera, vilket är vad en fil sammansatt genom att klippa ihop objektblock brukar producera

Utdatasäkerhet hanteras på det sätt det bör för ett verktyg som körs mot filer folk bryr sig om. Resultatet skrivs till en temporär fil i målkatalogen, verifieras sedan genom att laddas via den normala vägen med korsreferensåterställning avstängd, så att en fil som bara öppnas tack vare återställning räknas som ett misslyckande. Först efter att sidträdet verifierats ersätter den temporära filen atomärt målet. En misslyckad reparation skriver aldrig över en befintlig utdatafil

Budgetar och ärliga felkoder

Tre gränser begränsar arbetet: indatabyte, kandidatobjektantal och maximalt objektnummer, som standard 8 GiB, två miljoner respektive två miljoner. Objektnummertaket spelar större roll än det verkar, eftersom en gles tabell med några få objekt numrerade i miljarder annars skulle blåsa upp den återuppbyggda korsreferenstabellen enormt. Klassiska korsreferenstabeller avvisar också offset som skulle behöva mer än tio decimalsiffror, vilket är en formatbegränsning snarare än ett implementationsval

Misslyckanden rapporterar en specifik status snarare än ett booleskt värde: ogiltiga argument, källan för stor, ingen diagnostisk fil, objektgräns överskriden, ogiltigt objekt, dubblerat objekt, saknad katalog, ogiltig trailer, utdatafel, eller validering misslyckades. Var och en motsvarar en annan åtgärd. Ett dubblerat objekt betyder att du klistrade in ett objektblock två gånger. En saknad katalog betyder vanligtvis att objektet som håller /Type /Catalog skadades. Validering misslyckades betyder att strukturen nu är konsekvent men att sidträdet inte laddas, vilket vanligtvis är en /Kids-array redigerad utan att matcha sin /Count

Två praktiska anmärkningar sluter cirkeln. Behåll den diagnostiska filen, inte bara den reparerade, så att nästa redigering startar från en läsbar bas. Och skicka inte diagnostisk utdata till slutanvändare: den är större än en normal sparning eftersom ingenting komprimeras till objektströmmar, och produktionsvägen för en kompakt fil är den vanliga sparvägen som beskrivs i objektströmmar och inkrementella uppdateringar. När målet är att hitta vad som är fel snarare än att ändra det, svarar den automatiserade vägen i automatisering av preflight-rapporter snabbare än att läsa byte

Diagnostisk sparning, reparation, preflight och jämförelse arbetar alla på samma inlästa dokumentmodell i Delphi och C++Builder; den fullständiga funktionslistan finns på HotPDF Delphi PDF-komponentsidan