Teknisk artikel

Ersätt PDF-sidor i Delphi utan att förstöra bokmärken

Att ersätta sida 3 i ett undertecknat avtal bör inte flytta innehållsförteckningen. Ta bort den gamla sidan, infoga den nya, och varje bokmärke som brukade peka dit hamnar nu någon annanstans. PDFlibPas Delphi PDF-bibliotek undviker detta genom att behålla själva målsidans objekt och bara överföra de poster som bär visuellt innehåll

Varför går bokmärken sönder efter att en PDF-sida ersatts?

Bokmärken går sönder eftersom en PDF-destination namnger en sida via indirekt objektreferens, inte via sidnummer. ISO 32000-1 §12.3.2.2 definierar en explicit destination som en array vars första element är en indirekt referens till sidobjektet. Ta bort det objektet och lägg till en ersättning, och referensen hänger löst: de flesta läsare svarar genom att lämna läsaren på sida 1, vilket är precis det symptom folk rapporterar efter en ersättning i stil med ta-bort-och-infoga. Sidträdet ser perfekt ut, sidantalet stämmer, renderingen är korrekt, och hela navigeringslagret är tyst felaktigt

Namngivna destinationer räddar dig inte heller. §12.3.2.3 dirigerar ett namn genom namnträdet /Dests i dokumentkatalogen, men det löv som namnet löses upp till är fortfarande en explicit destinationsarray som håller samma sidreferens. Namngivning lägger till ett indirektionslager ovanpå sidreferensen, inte runt den. Samma resonemang täcker resten av det interaktiva lagret som beskrivs i §12.5: en länkanteckning bär en /Dest eller en /A GoTo-åtgärd vars /D är den arrayen, varje anteckning kan bära en /P-post som är en indirekt referens till dess sida, och en formulärfältswidget är en anteckning på exakt samma villkor. Ett naivt sidbyte kopplar loss fyra delsystem på en gång, och om du vill se dem uppräknade på en riktig fil är det samma objektgraf som disposition- och anteckningsintrospektion vandrar genom

Vilka sidposter bär identitet och vilka bär utseende

En sidordbok blandar två sorters poster, och en ersättning på plats lyckas precis när man skiljer dem åt. Utseendesidan är ändlig och uppräkningsbar: /Contents, /Resources, de fem sidrutorna /MediaBox, /CropBox, /BleedBox, /TrimBox och /ArtBox, plus /Rotate, /Group, /UserUnit och /BoxColorInfo. Dessa elva poster avgör allt en rasteriserare producerar för sidan, och inget annat i filen pekar på dem via namn

Identitetssidan är det resten av dokumentet har bundit sig till: sidobjektets nummer och generation, återlänken /Parent in i sidträdet, och /Annots. PDFlibPas lämnar var och en av dem orörda. ReplacePageRanges rensar bort de elva visuella posterna från målsidans ordbok och lägger till dem igen från den importerade källsidan, så att målsidans objekt muteras på plats snarare än ersätts. Sidträdsstrukturen som krävs av §7.7.3 förblir också byte-identisk i form: /Kids-ordning, /Count, och varje kvarvarande /Parent är desamma före och efter, eftersom ingen nod någonsin kopplades loss

Hur ersätter PDFlibPas en sida utan att numrera om objekt?

Anropet tar ett källdokument, en 1-baserad målstartsida, ett källintervalluttryck och en alternativflagga. Båda dokumenten måste vara öppna i samma instans, och måldokumentet är det valda. Eftersom målets sidantal aldrig ändras måste intervallet du begär rymmas inom dokumentet med start vid TargetStartPage, och det kontrolleras innan något skapas

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;

Internt kan källsidorna inte helt enkelt läsas över dokumentgränser, eftersom varje indirekt referens inuti dem tillhör källans objektnumrering. Så källintervallet importeras först på det vanliga sättet, som temporära sidor tillagda efter den sista riktiga sidan, vilket kör den fullständiga ommappningen av objektgrafen: innehållsströmmar, teckensnitt, XObjects, skuggningar och färgrymder numreras alla om till måldokumentet. Först därefter kopieras de elva visuella posterna från varje temporär sida till dess målsida, och först därefter kopplas de temporära sidorna loss från sidträdet. Ommappningsarbetet sker där det är billigt och säkert, och den destruktiva redigeringen reduceras till ett byte på ordboksnivå på sidor som redan finns

Borttagningsvägen som skulle förstöra det du just överfört

Att ta bort de temporära sidorna är steget som ser trivialt ut och inte är det. Bibliotekets vanliga sidborttagningsväg gör mer än att koppla loss en nod: den slår samman lagren för varje sida som tas bort, tömmer den första innehållsströmmen och återvinner resurser som ingen annan sida delar. Det är korrekt beteende för en riktig borttagning, och katastrofalt här, eftersom målsidorna vid den tidpunkt då de temporära sidorna tas bort redan refererar till exakt de innehållsströmmarna och resursobjekten. Att tömma dem skulle tömma sidan du just ersatte, och resurssopningen skulle samla in teckensnitt och bilder som nu har en levande ägare

Lösningen är ett läge som bevarar refererade objekt på den interna borttagningsvägen. När det är satt hoppar borttagningen över både sopningen av odelade resurser och rensningen av innehållsströmmen, och gör ingenting mer än att koppla loss sidorna från sidträdet och fixa till trädets bokföring. De överförda objekten överlever med en ny ägare, och objektägandet efter operationen är det du skulle rita på en whiteboard: en innehållsström, en ägande sida, ett objektnummer som aldrig flyttades. De relaterade livscykelreglerna för att skapa, ta bort och ordna om sidor täcks separat i anteckningarna om dokument- och sidlivscykeloperationer

Ordning, dubbletter och allt-eller-inget-misslyckande

Alternativflaggan väljer hur källintervallet tolkas. 0 sorterar de tolkade sidnumren och tar bort dubbletter, vilket är det förnuftiga standardvalet när anroparen skickar något i stil med '4-6,2' och helt enkelt menar de fyra sidorna. 1 bevarar den ordning du skrev och tillåter att en sida upprepas, så '2,1,2' betyder verkligen tre ersättningar hämtade från två källsidor. Validering körs först och körs fullständigt: intervallsyntaxen, varje sidnummer mot källans sidantal, själva alternativvärdet och målets kapacitet kontrolleras alla innan ett enda objekt skapas. Ett avvisat anrop sätter LastErrorCode till 412, återställer den tidigare valda sidan och lämnar dokumentet exakt som det var

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;

Atomiciteten sträcker sig bortom valideringen och in i själva överföringen. Innan den första källsidan importeras tas ett ögonblicksavtryck av de elva visuella posterna för varje målsida i intervallet, som kodade värden. Om importen misslyckas, eller om det importerade sidantalet inte matchar det som begärdes, avkodas ögonblicksavtrycken tillbaka till målsidorna och de temporära sidorna tas bort, så ett misslyckande mitt i flödet lämnar ändå det ursprungliga utseendet på plats på sina ursprungliga objekt. Det spelar större roll än det låter: ett halvt ersatt sidintervall i ett avtal är värre än ett misslyckat anrop, eftersom ingenting i filen markerar det som halvfärdigt

// 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);

Vad gör ersättning på plats fortfarande inte åt dig?

Källanteckningar, källformulärfält och källdispositioner importeras medvetet inte. Att föra över en widget utan dess /AcroForm-fältpost, eller en anteckning som bär märkt innehåll utan dess ägarskap i strukturträdet, ger ett halvimporterat interaktivt objekt som ingen läsare kan resonera kring, så operationen överför bara utseende. Den praktiska konsekvensen är att om ersättningssidan är tänkt att bära nya formulärfält eller nya länkar lägger du till dem på målsidan efteråt, mot det målsidesobjekt som fortfarande sitter kvar och väntar på dem

Ytterligare två gränser är värda att kontrollera på dina egna filer. För det första bevaras /Annots men sidgeometrin gör det inte, så att ersätta en 220 mm-sida med en 320 mm-sida håller anteckningsrektanglar kvar på sina gamla koordinater inuti en olika stor /MediaBox; om geometrin ändras, positionera om de anteckningar du behöll. För det andra stannar poster utanför de elva visuella nycklarna med målsidan by design, vilket är rätt för /Trans eller /AA och inaktuellt för /Thumb, så regenerera miniatyrer efter en ersättning. Taggade dokument behöver en extra tanke: strukturelementen pekar fortfarande på rätt sidobjekt via /Pg, men deras identifierare för märkt innehåll beskriver innehåll som inte längre finns där, så ett sidbyte inuti ett PDF/UA-arbetsflöde är en redigering av strukturträdet lika mycket som en innehållsredigering. Om ditt jobb egentligen är sammansättning snarare än utbyte, att lägga grafik ovanpå sidor du behåller, är metoden för sidhopsättning och mallar det billigare verktyget

Allt som beskrivs här, inklusive syntaxen för intervalluttryck, alternativvärdena och det omgivande sidhanterings-API:et, levereras i det vanliga PDFlibPas Delphi PDF Library för Delphi och C++Builder, vars referensdokumentation innehåller den fullständiga posten för sidersättningsanropet och dess felkoder