Teknisk artikel

Erstat PDF-sider i Delphi uden at ødelægge bogmærker

At erstatte side 3 i en godkendt kontrakt bør ikke flytte indholdsfortegnelsen. Slet den gamle side, indsæt den nye, og hvert bogmærke, der plejede at pege derhen, lander nu et andet sted. PDFlibPas Delphi PDF-biblioteket undgår det ved at beholde selve målsideobjektet og kun overføre de poster, der bærer visuelt indhold

Hvorfor går bogmærker i stykker efter man erstatter en PDF-side?

Bogmærker går i stykker, fordi en PDF-destination navngiver en side ved indirekte objektreference, ikke ved sidenummer. ISO 32000-1 §12.3.2.2 definerer en eksplicit destination som et array, hvis første element er en indirekte reference til sideobjektet. Slet det objekt og tilføj en erstatning, og referencen hænger løst: de fleste viewere reagerer ved at smide læseren på side 1, hvilket er præcis det symptom, folk rapporterer efter en slet-så-indsæt-erstatning. Sidetræet ser perfekt ud, sideantallet er rigtigt, renderingen er rigtig, og hele navigationslaget er stille forkert

Navngivne destinationer redder en heller ikke. §12.3.2.3 ruter et navn gennem /Dests-navnetræet i dokumentkataloget, men det blad, det navn opløser til, er stadig et eksplicit destinationsarray, der holder den samme sidereference. Navngivning tilføjer et lag af indirektion oven på sidereferencen, ikke omkring den. Den samme logik dækker resten af det interaktive lag beskrevet i §12.5: en link-annotation bærer en /Dest eller en /A GoTo-handling, hvis /D er det array, hver annotation kan bære en /P-post, der er en indirekte reference til dens side, og et formularfelt-widget er en annotation på præcis samme fod. Ét naivt sideskift løsriver fire delsystemer på én gang, og hvis man vil se dem opremset på en rigtig fil, er det den samme objektgraf, outline- og annotations-introspektion gennemgår

Hvilke sideposter bærer identitet, og hvilke bærer udseende

Et sidedictionary blander to slags poster, og en erstatning på plads lykkes præcis når man adskiller dem. Udseendesiden er endelig og optællelig: /Contents, /Resources, de fem sideboks-poster /MediaBox, /CropBox, /BleedBox, /TrimBox og /ArtBox, plus /Rotate, /Group, /UserUnit og /BoxColorInfo. De elleve poster afgør alt, en rasterisering producerer for siden, og intet andet i filen peger på dem ved navn

Identitetssiden er hvad resten af dokumentet har bundet sig til: sideobjektnummeret og generationen, /Parent-tilbagelinket ind i sidetræet, og /Annots. PDFlibPas holder hver af dem urørt. ReplacePageRanges renser de elleve visuelle poster fra måldictionaryet og genindsætter dem fra den importerede kildeside, så målsideobjektet muteres på plads frem for at blive erstattet. Sidetræstrukturen krævet af §7.7.3 forbliver også byte-identisk i form: /Kids-rækkefølge, /Count, og hvert overlevende /Parent er de samme før og efter, fordi ingen node nogensinde blev afkoblet

Hvordan erstatter PDFlibPas en side uden at omnummerere objekter?

Kaldet tager et kildedokument, en 1-baseret mål-startside, et kildeinterval-udtryk og et flag for muligheder. Begge dokumenter skal være åbne i samme instans, og måldokumentet er det valgte. Fordi målsideantallet aldrig ændres, skal intervallet man anmoder om passe inden for dokumentet, startende ved TargetStartPage, og det tjekkes, før noget oprettes

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 kildesiderne ikke bare læses på tværs af dokumentgrænser, fordi hver indirekte reference inde i dem hører til kilde-objektnummereringen. Så kildeintervallet importeres først på den almindelige måde, som midlertidige sider tilføjet efter den sidste rigtige side, hvilket kører den fulde objektgraf-omnummerering: indholdsstrømme, fonte, XObjekter, shadinger og farverum omnummereres alle ind i måldokumentet. Først derefter kopieres de elleve visuelle poster fra hver midlertidig side over på dens målside, og først derefter afkobles de midlertidige sider fra sidetræet. Omnummereringsarbejdet sker der, hvor det er billigt og sikkert, og den destruktive redigering reduceres til et dictionary-niveau-skift på sider, der allerede findes

Sletningsstien, der ville ødelægge hvad man lige havde overført

At fjerne de midlertidige sider er trinnet, der ser trivielt ud og ikke er det. Den almindelige sidesletningssti i biblioteket gør mere end at afkoble en node: den kombinerer lagene af hver side, der slettes, tømmer den første indholdsstrøm, og genvinder ressourcer, ingen anden side deler. Det er korrekt opførsel for en rigtig sletning, og katastrofalt her, fordi målsiderne på det tidspunkt de midlertidige sider fjernes allerede refererer præcis de indholdsstrømme og ressourceobjekter. At tømme dem ville tømme siden, man lige havde erstattet, og ressource-fejningen ville indsamle fonte og billeder, der nu har en levende ejer

Rettelsen er en bevar-refererede-objekter-tilstand på den interne slette-sti. Når den er sat, springer sletningen både den ikke-delte-ressource-fejning og indholdsstrøm-rydningen over, og gør intet andet end at afkoble siderne fra sidetræet og rette op på træbogholderiet. De overførte objekter overlever med en ny ejer, og objektejerskabet efter operationen er hvad man ville tegne på en whiteboard: én indholdsstrøm, én ejende side, ét objektnummer der aldrig flyttede. De relaterede livscyklusregler for at oprette, slette og omordne sider dækkes separat i noterne om dokument- og side-livscyklus-operationer

Rækkefølge, dubletter og alt-eller-intet-fejl

Flaget for muligheder vælger, hvordan kildeintervallet fortolkes. 0 sorterer de parsede sidenumre og fjerner dubletter, hvilket er det fornuftige standardvalg, når kalderen overgiver noget som '4-6,2' og simpelthen mener de fire sider. 1 bevarer den rækkefølge man skrev og tillader en side at gentage sig, så '2,1,2' betyder ægte tre erstatninger taget fra to kildesider. Validering kører først og kører fuldstændigt: intervalsyntaksen, hvert sidenummer mod kildesideantallet, selve options-værdien, og målkapaciteten tjekkes alle sammen, før et eneste objekt oprettes. Et afvist kald sætter LastErrorCode til 412, gendanner den tidligere valgte side, og lader dokumentet være præcis 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;

Atomicitet strækker sig ud over validering og ind i selve overførslen. Før den første kildeside importeres, tages de elleve visuelle poster af hver målside i intervallet som et snapshot i kodede værdier. Hvis importen fejler, eller det importerede sideantal ikke matcher hvad der blev anmodet om, dekodes snapshotene tilbage på målsiderne, og de midlertidige sider fjernes, så en fejl midt i flyvningen stadig efterlader de originale visuelle elementer på plads på deres originale objekter. Det betyder mere end det lyder: et halv-erstattet sideinterval i en kontrakt er værre end et fejlet kald, fordi intet i filen markerer det som halvt gjort

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

Hvad gør erstatning på plads stadig ikke for dig?

Kilde-annotationer, kilde-formularfelter og kilde-outlines importeres bevidst ikke. At bringe en widget over uden dens /AcroForm-feltpost, eller en markeret-indhold-bærende annotation uden dens strukturtræ-ejerskab, producerer et halvt-importeret interaktivt objekt, ingen viewer kan ræsonnere om, så operationen overfører kun udseende. Den praktiske konsekvens er, at hvis erstatningssiden skal bære nye formularfelter eller nye links, tilføjer man dem til målsiden bagefter, mod målsideobjektet der stadig sidder der og venter på dem

To grænser mere er værd at tjekke på ens egne filer. For det første er /Annots bevaret, men sidegeometri er ikke, så at erstatte en 220 mm-side med en 320 mm-side beholder annotationsrektangler ved deres gamle koordinater inde i et forskelligt-størrelse /MediaBox; hvis geometrien ændres, ompositionér de annotationer man beholdt. For det andet forbliver poster uden for de elleve visuelle nøgler med målsiden by design, hvilket er rigtigt for /Trans eller /AA og forældet for /Thumb, så regenerér thumbnails efter en erstatning. Tagged dokumenter kræver én ekstra overvejelse: strukturelementerne peger stadig på den korrekte sideobjekt gennem /Pg, men deres marked-content-identifikatorer beskriver indhold, der ikke længere er der, så et sideskift inde i en PDF/UA-workflow er en strukturtræ-redigering såvel som en indholdsredigering. Hvis ens job egentlig er compositing frem for at bytte, at lægge artwork oven på sider man beholder, er side-sammensyning-og-skabelon-tilgangen det billigere værktøj

Alt beskrevet her, inklusive interval-udtryk-syntaksen, options-værdierne og den omkringliggende sidemanipulations-API, leveres i standard-PDFlibPas Delphi PDF-bibliotek til Delphi og C++Builder, hvis referencedokumentation bærer den fulde post for sideerstatningskaldet og dets fejlkoder