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

PDF Library for Delphi-jämförelsediagram över en bokmärkesdestination namngiven med indirekt referens som överlever en byte av sida på plats, men hänger löst efter ett radera-och-lägg-till-byte
Destinationer binder bokmärken, länkar och widgets till ett sidobjektnummer, så att mutera det objektet på plats håller navigationen vid liv där radera-sen-infoga tappar läsare på sida 1

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. PDF Library for Delphi 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 PDF Library for Delphi 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
    // Dokumentet vars bokmärken och länkar måste överleva
    if Lib.LoadFromFile('contract-final.pdf', '') <> 1 then
      Exit;
    TargetDoc := Lib.SelectedDocument;

    // Den reviderade klausulsidan, renderad av vad som än producerade den
    if Lib.LoadFromFile('clause-7-revised.pdf', '') <> 1 then
      Exit;
    SourceDoc := Lib.SelectedDocument;

    Lib.SelectDocument(TargetDoc);
    // Källsida 1 skriver över utseendet på målsida 3.
    // Sidantal, sida 3:s objektnummer, bokmärken och anteckningar behålls.
    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

PDF Library for Delphi: Sidordbokens anatomi som skiljer identitetsposter som filen beror på från de elva visuella poster som ReplacePageRanges byter från en importerad källsida
ReplacePageRanges rensar de elva visuella nycklarna och lägger till dem igen från importen medan objektnummer, generation, /Parent och /Annots förblir exakt som de var

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

PDF Library for Delphi: Trestegsflöde för ReplacePageRanges som visar tillfällig import med objektomkartläggning, kopiering av visuella poster, och en referensbevarande avlänkning som skonar överförda resurser
Att importera källan som tillfälliga sidor låter den vanliga ommappningen köras först, så det destruktiva ingreppet krymper till att kopiera visuella nycklar och koppla loss noder utan att frigöra levande resurser
var
  Replaced: Integer;
begin
  Lib.SelectDocument(TargetDoc);
  // Options = 1: källordningen bevaras och upprepningar tillåts, så
  // målsidorna 5, 6 och 7 får källsidorna 2, 1 respektive 2
  Replaced := Lib.ReplacePageRanges(SourceDoc, 5, '2,1,2', 1);
  if Replaced = 0 then
    raise Exception.CreateFmt('Replacement rejected, LastErrorCode = %d',
      [Lib.LastErrorCode]);
  // Vid lyckat resultat är valet den första ersatta sidan
  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

// Efterattribut värda att kontrollera i ett regressionstest
Lib.SelectPage(3);
// Geometrin kommer nu från källsidan
WriteLn(Format('%.2f x %.2f', [Lib.PageWidth, Lib.PageHeight]));
// Anteckningar som redan fanns på målsida 3 sitter fortfarande kvar
WriteLn(Lib.AnnotationCount);
// Bokmärket som skapades före ersättningen pekar fortfarande på sida 3
WriteLn(Lib.GetOutlinePage(OutlineID));
// Och dokumentet har fortfarande samma längd
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 PDF Library for Delphi Delphi PDF Library för Delphi och C++Builder, vars referensdokumentation innehåller den fullständiga posten för sidersättningsanropet och dess felkoder