Tekninen artikkeli

PDF-sivujen korvaaminen Delphissä ilman kirjanmerkkien hajoamista

Allekirjoitetun sopimuksen sivun 3 korvaamisen ei pitäisi siirtää sisällysluetteloa. Poista vanha sivu, lisää uusi, ja jokainen kirjanmerkki, joka aiemmin osoitti sinne, päätyy nyt jonnekin muualle. PDFlibPas Delphi PDF -kirjasto välttää tämän pitämällä itse kohdesivun objektin ja siirtämällä vain merkinnät, jotka kantavat visuaalista sisältöä

Miksi kirjanmerkit hajoavat PDF-sivun korvaamisen jälkeen?

Kirjanmerkit hajoavat, koska PDF-kohde nimeää sivun epäsuoran objektiviittauksen kautta, ei sivunumeron kautta. ISO 32000-1 §12.3.2.2 määrittelee eksplisiittisen kohteen taulukoksi, jonka ensimmäinen alkio on epäsuora viittaus sivuobjektiin. Poista tuo objekti ja liitä korvaava, ja viittaus jää roikkumaan: useimmat katseluohjelmat vastaavat pudottamalla lukijan sivulle 1, mikä on täsmälleen se oire, jota ihmiset raportoivat poista-sitten-lisää-korvauksen jälkeen. Sivupuu näyttää täydelliseltä, sivumäärä on oikein, renderöinti on oikein, ja koko navigointikerros on hiljaisesti väärin

Nimetyt kohteetkaan eivät pelasta sinua. §12.3.2.3 ohjaa nimen dokumenttikatalogin /Dests-nimipuun kautta, mutta lehti, johon tuo nimi ratkeaa, on yhä eksplisiittinen kohdetaulukko, joka pitää sisällään saman sivuviittauksen. Nimeäminen lisää epäsuoruuskerroksen sivuviittauksen päälle, ei sen ympärille. Sama päättely kattaa loput §12.5:ssä kuvatusta interaktiivisesta kerroksesta: linkkihuomautus kantaa /Dest-avainta tai /A-GoTo-toimintoa, jonka /D on tuo taulukko, jokainen huomautus voi kantaa /P-merkintää, joka on epäsuora viittaus sen sivuun, ja lomakekentän widget on huomautus täsmälleen samalla tasolla. Yksi naiivi sivunvaihto irrottaa neljä alijärjestelmää kerralla, ja jos haluat nähdä ne lueteltuna todellisessa tiedostossa, sama objektigraafi on se, jota sisällysluettelon ja huomautusten introspektio kulkee läpi

Mitkä sivumerkinnät kantavat identiteettiä ja mitkä ulkoasua

Sivusanakirja sekoittaa kahdenlaisia merkintöjä, ja paikallaan tehtävä korvaus onnistuu juuri silloin, kun ne erotellaan toisistaan. Ulkoasupuoli on rajallinen ja lueteltavissa: /Contents, /Resources, viisi sivulaatikkoa /MediaBox, /CropBox, /BleedBox, /TrimBox ja /ArtBox, sekä /Rotate, /Group, /UserUnit ja /BoxColorInfo. Nämä yksitoista merkintää päättävät kaiken, mitä rasteroija tuottaa sivulle, eikä mikään muu tiedostossa osoita niihin nimen kautta

Identiteettipuoli on se, mihin loppu dokumentista on sitoutunut: sivuobjektin numero ja sukupolvi, /Parent-takaisinlinkki sivupuuhun, ja /Annots. PDFlibPas pitää jokaisen niistä koskemattomana. ReplacePageRanges puhdistaa yksitoista visuaalista merkintää kohdesivun sanakirjasta ja lisää ne uudelleen tuodulta lähdesivulta, joten kohdesivun objektia muutetaan paikallaan sen sijaan, että se korvattaisiin. §7.7.3:n vaatima sivupuun rakenne pysyy myös muodoltaan tavu tavulta identtisenä: /Kids-järjestys, /Count, ja jokainen säilynyt /Parent ovat samat ennen ja jälkeen, koska yhtään solmua ei koskaan irrotettu

Miten PDFlibPas korvaa sivun uudelleennumeroimatta objekteja?

Kutsu ottaa lähdedokumentin, 1-pohjaisen kohteen alkusivun, lähdealuelausekkeen ja valintalipun. Molempien dokumenttien täytyy olla auki samassa instanssissa, ja kohdedokumentti on valittu dokumentti. Koska kohteen sivumäärä ei koskaan muutu, pyytämäsi alueen täytyy sopia dokumenttiin alkaen kohdasta TargetStartPage, ja se tarkistetaan ennen kuin mitään luodaan

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;

Sisäisesti lähdesivuja ei voida yksinkertaisesti lukea dokumenttirajojen yli, koska jokainen niiden sisällä oleva epäsuora viittaus kuuluu lähteen objektinumerointiin. Siksi lähdealue tuodaan ensin tavallisella tavalla, tilapäisinä sivuina viimeisen todellisen sivun jälkeen liitettynä, mikä ajaa täyden objektigraafin uudelleenkartoituksen: sisältövirrat, fontit, XObjectit, varjostukset ja väriavaruudet kaikki numeroidaan uudelleen kohdedokumenttiin. Vasta sitten yksitoista visuaalista merkintää kopioidaan jokaiselta tilapäiseltä sivulta sen kohdesivulle, ja vasta sitten tilapäiset sivut irrotetaan sivupuusta. Uudelleenkartoitustyö tapahtuu siellä, missä se on halpaa ja turvallista, ja tuhoisa muokkaus supistuu sanakirjatason vaihdoksi sivuille, jotka ovat jo olemassa

Poistopolku, joka tuhoaisi juuri siirtämäsi

Noiden tilapäisten sivujen poistaminen on vaihe, joka näyttää triviaalilta muttei ole. Kirjaston tavallinen sivunpoistopolku tekee enemmän kuin irrottaa solmun: se yhdistää jokaisen poistettavan sivun kerrokset, tyhjentää ensimmäisen sisältövirran ja palauttaa käyttöön resurssit, joita mikään muu sivu ei jaa. Se on oikeaa käytöstä todelliselle poistolle, ja katastrofaalista tässä, koska siihen mennessä, kun tilapäiset sivut poistetaan, kohdesivut jo viittaavat täsmälleen noihin sisältövirtoihin ja resurssiobjekteihin. Niiden tyhjentäminen tyhjentäisi juuri korvaamasi sivun, ja resurssipyyhkäisy keräisi fontteja ja kuvia, joilla nyt on elävä omistaja

Korjaus on säilytä-viitatut-objektit-tila sisäisellä poistopolulla. Kun se on asetettu, poisto ohittaa sekä jakamattomien resurssien pyyhkäisyn että sisältövirran tyhjennyksen, eikä tee muuta kuin irrottaa sivut sivupuusta ja korjaa puun kirjanpidon. Siirretyt objektit selviävät uudella omistajalla, ja objektien omistajuus toiminnon jälkeen on juuri se, mitä piirtäisit valkotaululle: yksi sisältövirta, yksi omistava sivu, yksi objektinumero, joka ei koskaan liikkunut. Sivujen luomisen, poistamisen ja järjestyksen vaihtamisen liittyvät elinkaarisäännöt käsitellään erikseen muistiossa dokumentin ja sivun elinkaaritoiminnoista

Järjestys, kaksoiskappaleet ja kaikki-tai-ei-mitään-epäonnistuminen

Valintalippu valitsee, miten lähdealue tulkitaan. 0 lajittelee jäsennetyt sivunumerot ja poistaa kaksoiskappaleet, mikä on järkevä oletus, kun kutsuja antaa jotain sellaista kuin '4-6,2' ja tarkoittaa yksinkertaisesti niitä neljää sivua. 1 säilyttää kirjoittamasi järjestyksen ja sallii sivun toistua, joten '2,1,2' tarkoittaa aidosti kolmea korvausta otettuna kahdesta lähdesivusta. Validointi ajetaan ensin ja täydellisesti: aluesyntaksi, jokainen sivunumero verrattuna lähteen sivumäärään, itse valinta-arvo ja kohteen kapasiteetti tarkistetaan kaikki ennen kuin yhtään objektia luodaan. Hylätty kutsu asettaa LastErrorCode-arvoksi 412, palauttaa aiemmin valitun sivun ja jättää dokumentin täsmälleen sellaiseksi kuin se oli

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;

Atomisuus ulottuu validoinnin ohi itse siirtoon. Ennen kuin ensimmäinen lähdesivu tuodaan, jokaisen alueella olevan kohdesivun yksitoista visuaalista merkintää otetaan tilannekuvaksi koodattuina arvoina. Jos tuonti epäonnistuu, tai tuotu sivumäärä ei täsmää pyydettyyn, tilannekuvat puretaan takaisin kohdesivuille ja tilapäiset sivut poistetaan, joten kesken jäänyt epäonnistuminen jättää yhä alkuperäiset visuaaliset sisällöt paikalleen niiden alkuperäisiin objekteihin. Tällä on enemmän merkitystä kuin miltä se kuulostaa: puoliksi korvattu sivualue sopimuksessa on pahempi kuin epäonnistunut kutsu, koska mikään tiedostossa ei merkitse sitä puolivalmiiksi

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

Mitä paikallaan tehtävä korvaus ei silti tee puolestasi?

Lähteen huomautuksia, lähteen lomakekenttiä ja lähteen sisällysluetteloa ei tarkoituksella tuoda. Widgetin tuominen mukaan ilman sen /AcroForm-kenttämerkintää, tai merkittyä sisältöä kantavan huomautuksen tuominen ilman sen rakennepuuomistajuutta, tuottaa puoliksi tuodun interaktiivisen objektin, josta mikään katseluohjelma ei pysty päättelemään, joten toiminto siirtää vain ulkoasun. Käytännön seuraus on se, että jos korvaavan sivun on tarkoitus kantaa uusia lomakekenttiä tai uusia linkkejä, lisäät ne kohdesivulle jälkikäteen, kohdesivun objektia vasten, joka on yhä siellä odottamassa niitä

Kaksi muuta rajaa kannattaa tarkistaa omista tiedostoistasi. Ensinnäkin /Annots säilyy, mutta sivun geometria ei, joten 220 mm:n sivun korvaaminen 320 mm:n sivulla pitää huomautuslaatikot vanhoissa koordinaateissaan erikokoisen /MediaBox:n sisällä; jos geometria muuttuu, sijoita säilyttämäsi huomautukset uudelleen. Toiseksi, yksitoista visuaalista avainta ulkopuoliset merkinnät pysyvät kohdesivulla tarkoituksella, mikä on oikein kohdalle /Trans tai /AA ja vanhentunutta kohdalle /Thumb, joten regeneroi pienoiskuvat korvauksen jälkeen. Merkityt dokumentit tarvitsevat yhden lisäajatuksen: rakenne-elementit yhä osoittavat oikeaan sivuobjektiin /Pg:n kautta, mutta niiden merkityn sisällön tunnisteet kuvaavat sisältöä, jota ei enää ole, joten sivunvaihto PDF/UA-työnkulun sisällä on yhtä lailla rakennepuun muokkaus kuin sisällön muokkaus. Jos tehtäväsi on todella kompositointi eikä vaihtaminen, taideteoksen kerrostaminen säilyttämiesi sivujen päälle, sivujen yhteenompelun ja mallipohjan lähestymistapa on halvempi työkalu

Kaikki tässä kuvattu, mukaan lukien aluelausekkeen syntaksi, valinta-arvot ja ympäröivä sivunkäsittely-API, toimitetaan vakiona PDFlibPas Delphi PDF Library -kirjastossa Delphille ja C++Builderille, jonka viitedokumentaatio sisältää täydellisen merkinnän sivunkorvauskutsusta ja sen virhekoodeista