Tekninen artikkeli

PDFlibPas MovePage: kun perityt laatikot jakavat instanssit

PDFlibPasissa, Delphi-PDF-kirjastossa, MovePageilla siirretty sivu sai aiemmin täsmälleen samat MediaBox-, CropBox- ja Resources-objektit, joita sen vanha Pages-solmu piti, joten myöhempi SetPageBox tai DrawText siirretyllä sivulla kirjoitti hiljaisesti kyseisen solmun ja jokaisen sisarun, joka yhä peri siitä. Versiosta v3.539.36 alkaen siirretty sivu saa omat koppionsa, ja epäsuora viittaus pysyy viittauksena. Sama julkaisu sulkee kaksi aiheeseen liittyvää polkua: SetPageBoxin epäsuoralle laatikolle, jota useat sivut jakavat, ja CopyPageRangesin, joka jätti lähdeasiakirjan sivut kiinnittyneiksi Pages-solmuunsa, ja CropBoxin kiinnittyneeksi MediaBoxiin

Raportit, jotka johtavat tänne, eivät koskaan mainitse objekti-identiteettiä. Ne sanovat asioita kuten "rajasin sivun 7, ja sivut 8–12 rajautuivat myös", tai "kavensin CropBoxin, ja MediaBox liikkui mukana", tai, sekavimmaksi asti, "kopioin sivun uuteen asiakirjaan ja alkuperäinen tiedosto muuttui". Mikään ei kaadu, mikään ei vuoda, ja tallennettu tiedosto on täysin kelvollista PDF:ää. Se sisältää vain geometriaa, jota kukaan ei pyytänyt

Miksi SetPageBox yhdellä sivulla muuttaa sisarsivujen kokoa?

SetPageBox muutti sisarsivujen kokoa, koska kaksi sivupuumerkintää osoitti yhteen muistissa olevaan taulukkoon, ja SetPageBox muokkaa kohdetaulukkoaan paikallaan. Jokainen samaa instanssia pitävä sivu tai Pages-solmu näki muokkauksen. Kolme koodipolkua PDFlibPasissa tuotti tuon jakamisen ennen v3.539.36:ta:

  • MovePage materialisoi perittävät attribuutit sivulle ennen sen irrottamista vanhemmastaan, ja se kiinnitti esi-isän omat objektit kopioiden sijaan, joten siirretty sivu ja sen entiset sisaret jakivat laatikkotaulukon ja Resources-sanakirjan
  • SetPageBox seurasi epäsuoria viittauksia ja muokkasi viitattua taulukkoa, joten tiedostossa, jossa useat sivut osoittivat yhteen /MediaBox 11 0 R-objektiin, yksi kutsu muutti kaikkien kyseisten sivujen koon, riippumatta siitä, oli MovePageia koskaan mukana
  • CopyPageRanges materialisoi perityt arvot lähdösivulle ennen sen kloonaamista kohdeasiakirjaan, ja se kiinnitti Pages-solmun instanssit lähdesivulle sekä MediaBox-instanssin itsensä oletus-CropBoxiksi
PDFlibPasin MovePage-aliasointi, jossa siirretty sivu ja sen entinen sisar pitivät kumpikin esi-isän omaa MediaBox-taulukkoinstanssia, joten SetPageBox muokkasi yhtä sivua ja muutti toisen kokoa; versiosta v3.539.36 alkaen materialisointi kiinnittää dekoodattuja kopioita ja muokkaukset pysyvät kosketamassasi sivussa
Kaksi sivupuumerkintää, jotka osoittivat yhteen muistissa olevaan taulukkoon, saivat jokaisen muokkauksen laskeutumaan jokaiseen pitäjään, ja tallennettu PDF pysyi kelvollisena koko ajan

MovePage-tapauksella on lyhyt historia. Ennen v3.539.27:aa MovePage vei mukanaan vain /Resourcesin, joten eri vanhemman alle siirretty sivu omi hiljaisesti kyseisen vanhemman koon ja kiertyman. v3.539.27 korjasi puuttuvat MediaBoxin, CropBoxin ja Rotaten, mistä CollateDocumentsExkin nojautuu järjestellessään sivuja uudelleen, mutta se kiinnitti esi-isän arvot jaetuiksi instansseiksi. Kyseinen ikkuna on se, jonka v3.539.36 sulkee. SetPageBox- ja CopyPageRanges-polut ovat vanhempia; mikä tahansa ennen v3.539.36:a tehty buildi sisältää ne

Suorat arvot, epäsuorat viittaukset ja sivuattribuuttien perintä

Perityn sivuattribuutin oikea kopio monistaa suorat arvot ja pitää epäsuorat viittaukset viittauksina, koska juuri tuohon eroon ISO 32000-1 itse vetoaa. Sanakirjan sisälle kirjoitettu suora objekti kuten [0 0 400 300] kuuluu yksin kyseiselle sanakirjalle. Epäsuora objekti, joka on määritelty kerran muodossa 11 0 obj ja viitattu muodossa 11 0 R, on jaettu suunnitellusti: ISO 32000-1 §7.3.10 tekee sen osoitettavaksi mistä tahansa tiedoston kohdasta, ja jokainen 11 0 R tarkoittaa samaa objektia

Sivuattribuuttien perintä, ISO 32000-1 §7.7.3.4, lisää kolmannen tapauksen. Resources, MediaBox, CropBox ja Rotate voivat sijaita Pages-solmulla ja koskea jokaista jälkeläissivua, joka ei määrittele omiaan. Sivu ei pidä arvoa hallussaan; se hakee arvon /Parentin kautta. Kyseinen hakuketju katkeaa sillä hetkellä, kun sivu vaihtaa vanhempaa, minkä vuoksi MovePagen ja BalancePageTreen on ensin kirjoitettava voimaan astuvat arvot sivun itsensä päälle. Kysymys on vain siitä, miten ne kirjoitetaan

Miksi objektiallas kätkee virheen

PDFlibPasissa jokainen jäsennetty tai luotu PDF-objekti on asiakirjan TPDFStructure-altaan omistama, ja sanakirjat sekä taulukot tallettavat pelkkiä osoittimia merkintöihinsä. TPDFDictionary.Add kirjaa osoittimen eikä muuta mitään. Yhden instanssin lisääminen kahteen vanhempaan säilöön on siksi laillista jokaisella tasolla, jota ajonaikainen ympäristö pystyy tarkistamaan: ei kaksinkertaista vapautusta purkamisessa, ei viitemäärää, joka voisi mennä pieleen, ei poikkeusta. Serialisointi on yhtä armollinen, sillä jokainen säilö kirjoittaa jaetun instanssin nykyisen arvon inline-muodossa, ja ennen muokkausta tulos on tavu tavulta se, minkä oikea kopio tuottaisi

Aliasointi nousee pintaan vasta, kun joku muuttaa jaettua instanssia paikallaan. SetPageBox tekee juuri niin olemassa olevan taulukon yli kiedotun suorakulmiokääreen kautta, ja sivulle piirtäminen tekee saman Resources-sanakirjalle, kun fontti tai kuva rekisteröidään. Muokkaus laskeutuu hiljaisesti jokaiseen muuhun osoitinta pitävään säilöön

Miten PDFlibPas v3.539.36 kopioi jakamisen sijaan

PDFlibPas v3.539.36 korjaa ongelman molemmissa päissä: materialisointi kiinnittää nyt kopioita, ja laatikkokirjoitukset muokkaavat nyt vain taulukkoa, jonka sivu omistaa. Kumpikin korjaus kattaa tapauksen, jonka toinen ei pysty

Materialisointiavustaja PLInheritPageAttributes kiinnittää nyt muodon Page.Owner.Decode(Value.Output) muodon Value sijaan. Kierrokset serialisoijan läpi on tökerö mutta täsmällinen tapa saada PDF:n semantiikka ilmaiseksi. Suora taulukko tai sanakirja serialisoituu kirjaimelliseksi tekstikseen ja dekoodautuu tuoreeksi, itsenäiseksi instanssiksi. Epäsuora viittaus serialisoituu muodoksi 11 0 R ja dekoodautuu uudeksi viittausobjektiksi, joka osoittaa samaan objektiin 11, joten sivu viittaa yhä jaettuun objektiin sen sijaan, että saisi sisällytetyn kopion, mikä säilyttää v3.539.27:ssa käyttöönotetun viittauskäyttäytymisen. Kopio on yhtä syvä kuin suora rakenne: mitä tahansa, mihin päästään viittauksen kautta kopioidun sanakirjan sisällä, pysyy jaettuna, aivan kuten tiedostomuoto aikoo. BalancePageTree kutsuu samaa avustajaa jokaiselle sivulle, jonka se asettaa uuden vanhemman alle, joten siellä materialisoidut sivut saavat myös erilliset instanssit

PDFlibPasin materialisoinnin kierrokset, joissa PLInheritPageAttributes kiinnittää muodon Page.Owner.Decode(Value.Output): suora taulukko serialisoituu kirjaimelliseksi tekstikseen ja dekoodautuu tuoreeksi instanssiksi, kun taas epäsuora 11 0 R serialisoituu ja dekoodautuu uudeksi viittaukseksi, joka osoittaa yhä jaettuun objektiin 11
Serialisoiminen ja uudelleenjäsentäminen saa PDF-objektien semantiikan ilmaiseksi: suorat arvot kopioituvat, viittaukset pysyvät viittauksina, täsmälleen kuten ISO 32000-1 aikoo

Pelkkä kopiointi ei riitä, koska viittaustapaus osoittaa yhä jaettuun objektiin. Jos SetPageBox seuraisi kyseistä viittausta ja muokkaisi objektia 11, siirretty sivu muuttaisi taas vanhan vanhemman ja sen muiden lasten kokoa. Laatikkokirjoittaja soveltaa siksi nyt copy-on-write-tekniikkaa: se muokkaa paikallaan vain, kun sivun oma merkintä on suora taulukko, ja korvaa epäsuoran tai puuttuvan laatikon uudella suoralla taulukolla. Objekti 11 jätetään koskemattomaksi kaikille muille siihen viittaaville sivuille

PDFlibPasin SetPageBoxin copy-on-write-päätös: kun sivun oma merkintä on suora taulukko, sitä muokataan paikallaan, ja kun se on epäsuora viittaus tai puuttuu, kirjoittaja korvaa sen uudella suoralla taulukolla, joten jaettu objekti 11 säilyttää arvonsa kaikille muille siihen viittaaville sivuille
Kopiointi materialisoinnissa ei riitä, kun viittaukset osoittavat yhä jaettuihin objekteihin, joten laatikkokirjoittaja muokkaa vain sitä, mitä sivu omistaa
KoodipolkuEnnen v3.539.36Versiosta v3.539.36 alkaen
MovePage-materialisointiSivu pitää esi-isän omia suoria instanssejaSivu pitää dekoodattuja kopioita; viittaukset pysyvät viittauksina
SetPageBoxSeuraa viittausta ja muokkaa jaettua taulukkoaMuokkaa vain suoraa taulukkoa sivulla, muuten kirjoittaa uuden
CopyPageRanges-lähdesivuJakaa Pages-solmun laatikot; CropBox on MediaBox-instanssiJokainen materialisoitu arvo lähdesivulla on kopio
Oletuslaatikot sivuresursseja kloonattaessaCropBox, BleedBox, TrimBox ja ArtBox jakavat yhden taulukonJokainen oletuslaatikko saa oman taulukkonsa

Viimeinen rivi on piilossa lymyävä. Kun kirjasto kloonaa sivun resurssit sivun kaappausta tai yhdistämistä varten, se täyttää puuttuvat CropBox-, BleedBox-, TrimBox- ja ArtBox-merkinnät, ja ne olivat aiemmin sama taulukkoinstanssi. Mikään nykyinen kutsuja ei antanut kyseisen aliasoinnin elää tarpeeksi pitkään muokattavaksi, mutta seuraava kutsuja olisi antanut. Se, miten kyseiset oletuslaatikkoarvot valitaan, on oma aiheensa, joka käsitellään artikkelissa PDFlibPasin opas TrimBox-, BleedBox- ja CropBox-oletuksiin

MovePage-aliasoinnin toistaminen käsin rakennetulla PDF:llä

Nopein tapa tarkistaa mikä tahansa PDFlibPasin buildi on pieni käsin kirjoitettu PDF, joka ladataan funktiolla LoadFromString, jossa jokainen objektinumero tunnetaan etukäteen. Alla oleva avustaja kirjoittaa klassisen ristiviitetaulukon oikein laskituilla tavuoffseteilla, joten testi ei nojaa jäsentimen toipumiskäyttäytymiseen rikkoutuneilla tiedostoilla

uses
  System.SysUtils, PDFlibrary;

function BuildPdf(const Objects: array of AnsiString): AnsiString;
var
  Offsets: array of Integer;
  I, XRefPos: Integer;
begin
  Result := '%PDF-1.4'#10;
  SetLength(Offsets, Length(Objects));
  for I := 0 to High(Objects) do
  begin
    Offsets[I] := Length(Result);   // "N 0 obj":n tavuoffsetti nollasta lukien
    Result := Result + AnsiString(IntToStr(I + 1)) + ' 0 obj'#10 +
      Objects[I] + #10'endobj'#10;
  end;
  XRefPos := Length(Result);
  Result := Result + 'xref'#10'0 ' + AnsiString(IntToStr(Length(Objects) + 1)) +
    #10'0000000000 65535 f '#10;
  for I := 0 to High(Offsets) do      // jokainen merkintä on tasan 20 tavua
    Result := Result + AnsiString(Format('%.10d 00000 n ', [Offsets[I]])) + #10;
  Result := Result + 'trailer'#10'<< /Size ' +
    AnsiString(IntToStr(Length(Objects) + 1)) + ' /Root 1 0 R >>'#10 +
    'startxref'#10 + AnsiString(IntToStr(XRefPos)) + #10'%%EOF'#10;
end;

function StreamObj(const Content: AnsiString): AnsiString;
begin
  Result := '<< /Length ' + AnsiString(IntToStr(Length(Content))) +
    ' >>'#10'stream'#10 + Content + #10'endstream';
end;

Testiasiakirjassa on kaksi välitasoista Pages-solmua. Solmu 3 kantaa epäsuoraa MediaBoxia (objekti 11, 400 kertaa 300 pistettä), suoraa CropBoxia ja suoraa Resources-sanakirjaa, ja omistaa kaksi sivua. Solmu 4:llä on Letter-kokoinen MediaBox, ja se omistaa kolmannen sivun. Sivun 1 siirtäminen positioon 3 asettaa sen uuden vanhemman alle solmun 4 muodossa, ja juuri kyseinen siirto tarvitsee materialisoinnin: ilman sitä sivusta tulisi Letter-sivu

procedure Check(Condition: Boolean; const Msg: string);
begin
  if not Condition then
    raise Exception.Create(Msg);
end;

procedure CheckMovedPageIsIsolated;
var
  Lib: TPDFlib;
  FontID: Integer;
begin
  Lib := TPDFlib.Create;
  try
    Check(Lib.LoadFromString(BuildPdf([
      '<< /Type /Catalog /Pages 2 0 R >>',
      '<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 3 >>',
      '<< /Type /Pages /Parent 2 0 R /Kids [5 0 R 6 0 R] /Count 2 ' +
        '/MediaBox 11 0 R /CropBox [10 20 390 280] /Resources << >> >>',
      '<< /Type /Pages /Parent 2 0 R /Kids [7 0 R] /Count 1 ' +
        '/MediaBox [0 0 612 792] >>',
      '<< /Type /Page /Parent 3 0 R /Contents 8 0 R >>',
      '<< /Type /Page /Parent 3 0 R /Contents 9 0 R >>',
      '<< /Type /Page /Parent 4 0 R /Contents 10 0 R >>',
      StreamObj('1 w'), StreamObj('2 w'), StreamObj('3 w'),
      '[0 0 400 300]']), '') = 1, 'load failed');

    Lib.SelectPage(1);
    Check(Lib.MovePage(3) = 1, 'MovePage failed');
    Lib.SelectPage(3);                       // sivu, jonka juuri siirsimme
    Check(Abs(Lib.GetPageBox(1, 2) - 400) < 0.001, 'inherited MediaBox lost');

    Lib.SetPageBox(1, 0, 200, 200, 200);     // MediaBox 200 x 200
    Lib.SetPageBox(2, 0, 100, 100, 100);     // CropBox 100 x 100
    FontID := Lib.AddStandardFont(4);        // Helvetica
    Lib.SelectFont(FontID);
    Lib.SetTextSize(12);
    Lib.DrawText(20, 20, 'MOVED');

    // Tutki vanhaa vanhempaa ENNEN toisen sivun valitsemista (katso alla)
    Check(Pos(AnsiString('/Font'), Lib.GetObjectToString(3)) = 0,
      'font registered in the old Pages node');

    Lib.SelectPage(1);                       // entinen sivu 2, yhä solmun 3 alla
    Check(Abs(Lib.GetPageBox(1, 2) - 400) < 0.001, 'sibling MediaBox changed');
    Check(Abs(Lib.GetPageBox(2, 2) - 380) < 0.001, 'sibling CropBox changed');
    Check(Pos(AnsiString('400'), Lib.GetObjectToString(11)) > 0,
      'shared object 11 was rewritten');
  finally
    Lib.Free;
  end;
end;

GetPageBox(BoxType, Dimension) ottaa laatikkotyypin 1 MediaBoxille ja 2 CropBoxille sekä dimension 2 leveydelle. Oletusarvoisella vasemman alakulman alkupisteellä SetPageBox(1, 0, 200, 200, 200) tarkoittaa vasen 0, ylä 200, 200 leveä ja 200 korkea. Buildeilla v3.539.27:n ja v3.539.35:n välillä sisartarkistukset epäonnistuvat: CropBox-muokkaus laskeutuu solmun 3 suoraan taulukkoon, ja MediaBox-muokkaus kirjoittaa objektin 11 uudelleen viittauksen kautta

Muuttaako CopyPageRanges lähdeasiakirjaa?

Versiosta v3.539.36 alkaen CopyPageRanges kirjoittaa yhä lähdesivuille, mutta jokainen kirjoitettava arvo on erillinen kopio, joten myöhemmät muokkaukset lähteellä pysyvät muokkaamassasi sivussa. Kirjoittaminen sinänsä on tarkoituksellista: lähdesivu tarvitsee eksplisiittiset MediaBoxin, CropBoxin, Rotaten ja Resourcesin ennen kuin sen sanakirja kloonataan kohteeseen, muuten kopio menettäisi kaiken perimänsä. Uudelleennumerointi ja sivun kopiointi kohteeseen käsitellään artikkelissa asiakirjojen välinen objektien syväkopiointi PDFlibPasissa; kyseinen bugi istui lähteen puolella, josta useimmat olettavat kopion vain lukevan

Tuotos ei koskaan näyttänyt sitä. Jaettuna tai kopioiduna materialisoidut arvot serialisoituvat identtisesti, joten molemmat asiakirjat tallentuivat tavu tavulta samoina ennen ja jälkeen korjauksen. Vain muokkaus lähdeasiakirjaan kopioinnin jälkeen paljasti aliasoinnin:

procedure CheckSourceSurvivesCopy;
var
  Lib: TPDFlib;
  SourceID, TargetID: Integer;
begin
  Lib := TPDFlib.Create;
  try
    Check(Lib.LoadFromString(BuildPdf([
      '<< /Type /Catalog /Pages 2 0 R >>',
      '<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 2 ' +
        '/MediaBox [0 0 400 300] /Resources << >> >>',
      '<< /Type /Page /Parent 2 0 R /Contents 5 0 R >>',
      '<< /Type /Page /Parent 2 0 R /Contents 6 0 R >>',
      StreamObj('1 w'), StreamObj('2 w')]), '') = 1, 'load failed');
    SourceID := Lib.SelectedDocument;

    TargetID := Lib.NewDocument;             // tulee valituksi asiakirjaksi
    Check(Lib.CopyPageRanges(SourceID, '1') = 1, 'copy failed');

    Lib.SelectDocument(SourceID);
    Lib.SelectPage(1);
    Lib.SetPageBox(2, 50, 250, 100, 100);    // kavenna vain CropBox
    Check(Abs(Lib.GetPageBox(1, 2) - 400) < 0.001, 'MediaBox followed CropBox');
    Lib.SetPageBox(1, 0, 200, 200, 200);

    Lib.SelectPage(2);
    Check(Abs(Lib.GetPageBox(1, 2) - 400) < 0.001, 'sibling page resized');

    Lib.SelectDocument(TargetID);            // kopio säilyttää alkuperäisen kokonsa
    Lib.SelectPage(Lib.PageCount);
    Check(Abs(Lib.GetPageBox(1, 2) - 400) < 0.001, 'copied page resized');
  finally
    Lib.Free;
  end;
end;

Ennen v3.539.36:ta molemmat sivut täällä perivät juurisolmun suoran MediaBoxin, kopio kiinnitti kyseisen instanssin lähdesivulle 1 ja kiinnitti sen uudelleen sivun 1 CropBoxiksi. CropBoxin kaventaminen kavensi siis MediaBoxia, ja MediaBoxin koon muuttaminen muutti sivun 2 koon juurisolmun kautta. Työnkulut, jotka kopioivat sivuja ulos ja jatkavat sitten lähteen muokkaamista, kuten artikkelin duplex-kuvien kokoaminen yhdeksi PDF:ksi kuvaava kokoaminen ennen alkuperäisten trimmaamista, ovat paikka, jossa tämä ilmeni

Miksi instanssien aliasointi on niin vaikea testata?

Instanssien aliasointi on vaikea testata, koska havaittava vaikutus tarvitsee kolme vaihetta tietyssä järjestyksessä: luo aliasointi, muuta toista puolta ja tarkista sitten toinen puoli ennen kuin mikään muu koskettaa sitä. Useimmat testit tekevät vain ensimmäisen vaiheen ja vertailevat tallennettua tuotosta, joka on identtinen riippumatta siitä, onko aliasointia olemassa

Järjestysansa PDFlibPasissa on SelectPage. Sivun valitseminen soveltaa nykyistä fonttia uudelleen funktiolla SelectFont, joka rekisteröi kyseisen fontin sivun resursseihin. Sivu, jolla ei ole omaa /Resourcesia, ratkeaa vanhempansa sanakirjaan, joten pelkkä tällaisen sivun valitseminen lisää laillisesti /Fontin Pages-solmulle. Yllä olevassa MovePage-testissä entisen sivun 2 valitseminen lisää Helvetica-merkinnän solmuun 3, mikä on oikeaa käyttäytymistä eikä vuoto. Siksi GetObjectToString(3)-tarkistus ajetaan ennen SelectPage(1)ia; vaihda ne keskenään ja testi epäonnistuu korjatulla buildilla

Kyseinen sääntö merkitsee myös sen, minkä v3.539.36 jättää tahallaan rauhaan. Resurssin kirjoittaminen sivulle, joka perii Resources-sanakirjansa, kirjoittaa esi-isän sanakirjaan, ja jokainen sisar näkee uuden merkinnän. Kyse on perinnästä määritelmän mukaan, ei instanssien jakamisesta, ja se on harmitonta, koska fontin tai kuvan nimen lisääminen jaettuun sanakirjaan ei muuta sitä, miten muut sivut renderöityvät. Jos tarvitset sivun lopettavan perimisen, anna sille ensin oma Resources-sanakirja

Tarkistuslista PDF-objektimallikoodille

Oppitunnit yleistyvät mihin tahansa altaaseen ja osoitinsäilöihin rakennettuun PDF-objektimalliin, Delphissä tai muualla:

  • Kun materialisoit perittäviä attribuutteja ISO 32000-1 §7.7.3.4:n mukaan, tee suorista arvoista syväkopio ja pidä epäsuorat viittaukset uusina viittauksina samaan objektiin
  • Älä koskaan lisää Addilla olemassa olevaa instanssia toiseen säilöön, ellei jakamista ole tarkoitettu ja dokumentoitu; allaan omistaminen merkitsee, ettei ajonaikainen ympäristö koskaan valita
  • Muokkaa paikallaan vain sitä, mitä nykyinen solmu omistaa suorana objektina; korvaa epäsuorat tai perityt arvot tuoreella suoralla objektilla (copy-on-write)
  • Toisesta merkinnästä johdetut oletusarvot, kuten CropBox MediaBoxista, tarvitsevat oman instanssinsa
  • Testaa aliasointia muuta-sitten-tutki-sekansseilla toisella pitäjällä ja tarkista niiden kutsujen järjestys, jotka voivat kirjoittaa laillisesti väliin
  • Tallennetun tuotoksen vertailu ei todista täällä mitään: jaetut ja kopioidut arvot serialisoituvat identtisesti ensimmäiseen muokkaukseen asti
  • PDFlibPasissa päivitys versioon v3.539.36 tai uudempaan, jos kutsut funktioita MovePage, CollateDocumentsEx, BalancePageTree tai CopyPageRanges ja muokkaat sitten sivulaatikoita tai piirrät sivuille

PDFlibPas tuo sivupuun muokkauksen, asiakirjojen välisen kopioinnin ja sivulaatikoiden hallinnan esiin yhden TPDFlib-luokan kautta Delphille, C++Builderille ja Free Pascalille. Katso PDFlibPas Delphi PDF library -tuotesivulta versiot, alustat ja täysi API-viite