Teknisk artikkel

Erstatt PDF-sider i Delphi uten å ødelegge bokmerker

Å erstatte side 3 i en signert kontrakt bør ikke flytte innholdsfortegnelsen. Slett den gamle siden, sett inn den nye, og hvert bokmerke som pekte dit havner nå et annet sted. PDFlibPas Delphi PDF-biblioteket unngår dette ved å beholde selve målsideobjektet og bare overføre oppføringene som bærer visuelt innhold

Hvorfor knekker bokmerker etter at en PDF-side erstattes?

Bokmerker knekker fordi en PDF-destinasjon navngir en side via indirekte objektreferanse, ikke via sidenummer. ISO 32000-1 §12.3.2.2 definerer en eksplisitt destinasjon som en array hvis første element er en indirekte referanse til sideobjektet. Slett det objektet og legg til en erstatning, og referansen henger i løse luften: de fleste visningsprogrammer reagerer ved å sette leseren på side 1, som er akkurat symptomet folk rapporterer etter en slett-så-sett-inn-erstatning. Sidetreet ser perfekt ut, sideantallet er riktig, renderingen er riktig, og hele navigasjonslaget er stille og rolig feil

Navngitte destinasjoner redder deg heller ikke. §12.3.2.3 ruter et navn gjennom /Dests-navnetreet i dokumentkatalogen, men bladet det navnet løses opp til er fortsatt en eksplisitt destinasjonsarray som holder samme sidereferanse. Navngiving legger til et lag med indireksjon over sidereferansen, ikke rundt den. Samme resonnement dekker resten av det interaktive laget beskrevet i §12.5: en lenkeannotasjon bærer et /Dest eller en /A GoTo-handling hvis /D er den arrayen, hver annotasjon kan bære en /P-oppføring som er en indirekte referanse til siden sin, og et skjemafelt-widget er en annotasjon på nøyaktig samme grunnlag. Ett naivt sidebytte løsriver fire delsystemer på én gang, og hvis du vil se dem listet opp på en ekte fil, er det den samme objektgrafen disposisjons- og annotasjonsintrospeksjon traverserer

Hvilke sideoppføringer bærer identitet og hvilke bærer utseende

En sideordbok blander to typer oppføringer, og en erstatning på stedet lykkes nettopp når du skiller dem. Utseendesiden er endelig og opplistbar: /Contents, /Resources, de fem sideboksene /MediaBox, /CropBox, /BleedBox, /TrimBox og /ArtBox, pluss /Rotate, /Group, /UserUnit og /BoxColorInfo. Disse elleve oppføringene avgjør alt en rasterisering produserer for siden, og ingenting annet i filen peker på dem ved navn

Identitetssiden er det resten av dokumentet har bundet seg til: sideobjektets nummer og generasjon, /Parent-tilbakelenken inn i sidetreet, og /Annots. PDFlibPas lar hver eneste av dem stå urørt. ReplacePageRanges renser de elleve visuelle oppføringene fra målsidens ordbok og legger dem til på nytt fra den importerte kildesiden, slik at målsideobjektet muteres på stedet fremfor å bli erstattet. Sidetrestrukturen som kreves av §7.7.3 forblir også byte-identisk i form: /Kids-rekkefølge, /Count, og hver overlevende /Parent er de samme før og etter, fordi ingen node noensinne ble koblet løs

Hvordan erstatter PDFlibPas en side uten å renummerere objekter?

Kallet tar et kildedokument, en 1-basert start-målside, et kilde-rangeuttrykk og et alternativflagg. Begge dokumentene må være åpne i samme instans, og måldokumentet er det valgte. Fordi målets sideantall aldri endrer seg, må rangen du ber om passe inn i dokumentet med start ved TargetStartPage, og det sjekkes før noe som helst opprettes

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 ikke kildesidene ganske enkelt leses på tvers av dokumentgrenser, fordi hver indirekte referanse i dem tilhører kildens objektnummerering. Så kilde-rangen importeres først på den vanlige måten, som midlertidige sider lagt til etter den siste ekte siden, noe som kjører full objektgraf-remapping: innholdsstrømmer, fonter, XObjects, skygger og fargerom blir alle renummerert inn i måldokumentet. Først deretter kopieres de elleve visuelle oppføringene fra hver midlertidige side over på sin målside, og først deretter kobles de midlertidige sidene løs fra sidetreet. Remappingarbeidet skjer der det er billig og trygt, og den destruktive redigeringen reduseres til et ordbok-nivå-bytte på sider som allerede finnes

Sletteveien som ville ødelagt det du nettopp overførte

Å fjerne de midlertidige sidene er steget som ser trivielt ut og ikke er det. Bibliotekets vanlige sideslettevei gjør mer enn å koble løs en node: den kombinerer lagene til hver side som slettes, tømmer den første innholdsstrømmen, og gjenvinner ressurser ingen annen side deler. Det er korrekt oppførsel for en ekte sletting, og katastrofalt her, fordi målsidene på det tidspunktet de midlertidige sidene fjernes allerede refererer til nøyaktig de innholdsstrømmene og ressursobjektene. Å tømme dem ville tømt siden du nettopp erstattet, og ressursopprydningen ville samlet inn fonter og bilder som nå har en levende eier

Fiksen er en bevar-refererte-objekter-modus på den interne sletteveien. Når den er satt, hopper slettingen over både opprydningen av udelte ressurser og innholdsstrøm-tømmingen, og gjør ingenting annet enn å koble sidene løs fra sidetreet og fikse opp treets bokføring. De overførte objektene overlever med en ny eier, og objekteierskapet etter operasjonen er det du ville tegnet på en tavle: én innholdsstrøm, én eiende side, ett objektnummer som aldri flyttet seg. De relaterte livssyklusreglene for å opprette, slette og omorganisere sider er dekket separat i notatene om dokument- og sidelivssyklusoperasjoner

Rekkefølge, duplikater, og alt-eller-ingenting-feil

Alternativflagget velger hvordan kilde-rangen tolkes. 0 sorterer de tolkede sidenumrene og fjerner duplikater, som er det fornuftige standardvalget når den som kaller sender inn noe som '4-6,2' og rett og slett mener de fire sidene. 1 bevarer rekkefølgen du skrev og tillater at en side gjentas, så '2,1,2' betyr genuint tre erstatninger hentet fra to kildesider. Validering kjører først og kjører fullstendig: range-syntaksen, hvert sidenummer mot kildens sideantall, selve alternativverdien og målets kapasitet blir alle sjekket før et eneste objekt opprettes. Et avvist kall setter LastErrorCode til 412, gjenoppretter den tidligere valgte siden, og lar dokumentet stå akkurat 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;

Atomisitet strekker seg forbi validering og inn i selve overføringen. Før den første kildesiden importeres, tas det et øyeblikksbilde av de elleve visuelle oppføringene til hver målside i rangen, som kodede verdier. Hvis importen mislykkes, eller det importerte sideantallet ikke stemmer med det som ble bedt om, dekodes øyeblikksbildene tilbake på målsidene og de midlertidige sidene fjernes, slik at en feil midtveis fortsatt lar de originale visuelle elementene stå igjen på sine originale objekter. Det betyr mer enn det høres ut som: en halvveis erstattet sonerekke i en kontrakt er verre enn et mislykket kall, fordi ingenting i filen markerer den som halvferdig

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

Hva gjør ikke erstatning på stedet for deg fremdeles?

Kildeannotasjoner, kildeskjemafelt og kildedisposisjoner importeres bevisst ikke. Å bringe et widget over uten dets /AcroForm-feltoppføring, eller en annotasjon som bærer merket innhold uten sin strukturtre-eierskap, produserer et halvimportert interaktivt objekt ingen visning kan resonnere om, så operasjonen overfører bare utseende. Den praktiske konsekvensen er at hvis erstatningssiden er ment å bære nye skjemafelt eller nye lenker, legger du dem til på målsiden etterpå, mot målsideobjektet som fortsatt sitter der og venter på dem

To grenser til er verdt å sjekke på dine egne filer. Først, /Annots bevares, men sidegeometrien gjør det ikke, så å erstatte en 220 mm-side med en 320 mm-side beholder annotasjonsrektanglene ved sine gamle koordinater inne i en annerledes dimensjonert /MediaBox; hvis geometrien endres, må du reposisjonere annotasjonene du beholdt. For det andre forblir oppføringer utenfor de elleve visuelle nøklene med målsiden by design, som er riktig for /Trans eller /AA og foreldet for /Thumb, så regenerer miniatyrbilder etter en erstatning. Merkede dokumenter trenger én tanke til: strukturelementene peker fortsatt på det korrekte sideobjektet gjennom /Pg, men de merket-innhold-identifikatorene deres beskriver innhold som ikke lenger er der, så et sidebytte inne i en PDF/UA-arbeidsflyt er en strukturtre-redigering like mye som en innholdsredigering. Hvis jobben din egentlig er sammensetning fremfor bytte, å legge grafikk oppå sider du beholder, er side-sammenstitching og maloppsettet det billigere verktøyet

Alt beskrevet her, inkludert range-uttrykkssyntaksen, alternativverdiene og det omkringliggende API-et for sidemanipulering, leveres i det standard PDFlibPas Delphi PDF Library for Delphi og C++Builder, hvis referansedokumentasjon inneholder hele oppføringen for sideerstatningskallet og feilkodene dets