Tehnični članak

Urejanje orisa PDF in preslikava strani v Delphiju

Spustite sedem strani iz priročnika s 200 stranmi in vsak zaznamek pristane nekje narobe. Rešitev ni ponovna izgradnja orisa iz ravnega seznama naslovov. PDFiumPas izpostavi TPdfOutlineEditor, ki naloži pravo drevo orisa, vam dovoli premikati in ponovno ciljati predmete, nato pa izvede ApplyPageMap, da prestavi vsak izrecni cilj skozi vaš načrt strani

Zakaj brisanje strani pokvari vsak zaznamek?

Ker predmet orisa ne shranjuje številke strani. Shranjuje sklic na predmet strani, in ko se predmeti strani spremenijo, sklic bodisi kaže na stran, ki se je premaknila, bodisi na nič. ISO 32000-1 §12.3.2.2 definira izrecni cilj kot tabelo, katere prvi element je posredni sklic na slovar strani, ki mu sledi ime prilagajanja, kot je /Fit ali /XYZ. Izbrišite stran in ostane vam viseč sklic; preuredite strani in sklic je še vedno veljaven, a zdaj opisuje drugo poglavje. PDFiumPas razreši to tabelo nazaj na številko strani, ko naloži, tako da TPdfOutlineItem.PageNumber da eniško osnovan indeks strani, ki se ujema z javnim API TPdf in ne s številko predmeta. To je celotna poanta abstrakcije: vaša logika preslikave deluje v istem koordinatnem sistemu kot načrt strani, ki ste ga že zgradili, ko ste dokument razdelili, preuredili ali izvedli n-up. Če ta načrt gradite, ista eniško osnovana dogovornost teče skozi razdeljevanje dokumentov PDF v več datotek in skozi n-up izvedbo in preurejanje strani

Oris je dvojno povezano drevo, ne seznam

Razlog, da preprosto ne morete serializirati ravne tabele naslovov, je, da ISO 32000-1 §12.3.3 poveže vsak predmet orisa v pet ločenih povezav: /Parent, /Prev, /Next, /First in /Last. Premik enega samega poddrevesa torej prepisuje starega starša, novega starša, oba sosednja brata na vsaki strani reza in vstavitvene točke ter kazalec na starša premaknjenega vozlišča samega. Zadelite enega od teh narobe in skladni bralniki pokažejo skrajšano drevo ali zanko. PDFiumPas hrani stanje urejanja kot tabelo zapisov TPdfOutlineItem v globino, s stabilnim celoštevilčnim Id, tako da je poddrevo neprekinjen rez in veriga bratov izpeljana, nikoli ročno vzdrževana. TPdfOutlineEditor.Move dvigne ta rez, ga ponovno vstavi pod novega starša pri zahtevanem indeksu bratov in ponovno dodeli le koren bloka. Prav tako zavrne dva premika, ki bi pokvarila graf: premik predmeta v lastno poddrevo in imenovanje starša, ki ne obstaja

Urejanje orisa PDFiumPas v Delphiju: premik Poglavja 3 iz Dela I pod koren dokumenta prepisuje kazalec /Parent premaknjenega vozlišča ter povezave /First in bratske /Prev in /Next okoli reza in vstavitvene točke
En klic Move prepisuje kazalec na starša dvignjenega poddrevesa in bratske povezave na obeh straneh reza in vstavitvene točke

Zakaj je /Count predznačen?

Ker predznak nosi stanje razširjenosti, ne velikost. Pozitiven /Count pomeni, da je predmet odprt in številka pove, koliko potomcev je trenutno vidnih; negativen /Count pomeni, da je predmet zložen. PDFiumPas zapiše števec potomcev za vsak predmet, ki ima otroke, in ga negira, ko je IsOpen False, ob nalaganju pa prebere stanje nazaj kot IsOpen := HasCount and (CountValue > 0). To je najpogostejša ročno izdelana napaka v pisalnikih orisov: oddajanje nepredznačenega števca in tiho vsiljevanje celega drevesa odprtega

Kako PDFiumPas kodira stanje razširjenosti orisa v Delphiju: pozitiven /Count pomeni, da je predmet odprt in šteje vidne potomce, negativen /Count pomeni zloženo, nepredznačen števec pa vsili vsakemu bralniku razširiti celo drevo
Predznak /Count je stanje razširjenosti, velikost pa števec vidnih potomcev, zato nepredznačen števec tiho vsili celo drevo odprto
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);           // postane drugi otrok korena
    Editor.SetTitle(ChapterId, 'Appendix B');
    Editor.SetStyle(ChapterId, [posBold, posItalic]);
    Editor.SetColor(ChapterId, 0.25, 0.5, 0.75);
    Editor.SetExpanded(RootId, False);           // zapiše negativen /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 obravnava obe obliki, ki ju specifikacija dovoli. Podajte DestinationInAction kot False in PDFiumPas zapiše neposredno tabelo /Dest; podajte True in zapiše dejanje Go-To, /A << /S /GoTo /D [ page ref suffix ] >>, po ISO 32000-1 §12.6.4.2. V obeh primerih najprej odstrani obstoječa /Dest in /A s predmeta, tako da dva ne moreta soobstajati in se razhajati. Pripona je privzeto /Fit in se mora začeti s PDF imenom, zato prazna ali deformirana pripona takoj sproži namesto da bi dala tabelo cilja, ki je noben bralnik ne more razčleniti

Kako ApplyPageMap sprejme načrt strani?

ApplyPageMap vzame točno tabelo, ki jo je vaš načrt strani že potrdil: NewPageNumbers, indeksirano po stari strani minus ena, ki drži novo eniško osnovano številko strani ali ničlo, ko ta stran ni preživela. Hodi po tabeli predmetov nazaj, tako da brisanje poddrevesa nikoli ne razveljavi indeksa, ki ga še ni obiskal, in poroča, kaj je naredil, prek RemappedDestinationCount in RemovedDanglingItemCount

var
  NewPageNumbers: array of Integer;
  Report: TPdfOutlineValidationReport;
  I: Integer;
begin
  // En vnos na stran IZVIRNEGA dokumenta
  SetLength(NewPageNumbers, OriginalPageCount);
  for I := 0 to OriginalPageCount - 1 do
    NewPageNumbers[I] := 0;              // 0 == ta stran je bila spuščena

  NewPageNumbers[0] := 1;                // stara stran 1 -> nova stran 1
  NewPageNumbers[1] := 2;
  NewPageNumbers[9] := 3;                // stara stran 10 -> nova stran 3

  // True: izbriši celo viseče poddrevo. False: obdrži predmet, odstrani njegov cilj
  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 odloči politiko za cilj, ki se je preslikal na ničlo, obe veji pa sta namerni. Z True PDFiumPas izbriše predmet in njegovo celotno poddrevo, ker vozlišče orisa, katerega cilj je izginil, običajno vodi poglavje, ki je izginilo z njim. Z False predmet preživi z naslovom in hierarhijo nedotaknjena, vendar z odstranjenima /Dest in /A, kar hočete, ko ga bo človek ponovno ciljal v pregledu. Res deformiran vhod še vedno odpove glasno namesto da bi bil zakrpan: negativen vnos ali cilj, ki kaže čez konec podane karte, vrne False z IssueKind nastavljenim na poviInvalidPageMap

Kako ApplyPageMap PDFiumPas preusmerja zaznamke PDF v Delphiju: karta strani, indeksirana po stari strani minus ena, pošlje preživele cilje na njihove nove številke strani, vnosi, ki se preslikajo na ničlo, pa se bodisi izbrišejo s svojim poddrevesom bodisi ostanejo brez cilja
Karta strani je indeksirana po stari strani minus ena, vnos z ničlo pa bodisi izbriše viseče poddrevo bodisi pusti predmet brez cilja

Neprozorni vnosi in pošten kompromis

Nima vsak predmet orisa številke strani, o kateri PDFiumPas lahko sklepa. Tri vrste se prenašajo naprej nedotaknjene: poimenovani cilji, dejanja, ki niso /S /GoTo, in neznani ključi slovarja, dodani s strani tistega, kar je dalo datoteko. Ti se naložijo z PageNumber enakim ničli, ohranijo svoje izvirne bajte v predmetu in se zapišejo nazaj dobesedno, razen če na njih izrecno pokličete Retarget

  • Poimenovani cilj je ključ v drevo imen dokumenta, zato njegova pravilna preslikava pomeni razrešiti drevo in prepisati ciljni vnos, ne ugibati na ravni orisa
  • Dejanje /URI, /Launch ali JavaScript sploh nima semantike strani in se ne sme tiho pretvoriti v Go-To
  • Ključi, specifični za ponudnika, in strukturni cilji so ohranjeni, ker je spuščanje tistega, česar ne razumete, način, kako izmenjave izgubijo podatke

Cena je resnična in vredna jasne izjave: ApplyPageMap te predmete popolnoma preskoči, zato bo dokument, katerega zaznamki vsi uporabljajo poimenovane cilje, prišel skozi brisanje strani s strukturno veljavnim in semantično zastarelim orisom. To je namerna izbira — zastarela povezava, ki jo recenzent lahko ujame, je boljša od samozavestno napačne, ki jo nihče ne opazi. Če razvrščate dohodne datoteke, preden jih urejate, vam prehod inventarja v pregledni delovni mizi za sprejem PDF pove, kateri dokumenti padejo v to vedro

Shranjevanje: prirastna revizija, nato neodvisno ponovno nalaganje

TPdfOutlineEditor.SaveIncremental pripne redko prirastno revizijo namesto da bi prepisal datoteko. Predmeti, ki so bili naloženi, ohranijo svoj izvirni posredni predmetni sklic, vključno s točno generacijo, zato obstoječi križni sklici ostanejo veljavni; le predmeti, ki ste jih dodali, žrebujejo svežo številko, dodeljeno od ena nad največjo predmetno številko revizije. Katalog se posodobi v isti reviziji, manjkajoč vnos /Outlines pa se mu doda, ko vir sploh ni imel orisa

Kaj se zgodi po zapisu, je del vreden kopiranja. PDFiumPas znova odpre ciljni tok s povsem neodvisnim urejevalnikom in primerja ponovno naloženo drevo z tistim v pomnilniku — števec predmetov, naslovi, številke strani, pripone ciljev, oblika dejanje-proti-neposrednemu-cilju, slogi, stanje razširjenosti in starševska razmerja. Vsako neujemanje ali vsak spodletel nalaganje počisti ciljni tok in vrne poviVerificationFailure namesto da bi vam podala datoteko, ki izgleda verjetno. Šifrirani viri so zavrnjeni na vnaprej s poviEncryptedInput, saj novi naslovi in cilji ustvarjajo vsebino nizov, ki je ni moč dati s kopiranjem priklopnika /Encrypt naprej

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;

Ravnajte z orisom kot s tem, kar je — povezani graf predmetov s svojimi invariantami — in brisanje strani preneha biti katastrofa zaznamkov ter postane karta strani, ki jo predate enemu klicu metode. TPdfOutlineEditor, ApplyPageMap in preverjeni prirastni pisalnik prihajajo v PDFiumPas od v3.98.0 za Delphi, C++Builder in Lazarus; celoten API si lahko ogledate in poskusno različico prenesete na strani izdelka PDFium Delphi Component