Izbacite sedam stranica iz priručnika od 200 stranica i svaka oznaka sleti negdje krivo. Rješenje nije gradnja obrisa iz ravne liste naslova. PDFiumPas izlaže TPdfOutlineEditor, koji učita pravo drvo obrisa, pusti vas premještati i preusmjeravati stavke, a zatim provede ApplyPageMap da svako izričito odredište provuče kroz vaš plan stranica
Zašto brisanje stranica slomi svaku oznaku?
Jer stavka obrisa ne sprema broj stranice. Sprema referencu na objekt stranice, i kad se objekti stranica promijene referenca ili pokazuje na stranicu koja se premjestila ili na ništa. ISO 32000-1 §12.3.2.2 definira izričito odredište kao niz čiji je prvi element neizravna referenca na rječnik stranice, a iza njega ime pristajanja poput /Fit ili /XYZ. Izbrišite stranicu i ostaje referenca koja visi u prazno; preuredite stranice i referenca je još valjana ali sada opisuje drugo poglavlje. PDFiumPas taj niz razrješuje natrag u broj stranice pri učitavanju, pa vam TPdfOutlineItem.PageNumber daje indeks stranice od jedan koji odgovara javnom TPdf API-ju umjesto broja objekta. U tome je cijela poanta apstrakcije: vaša logika remapiranja radi u istom koordinatnom sustavu kao plan stranica koji ste već sagradili kad ste dokument rastavljali, preuređivali ili slagali. Ako taj plan gradite, ista konvencija od jedinice prolazi kroz rastavljanje PDF dokumenata u više datoteka i kroz n-up slaganje i preuređivanje stranica
Obris je dvostruko povezano drvo, ne lista
Razlog zašto ne možete jednostavno serializirati ravni niz naslova je što ISO 32000-1 §12.3.3 svaku stavku obrisa poveže u pet odvojenih veza: /Parent, /Prev, /Next, /First i /Last. Premještanje jednog podstabla zato prepravlja starog roditelja, novog roditelja, oba susjedna brata s obje strane reza i točke umetanja, i pokazivač roditelja samog premještenog čvora. Pogriješite u jednom i sukladni čitači pokazuju odrezano drvo, ili petlju. PDFiumPas stanje uređivanja drži kao dubinski niz zapisa TPdfOutlineItem sa stabilnim cjelobrojnim Id, pa je podstablo povezani odsječak a lanac braće izveden, nikad ručno održavan. TPdfOutlineEditor.Move podigne taj odsječak, ponovno ga umetne pod novog roditelja na zatraženom bratskom indeksu i dodijeli ponovno samo korijen bloka. Odbija i dva premještanja koja bi pokvarila graf: premještanje stavke u njeno vlastito podstablo i imenovanje roditelja koji ne postoji
Zašto je /Count predznačen?
Jer predznak nosi stanje proširenosti, ne veličinu. Pozitivan /Count znači da je stavka otvorena i broj je koliko potomaka je trenutno vidljivo; negativan /Count znači da je stavka skupljena. PDFiumPas upisuje broj potomaka za svaku stavku koja ima djecu i negira ga kad je IsOpen False, a pri učitavanju stanje čita natrag kao IsOpen := HasCount and (CountValue > 0). Ovo je najčešća ručna greška u piscima obriša: ispisati nepredznačen broj i tiho prisiliti cijelo drvo otvoreno
var
Source, Dest: TMemoryStream;
Editor: TPdfOutlineEditor;
Options: TPdfOutlineEditOptions;
Report: TPdfOutlineValidationReport;
RootId, ChapterId: Integer;
begin
Source := TMemoryStream.Create;
Dest := TMemoryStream.Create;
Editor := nil;
try
Source.LoadFromFile('handbook.pdf');
Options := TPdfOutlineEditOptions.Default; // MaxItems 100000, MaxDepth 64
if not TPdfOutlineEditor.TryLoad(Source, Options, Editor, Report) then
raise Exception.Create(Report.ErrorMessage);
RootId := Editor[0].Id;
ChapterId := Editor[2].Id;
Editor.Move(ChapterId, RootId, 1); // becomes second child of root
Editor.SetTitle(ChapterId, 'Appendix B');
Editor.SetStyle(ChapterId, [posBold, posItalic]);
Editor.SetColor(ChapterId, 0.25, 0.5, 0.75);
Editor.SetExpanded(RootId, False); // writes a negative /Count
Editor.Retarget(ChapterId, 12, '/XYZ 10 20 1');
if not Editor.SaveIncremental(Source, Dest, Report) then
raise Exception.Create(Report.ErrorMessage);
Dest.SaveToFile('handbook-edited.pdf');
finally
Editor.Free;
Dest.Free;
Source.Free;
end;
end;
Retarget rukuje oba oblika koje specifikacija dopušta. Predajte DestinationInAction kao False i PDFiumPas upisuje izravni niz /Dest; predajte True i upisuje Go-To akciju, /A << /S /GoTo /D [ page ref suffix ] >>, prema ISO 32000-1 §12.6.4.2. U oba slučaja prvo oguli postojeći /Dest i /A sa stavke da dvoje ne može koegzistirati i proturječiti. Sufiks je po zadanom /Fit i mora početi PDF imenom, zato prazan ili pokvaren sufiks odmah podiže iznimku umjesto da proizvede niz odredišta koji nijedan čitač ne može raščlaniti
Kako ApplyPageMap troši plan stranica?
ApplyPageMap prima točno niz koji vaš plan stranica već provjerio: NewPageNumbers, indeksiran starom stranicom minus jedan, drži novi broj stranice od jedan ili nulu kad ta stranica nije preživjela. Prolazi niz stavki unatrag da brisanje podstabla nikad ne poništi indeks koji još treba posjetiti, a ono što je učinio izvještava kroz RemappedDestinationCount i RemovedDanglingItemCount
var
NewPageNumbers: array of Integer;
Report: TPdfOutlineValidationReport;
I: Integer;
begin
// One entry per page of the ORIGINAL document
SetLength(NewPageNumbers, OriginalPageCount);
for I := 0 to OriginalPageCount - 1 do
NewPageNumbers[I] := 0; // 0 == this page was dropped
NewPageNumbers[0] := 1; // old page 1 -> new page 1
NewPageNumbers[1] := 2;
NewPageNumbers[9] := 3; // old page 10 -> new page 3
// True: delete the whole dangling subtree. False: keep the item, strip its target
if not Editor.ApplyPageMap(NewPageNumbers, True, Report) then
raise Exception.Create(Report.ErrorMessage);
WriteLn(Format('%d remapped, %d dangling items removed',
[Report.RemappedDestinationCount, Report.RemovedDanglingItemCount]));
end;
Zastavica DeleteDangling odlučuje politiku za odredište mapirano na nulu, i obje grane su namjerne. S True, PDFiumPas briše stavku i cijelo njeno podstablo, jer čvor obrisa čiji je cilj nestao obično čeliko poglavlje koje je nestalo s njim. S False, stavka preživi s naslovom i hijerarhijom netaknutima ali bez /Dest i /A, što hoćete kad će je čovjek u pregledu preusmjeriti. Stvarno pokvaren ulaz i dalje propada glasno umjesto da se zakrpi: negativan unos ili odredište koje pokazuje preko kraja predane mape vraća False s IssueKind postavljenim na poviInvalidPageMap
Neprozirne stavke i pošten kompromis
Nije svaka stavka obrisa ima broj stranice o kojem PDFiumPas može razmišljati. Tri vrste provlače se nedirnute: imenovana odredišta, akcije koje nisu /S /GoTo i nepoznati ključevi rječnika dodani od onoga tko je datoteku proizveo. Ove se učitavaju s PageNumber jednakim nuli, čuvaju svoje izvorne bajtove u stavci i upisuju se natrag doslovno osim ako im izričito ne pozovete Retarget
- Imenovano odredište je ključ u stablo imena dokumenta, pa ga remapiranje ispravno znači razrješiti stablo i prepraviti ciljni unos, a ne nagađati na razini obrisa
- Akcija
/URI,/Launchili JavaScript nema nikakvu semantiku stranice i ne smije se tiho pretvoriti u Go-To - Ključevi specifični za dobavljača i odredišta strukture čuvaju se jer je bacanje onoga što ne razumijete način na koji povratni putevi gube podatke
Cijena je stvarna i vrijedi je izreći ravnim jezikom: ApplyPageMap te stavke potpuno preskače, pa dokument čije sve oznake koriste imenovana odredišta proći će brisanje stranica s obrisom strukturno valjanim ali semantički zastarjelim. To je namjerni izbor — zastarjela veza koju recenzent može uhvatiti bolja je od samouvjereno krive koje nitko ne primijeti. Ako prihvatne datoteke trijažirate prije uređivanja, prolaz inventara u PDF radnoj stolici za pregled prijema reći će vam koji dokumenti padaju u taj koš
Spremanje: inkrementalna dopuna pa neovisno ponovno učitavanje
TPdfOutlineEditor.SaveIncremental dopisuje rijetku inkrementalnu reviziju umjesto da prepravi datoteku. Stavke koje su učitane čuvaju svoju izvornu neizravnu referencu objekta uključujući točnu generaciju, pa postojeće unakrsne reference ostaju valjane; samo stavke koje ste dodali crpe svježi broj, alociran od jednog preko maksimalnog broja objekta revizije. Katalog se ažurira u istoj reviziji, a nedostajući unos /Outlines dodaje mu se kad izvor uopće nije imao obris
Ono što slijedi nakon upisa je dio vrijedan kopiranja. PDFiumPas ponovno otvara odredišni tok s potpuno neovisnim uređivačem i uspoređuje ponovno učitano drvo s onim u memoriji — broj stavki, naslovi, brojevi stranica, sufiksi odredišta, oblik akcija nasuprot izravnom odredištu, stilovi, stanje proširenosti i odnosi roditeljstva. Svako odstupanje, ili svaki neuspjeh učitavanja, briše odredišni tok i vraća poviVerificationFailure umjesto da vam uruči datoteku koja samo izgleda vjerodostojno. Šifrirani izvori se unaprijed odbijaju s poviEncryptedInput, jer novi naslovi i odredišta stvaraju sadržaj nizova koji se ne može proizvesti kopiranjem /Encrypt prikolice naprijed
if not Editor.SaveIncremental(Source, Dest, Report) then
case Report.IssueKind of
poviEncryptedInput:
Log('Source is encrypted; outline editing needs an unprotected copy');
poviInvalidDestination:
Log(Format('Item %d %d targets a missing page',
[Report.ObjectNumber, Report.Generation]));
poviVerificationFailure:
Log('Reload check rejected the written revision: ' + Report.ErrorMessage);
else
Log(Report.ErrorMessage);
end;
Tretirajte obris kao ono što jest — povezani graf objekata sa svojim nepromjenjivim pravilima — pa brisanje stranica prestaje biti katastrofa oznaka i postaje mapa stranica koju predajete jednom pozivu metode. TPdfOutlineEditor, ApplyPageMap i provjereni inkrementalni pisac stižu u PDFiumPasu od v3.98.0 za Delphi, C++Builder i Lazarus; puni API možete pregledati i probnu verziju preuzeti na stranici proizvoda PDFium Delphi komponente