Het vervangen van pagina 3 van een afgetekend contract zou de inhoudsopgave niet moeten verplaatsen. Verwijder de oude pagina, voeg de nieuwe in, en elke bladwijzer die er vroeger naar wees, komt nu ergens anders terecht. De PDFlibPas Delphi PDF library vermijdt dit door het doelpaginaobject zelf te behouden en alleen de entries over te dragen die visuele inhoud dragen
Waarom breken bladwijzers na het vervangen van een PDF-pagina?
Bladwijzers breken omdat een PDF-bestemming een pagina benoemt via een indirecte objectreferentie, niet via paginanummer. ISO 32000-1 §12.3.2.2 definieert een expliciete bestemming als een array waarvan het eerste element een indirecte referentie naar het paginaobject is. Verwijder dat object en voeg een vervanging toe, en de referentie hangt in de lucht: de meeste viewers reageren door de lezer op pagina 1 te laten landen, wat precies het symptoom is dat mensen rapporteren na een verwijder-dan-invoeg-vervanging. De paginaboom ziet er perfect uit, het paginaaantal klopt, de rendering klopt, en de hele navigatielaag is stilletjes verkeerd
Named destinations redden je ook niet. §12.3.2.3 leidt een naam door de /Dests-naamboom in de documentcatalog, maar het blad waar die naam naar oplost is nog steeds een expliciete-bestemming-array die dezelfde paginareferentie bevat. Naamgeving voegt een laag indirectie toe boven de paginareferentie, niet eromheen. Dezelfde redenering bestrijkt de rest van de interactieve laag beschreven in §12.5: een linkannotatie draagt een /Dest of een /A GoTo-actie waarvan de /D die array is, elke annotatie kan een /P-entry dragen die een indirecte referentie naar zijn pagina is, en een formuliervelden-widget is een annotatie op precies dezelfde voet. Eén naïeve paginawissel ontkoppelt vier subsystemen tegelijk, en als je ze op een echt bestand wilt zien opgesomd, is dezelfde objectgraaf wat outline- en annotatie-introspectie doorloopt
Welke paginaentries dragen identiteit en welke dragen uiterlijk
Een paginadictionary mengt twee soorten entries, en een in-place vervanging slaagt precies wanneer je ze scheidt. De uiterlijkkant is eindig en opsombaar: /Contents, /Resources, de vijf paginaboxen /MediaBox, /CropBox, /BleedBox, /TrimBox en /ArtBox, plus /Rotate, /Group, /UserUnit en /BoxColorInfo. Die elf entries bepalen alles wat een rasterizer voor de pagina produceert, en niets anders in het bestand verwijst er bij naam naar
De identiteitskant is waaraan de rest van het document zich gebonden heeft: het paginaobjectnummer en de generatie, de /Parent-terugkoppeling naar de paginaboom, en /Annots. PDFlibPas houdt elk daarvan onaangeroerd. ReplacePageRanges verwijdert de elf visuele entries uit het doelpaginadictionary en voegt ze opnieuw toe vanuit de geïmporteerde bronpagina, zodat het doelpaginaobject in-place gemuteerd wordt in plaats van vervangen. De door §7.7.3 vereiste paginaboomstructuur blijft ook byte-identiek van vorm: /Kids-volgorde, /Count, en elke overlevende /Parent zijn hetzelfde vóór en na, omdat er nooit een node ontkoppeld werd
Hoe vervangt PDFlibPas een pagina zonder objecten te hernummeren?
De aanroep neemt een brondocument, een op 1 gebaseerde doel-startpagina, een bronbereik-expressie, en een optievlag. Beide documenten moeten geopend zijn in dezelfde instantie, en het doeldocument is het geselecteerde. Omdat het doelpaginaaantal nooit verandert, moet het bereik dat je opvraagt passen binnen het document vanaf TargetStartPage, en dat wordt gecontroleerd voordat er iets aangemaakt wordt
var
Lib: TPDFlib;
TargetDoc, SourceDoc: Integer;
begin
Lib := TPDFlib.Create;
try
// The document whose bookmarks and links must survive
if Lib.LoadFromFile('contract-final.pdf', '') <> 1 then
Exit;
TargetDoc := Lib.SelectedDocument;
// The revised clause page, rendered by whatever produced it
if Lib.LoadFromFile('clause-7-revised.pdf', '') <> 1 then
Exit;
SourceDoc := Lib.SelectedDocument;
Lib.SelectDocument(TargetDoc);
// Source page 1 overwrites the visuals of target page 3.
// Page count, page 3 object number, bookmarks and annotations are kept.
if Lib.ReplacePageRanges(SourceDoc, 3, '1', 0) = 1 then
Lib.SaveToFile('contract-final.pdf');
finally
Lib.Free;
end;
end;
Intern kunnen de bronpagina's niet zomaar over documentgrenzen heen gelezen worden, omdat elke indirecte referentie erbinnen tot de bron-objectnummering behoort. Dus wordt het bronbereik eerst op de gewone manier geïmporteerd, als tijdelijke pagina's toegevoegd na de laatste echte pagina, wat de volledige objectgraaf-remapping laat draaien: content streams, fonts, XObjects, shadings en kleurruimtes worden allemaal hernummerd naar het doeldocument. Pas daarna worden de elf visuele entries gekopieerd van elke tijdelijke pagina naar zijn doelpagina, en pas daarna worden de tijdelijke pagina's ontkoppeld van de paginaboom. Het remapping-werk gebeurt waar het goedkoop en veilig is, en de destructieve bewerking wordt gereduceerd tot een dictionary-niveau-wissel op pagina's die al bestaan
Het verwijderpad dat zou vernietigen wat je zojuist overgedragen hebt
Het verwijderen van die tijdelijke pagina's is de stap die triviaal lijkt en dat niet is. Het gewone paginaverwijderingspad in de library doet meer dan een node ontkoppelen: het combineert de lagen van elke te verwijderen pagina, leegt de eerste content stream, en heroogst resources die geen andere pagina deelt. Dat is correct gedrag voor een echte verwijdering, en catastrofaal hier, omdat de doelpagina's tegen de tijd dat de tijdelijke pagina's verwijderd worden al precies naar die content streams en resource-objecten verwijzen. Ze leegmaken zou de pagina die je zojuist vervangen hebt leegmaken, en de resource-veeg zou fonts en afbeeldingen opruimen die nu een levende eigenaar hebben
De oplossing is een preserve-referenced-objects-modus op het interne verwijderpad. Wanneer die ingesteld is, slaat de verwijdering zowel de niet-gedeelde-resource-veeg als het leegmaken van de content stream over, en doet niets anders dan de pagina's van de paginaboom loskoppelen en de boekhouding van de boom repareren. De overgedragen objecten overleven met een nieuwe eigenaar, en het objecteigenaarschap na de bewerking is wat je op een whiteboard zou tekenen: één content stream, één eigenaarspagina, één objectnummer dat nooit verplaatst is. De gerelateerde levenscyclusregels voor het aanmaken, verwijderen en herordenen van pagina's worden apart behandeld in de notities over document- en paginalevenscyclus-bewerkingen
Volgorde, duplicaten, en alles-of-niets falen
De optievlag selecteert hoe het bronbereik geïnterpreteerd wordt. 0 sorteert de geparste paginanummers en verwijdert duplicaten, wat de verstandige standaard is wanneer de aanroeper iets als '4-6,2' doorgeeft en simpelweg die vier pagina's bedoelt. 1 behoudt de volgorde die je schreef en staat toe dat een pagina herhaald wordt, dus '2,1,2' betekent daadwerkelijk drie vervangingen genomen uit twee bronpagina's. Validatie draait eerst en draait volledig: de bereiksyntaxis, elk paginanummer tegen het bronpaginaaantal, de optiewaarde zelf, en de doelcapaciteit worden allemaal gecontroleerd voordat er één object aangemaakt wordt. Een afgewezen aanroep zet LastErrorCode op 412, herstelt de eerder geselecteerde pagina, en laat het document exact zoals het was
var
Replaced: Integer;
begin
Lib.SelectDocument(TargetDoc);
// Options = 1: source order is preserved and repeats are allowed, so
// target pages 5, 6 and 7 receive source pages 2, 1 and 2 respectively
Replaced := Lib.ReplacePageRanges(SourceDoc, 5, '2,1,2', 1);
if Replaced = 0 then
raise Exception.CreateFmt('Replacement rejected, LastErrorCode = %d',
[Lib.LastErrorCode]);
// On success the selection is the first replaced page
Assert(Lib.SelectedPage = 5);
end;
Atomiciteit reikt voorbij validatie tot in de overdracht zelf. Voordat de eerste bronpagina geïmporteerd wordt, worden de elf visuele entries van elke doelpagina in het bereik als gecodeerde waarden vastgelegd. Als de import faalt, of het geïmporteerde paginaaantal komt niet overeen met wat opgevraagd werd, worden de vastleggingen teruggedecodeerd naar de doelpagina's en worden de tijdelijke pagina's verwijderd, zodat een halverwege mislukte poging nog steeds de originele visuals op hun originele objecten achterlaat. Dat is belangrijker dan het klinkt: een half vervangen paginabereik in een contract is erger dan een mislukte aanroep, omdat niets in het bestand het als half gedaan markeert
// Post-conditions worth asserting in a regression test
Lib.SelectPage(3);
// Geometry now comes from the source page
WriteLn(Format('%.2f x %.2f', [Lib.PageWidth, Lib.PageHeight]));
// Annotations that were already on target page 3 are still attached
WriteLn(Lib.AnnotationCount);
// The bookmark created before the replacement still resolves to page 3
WriteLn(Lib.GetOutlinePage(OutlineID));
// And the document is still the same length
WriteLn(Lib.PageCount);
Wat doet in-place vervanging nog steeds niet voor je?
Bronannotaties, bronformuliervelden en bronoutlines worden bewust niet geïmporteerd. Een widget overbrengen zonder zijn /AcroForm-veldentry, of een annotatie met marked-content zonder zijn structuurboom-eigenaarschap, produceert een half-geïmporteerd interactief object waar geen enkele viewer chocola van kan maken, dus draagt de bewerking alleen uiterlijk over. Het praktische gevolg is dat als de vervangende pagina nieuwe formuliervelden of nieuwe links moet dragen, je die er achteraf aan toevoegt, tegen het doelpaginaobject dat daar nog steeds op ze zit te wachten
Nog twee grenzen zijn het waard om op je eigen bestanden te controleren. Ten eerste, /Annots wordt behouden maar paginageometrie niet, dus het vervangen van een pagina van 220 mm door een pagina van 320 mm houdt annotatierechthoeken op hun oude coördinaten binnen een anders gedimensioneerde /MediaBox; als de geometrie verandert, herpositioneer dan de annotaties die je behield. Ten tweede blijven entries buiten de elf visuele sleutels bij de doelpagina volgens ontwerp, wat juist is voor /Trans of /AA en verouderd voor /Thumb, dus regenereer thumbnails na een vervanging. Getagde documenten vergen nog één extra overweging: de structuurelementen wijzen nog steeds via /Pg naar het juiste paginaobject, maar hun marked-content-identifiers beschrijven inhoud die er niet meer is, dus is een paginawissel binnen een PDF/UA-workflow zowel een structuurboom-bewerking als een inhoudsbewerking. Als je taak eigenlijk compositie is in plaats van wisselen, artwork lagen over pagina's die je behoudt, is de page-stitching- en template-aanpak het goedkopere gereedschap
Alles wat hier beschreven wordt, inclusief de bereikexpressiesyntaxis, de optiewaarden en de omringende paginabewerkings-API, wordt geleverd in de standaard PDFlibPas Delphi PDF Library voor Delphi en C++Builder, waarvan de referentiedocumentatie de volledige entry voor de paginavervangingsaanroep en zijn foutcodes bevat