Technisch artikel

PDF-Pagina's Vervangen in Delphi Zonder Bladwijzers te Breken

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 PDF Library for Delphi 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

Vergelijkingsdiagram van PDF Library for Delphi: een bladwijzerbestemming benoemd door indirecte verwijzing overleeft een vervanging van pagina op zijn plek maar bungelt na een verwijder-en-voeg-toe-ruil
Doelen binden outlines, links en widgets aan een pagina-objectnummer, dus dat object ter plekke muteren houdt navigatie in leven, waar verwijderen-en-toevoegen lezers op pagina 1 laat belanden

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. PDF Library for Delphi 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 PDF Library for Delphi 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
    // Het document waarvan de bladwijzers en links moeten overleven
    if Lib.LoadFromFile('contract-final.pdf', '') <> 1 then
      Exit;
    TargetDoc := Lib.SelectedDocument;

    // De herziene clausulepagina, gerenderd door wat hem ook geproduceerd heeft
    if Lib.LoadFromFile('clause-7-revised.pdf', '') <> 1 then
      Exit;
    SourceDoc := Lib.SelectedDocument;

    Lib.SelectDocument(TargetDoc);
    // Bronpagina 1 overschrijft de visuals van doelpagina 3.
    // Paginaaantal, objectnummer van pagina 3, bladwijzers en annotaties blijven behouden.
    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

PDF Library for Delphi: Paginawoordenboek-anatomie die identiteitsentries waarvan het bestand afhangt scheidt van de elf visuele entries die ReplacePageRanges ruilt vanaf een geïmporteerde bronpagina
ReplacePageRanges zuivert de elf visuele sleutels en voegt ze opnieuw toe vanuit de import, terwijl objectnummer, generatie, /Parent en /Annots precies blijven zoals ze waren

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

PDF Library for Delphi: Driefasenstroom van ReplacePageRanges met tijdelijke import met objectremapping, kopiëren van visuele entries, en een preserve-referenced-unlink die overgedragen resources spaart
De bron importeren als tijdelijke pagina's laat gewone remapping eerst lopen, zodat de destructieve bewerking krimpt tot het kopiëren van visuele sleutels en het ontkoppelen van knooppunten, zonder levende bronnen terug te vorderen
var
  Replaced: Integer;
begin
  Lib.SelectDocument(TargetDoc);
  // Options = 1: bronvolgorde blijft behouden en herhalingen zijn toegestaan, dus
  // doelpagina's 5, 6 en 7 ontvangen respectievelijk bronpagina's 2, 1 en 2
  Replaced := Lib.ReplacePageRanges(SourceDoc, 5, '2,1,2', 1);
  if Replaced = 0 then
    raise Exception.CreateFmt('Replacement rejected, LastErrorCode = %d',
      [Lib.LastErrorCode]);
  // Bij succes is de selectie de eerst vervangen pagina
  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

// Postcondities die het waard zijn om in een regressietest te asserten
Lib.SelectPage(3);
// Geometrie komt nu van de bronpagina
WriteLn(Format('%.2f x %.2f', [Lib.PageWidth, Lib.PageHeight]));
// Annotaties die al op doelpagina 3 stonden blijven bevestigd
WriteLn(Lib.AnnotationCount);
// De bladwijzer die vóór de vervanging is aangemaakt lost nog steeds op naar pagina 3
WriteLn(Lib.GetOutlinePage(OutlineID));
// En het document heeft nog steeds dezelfde lengte
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 PDF Library for Delphi Delphi PDF Library voor Delphi en C++Builder, waarvan de referentiedocumentatie de volledige entry voor de paginavervangingsaanroep en zijn foutcodes bevat