Technical Article

Hybride-referentie PDF's laden uit Word en Excel in Delphi

Open een PDF die door Microsoft Word of Excel is gegenereerd, blader erdoorheen, en niets ziet er ongewoon uit. Laad het in een Delphi-programma, lees het aantal pagina's en het aantal klopt. Maar sla het vervolgens opnieuw op met versleuteling ingeschakeld en de taak mislukt met een EListError, of de uitvoer opent met een waarschuwing over een beschadigde kruisverwijzing. Het bestand was nooit corrupt. Het is een hybride-referentiebestand, en juist de structuur waarmee een vijftien jaar oude viewer het kan openen, is de structuur die een loader die te vroeg stopt met lezen, parten speelt.

Dit is een van de meest voorkomende manieren waarop een PDF-pijplijn die alle interne tests heeft doorstaan, stuit op een bestand dat hij niet volledig kan verwerken. De invoerbestanden werden allemaal intern gegenereerd en waren dus nooit hybride. Het eerste hybride bestand komt binnen op de dag dat een klant een factuur doorstuurt die is geëxporteerd vanuit een spreadsheet.

Wat Word en Excel daadwerkelijk schrijven

ISO 32000-1 beschrijft de hybride-referentielay-out in §7.5.8.4. Een applicatie die PDF 1.5-functies wil, zoals objectstreams, maar waarmee een PDF 1.4-lezer het bestand toch kan openen, schrijft de kruisverwijzingsinformatie dubbel. Er is een klassieke kruisverwijzingstabel, de ASCII-rijen met vaste breedte waarmee elke PDF tot versie 1.4 eindigde, en er is een kruisverwijzingsstream die de rest indexeert. De trailer van de klassieke sectie bevat een /XRefStm-verwijzing waarvan de waarde de byte-offset van die stream is.

Deze taakverdeling is bewust. Objecten die een oude lezer moet kunnen bereiken, waaronder de catalogus en de paginaboom, zijn adresseerbaar vanuit de klassieke tabel. Objecten die zijn samengevoegd in gecomprimeerde objectstreams worden in de klassieke tabel als vrij gemarkeerd met een type f-verwijzing, zodat een 1.4-lezer er direct overheen springt en nooit struikelt over een structuur die hij niet kan parseren. Hun werkelijke locaties bevinden zich uitsluitend in de kruisverwijzingsstream. De signatuur van een dergelijk bestand is de staart ervan: een kort klassiek gedeelte, vaak niets meer dan xref gevolgd door een 0 0 subsectie-header, waarvan de trailer naar de /XRefStm wijst waar de daadwerkelijke herstelgegevens zich bevinden.

Waarom een correct aantal pagina's niets bewijst

Omdat de catalogus en de paginaboom bewust bereikbaar zijn vanuit de klassieke tabel, vindt een loader die alleen die tabel leest /Root, doorloopt hij de paginaboom en rapporteert hij het juiste aantal pagina's. Alles wat een oude lezer nodig heeft is aanwezig, waardoor het bestand gezond lijkt. De objecten die ontbreken zijn de objecten die in objectstreams zijn verpakt: AcroForm-veld-dictionaries, tagged-PDF-structuurelementen en de lange reeks kleine dictionaries die nooit zichtbaar hoefden te zijn voor een legacy-viewer.

U merkt het gat pas op het moment dat iets die objecten raakt, en een volledige heropslag raakt ze allemaal. Het doorlopen van het document om het opnieuw te versleutelen of te herschrijven is precies de bewerking die achtereenvolgens om elk objectnummer vraagt. Dit is de reden waarom het symptoom zich voordoet tijdens het opslaan in plaats van tijdens het laden, ver verwijderd van de werkelijke oorzaak.

De valkuil is een detector die xref ziet en stopt

De eenvoudigste manier om te bepalen hoe een bestand is geïndexeerd, is door startxref te volgen en de eerste bytes te inspecteren waarnaar het verwijst. Het trefwoord xref betekent een klassieke tabel; een stream-object betekent een kruisverwijzingsstream. Die test is correct voor elk bestand dat zich tot één methode beperkt. Het is echter fout voor een hybride bestand, waarvan de startxref naar een klassieke sectie wijst met als enig doel oude lezers tevreden te stellen, terwijl de /XRefStm in de trailer van die sectie de plaats is waar het grootste deel van het document daadwerkelijk is geïndexeerd. Een detector die "classic" retourneert bij de eerste xref die hij tegenkomt, leest /XRefStm nooit, waardoor elk object dat alleen in de stream leeft onzichtbaar wordt.

var
  Pdf: THotPDF;
  PageCount: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    PageCount := Pdf.LoadFromFile('Invoice_XLS.pdf');  // count is correct
    // inspect or edit the loaded document here
    Pdf.SaveLoadedDocument('Invoice_secured.pdf');     // walks every object
  finally
    Pdf.Free;
  end;
end;

Met de early-exit-detector op zijn plaats ziet het laden er prima uit, maar bij het opslaan kondigen de afwezige objecten zich aan. De oplossing is niet om in het begin meer bytes te lezen; het is om de hybride trailer te herkennen en /XRefStm te volgen voordat wordt besloten dat het bestand gereed is.

De volgorde van samenvoegen is cruciaal

Zodra beide indexen zijn gelezen, kunnen ze maar in één richting worden gecombineerd. De kruisverwijzingsstream moet eerst worden samengevoegd, waarna de klassieke vermeldingen eromheen worden ingevuld. De reden is de kleine misleiding die aan de basis ligt van het formaat. Een hybride bestand markeert zijn gecomprimeerde objecten als vrij in de klassieke tabel, zodat oude lezers ze negeren. Een loader die een first-seen-wins-beleid hanteert en eerst de klassieke tabel leest, registreert die objectnummers als vrij en gooit vervolgens de streamvermeldingen weg die ze daadwerkelijk lokaliseren, omdat de slots al bezet zijn. Draai de volgorde om en de type 2-vermeldingen uit de stream, elk bestaande uit een objectstreamnummer plus een index, claimen de slots die ze horen te bezitten, en de klassieke vermeldingen nestelen zich eromheen.

Dezelfde discipline voorkomt dat een oudere revisie een verwijderd object herstelt. Incrementele updates linken achterwaarts via /Prev, en een type 0 vrije vermelding is een sentinel die geeft aan dat een recentere sectie een objectnummer heeft buiten gebruik gesteld. Een latere, oudere sectie in de keten mag die sentinel niet overschrijven met een verouderde locatie. Beschouw de eerste waarneming als leidend voor vrije markeringen en het verwijderde object blijft verwijderd; ga hier onvoorzichtig mee om en de geschiedenis van het bestand zelf wekt inhoud tot leven die in de laatste revisie was verwijderd.

Wat dit betekent in HotPDF

De engine lost hybride-referentiebestanden voor u op, en doet dit op elk pad dat kruisverwijzingsgegevens moet parseren. Laad een document met LoadFromFile of LoadFromStream, breng uw wijzigingen aan en roep SaveLoadedDocument aan; of voer een eenmalige bewerking uit zoals EncryptFile die een invoer leest en een uitvoer schrijft. In beide gevallen leest het herstel /XRefStm, voegt het de streamsectie samen vóór de klassieke vermeldingen, en lost het de objecten op die in streams leven voordat de schrijfbewerking ze opsomt. Het AES-256-versleutelingspad is de plek waar het probleem zich voor het eerst aandiende, omdat het versleutelen van een document elk object herschrijft en dus vereist dat elk object al gelokaliseerd is.

// One-shot: read the hybrid input, write an AES-256 encrypted copy
Pdf.EncryptFile('Letter_DOC.pdf', 'Letter_secured.pdf',
  'owner-secret', '', aes256, [prPrint, prFillAnnotations]);

Het detail dat de moeite waard is om te onthouden, bevindt zich stroomopwaarts van de API. Bestanden die afkomstig zijn uit Word, Excel, PowerPoint en een lange lijst van "Opslaan als PDF"-pijplijnen zijn standaard hybride, dus een loader die u alleen test met de uitvoer van uw eigen generator zal er in testscenario's misschien nooit een tegenkomen. Vul uw testomgevingen met documenten die zijn geëxporteerd uit echte Office-toepassingen, en niet alleen met bestanden die door uw eigen code zijn gegenereerd.

Een verdacht bestand controleren

Twee inspecties lossen de kwestie snel op. Open het bestand in een hex-view en lees de bytes na de laatste startxref; a hybride bestand toont een kort klassiek gedeelte waarvan de trailer-dictionary /XRefStm bevat. Of vergelijk het aantal objecten dat een volledige parse rapporteert met het hoogste objectnummer dat /Size declareert in de trailer. Een groot verschil betekent dat objecten zich verbergen in streams die de loader niet heeft geopend, wat precies het tekort is dat later leidt tot een fout tijdens het opslaan.

De schrijverskant van dit verhaal, hoe objectstreams en gecomprimeerde kruisverwijzingen überhaupt worden gegenereerd, wordt behandeld in ons artikel over objectstreams en incrementele updates. Wanneer het betreffende hybride bestand ook erg groot is, kunt u dit met de laadtechnieken in de handleiding over de Direct File API voor grote PDF-workflows inspecteren zonder het volledig in het geheugen te laden. Beide sluiten aan bij het herstel dat hier wordt beschreven en dat wordt geleverd als onderdeel van de HotPDF Component voor Delphi en C++Builder alongside the loading, editing, encryption, and signing APIs covered elsewhere on this blog.