Ejtsen ki hét lapot egy 200 oldalas kézikönyvből, és minden könyvjelző valahol rossz helyre esik. A megoldás nem a vázlat újjáépítése egy lapos címlistából. A PDFiumPas a TPdfOutlineEditor-t tárja fel, amely betölti a valódi vázlatfát, lehetővé teszi az elemek mozgatását és átirányítását, majd lefuttatja az ApplyPageMap-et, hogy minden explicit cél áthelyeződjön az Ön lapterve szerint
Miért töri el a lapok törlése az összes könyvjelzőt?
Mert egy vázlatelem nem lapszámot tárol. Egy oldalobjektumra mutató hivatkozást tárol, és amikor az oldalobjektumok változnak, a hivatkozás vagy egy áthelyezett lapra mutat, vagy semmire sem. Az ISO 32000-1 §12.3.2.2 az explicit célt olyan tömbként definiálja, amelynek első eleme egy oldalszótárra mutató indirekt hivatkozás, amelyet olyan fit-név követ, mint a /Fit vagy a /XYZ. Törölje a lapot, és függő hivatkozás marad; rendezze át a lapokat, és a hivatkozás továbbra is érvényes, de most egy másik fejezetet ír le. A PDFiumPas betöltéskor visszaoldja azt a tömböt lapszámra, így a TPdfOutlineItem.PageNumber egyalapú lapindexet ad, amely a nyilvános TPdf API-hoz illeszkedik, nem objektumszámot. Ez az absztrakció teljes értelme: az átrendezési logikája ugyanabban a koordinátarendszerben dolgozik, mint a lapterv, amelyet már felépített, amikor szétvágta, átrendezte vagy nyomdailag előkészítette a dokumentumot. Ha éppen azt a tervet építi, ugyanaz az egyalapú konvenció fut végig a PDF-dokumentumok több fájlra bontása és a n-up imposition és lapátrendezés cikkekben is
A vázlat duplán láncolt fa, nem lista
Az nem megy, hogy egyszerűen sorosítsa a címek lapos tömbjét, mert az ISO 32000-1 §12.3.3 minden vázlatelemet öt külön láncra köt: /Parent, /Prev, /Next, /First és /Last. Egyetlen részfa áthelyezése ezért újraírja a régi szülőt, az új szülőt, a vágás és a beszúrás pontján mindkét oldalon a szomszédos testvéreket, és az áthelyezett csomópont szülő-mutatóját is. Ha az egyiket elrontja, a szabványkövető megjelenítők csonka fát vagy hurkot mutatnak. A PDFiumPas a szerkesztési állapotot mélységi bejárási sorrendű TPdfOutlineItem rekordtömbként tartja stabil egész Id-vel, így egy részfa összefüggő szelet, a testvér-lánc pedig levezetett, sosem kézzel karbantartott. A TPdfOutlineEditor.Move felemeli azt a szeletet, az új szülő alá, a kért testvérindexre szúrja vissza, és csak a blokk gyökerét rendeli újra. Emellett visszautasítja a két mozgatást, amely megrontaná a gráfot: egy elem mozgatását a saját részfájába, és egy nem létező szülő megnevezését
Miért előjeles a /Count?
Mert az előjel hordozza a kinyitott állapotot, nem a méretet. A pozitív /Count azt jelenti, hogy az elem nyitva van, és a szám azt mutatja, hány leszármazott látható jelenleg; a negatív /Count azt jelenti, hogy az elem becsukva van. A PDFiumPas minden gyerekkel rendelkező elemhez kiírja a leszármazottszámot, és negálja, amikor az IsOpen False, betöltéskor pedig IsOpen := HasCount and (CountValue > 0) alakban olvassa vissza az állapotot. Ez a leggyakoribb kézzel írt hiba a vázlatírókban: előjel nélküli count kiírása, amely csendben az egész fát kinyitva kényszeríti
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); // a gyökér második gyereke lesz
Editor.SetTitle(ChapterId, 'Appendix B');
Editor.SetStyle(ChapterId, [posBold, posItalic]);
Editor.SetColor(ChapterId, 0.25, 0.5, 0.75);
Editor.SetExpanded(RootId, False); // negatív /Count-ot ír
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;
A Retarget a specifikáció által engedett mindkét alakot kezeli. DestinationInAction False mellett a PDFiumPas közvetlen /Dest tömböt ír; True mellett Go-To műveletet ír, /A << /S /GoTo /D [ page ref suffix ] >> alakban, az ISO 32000-1 §12.6.4.2 szerint. Mindkét esetben előbb letisztítja az elemről a meglévő /Dest-et és /A-t, hogy a kettő nem létezhet együtt és nem ellentmondhat egymásnak. Az utótag alapértéke /Fit, és PDF-névvel kell kezdődnie, ezért az üres vagy rosszul formált utótag azonnal kivételt dob, ahelyett hogy olyan céltömböt adna, amelyet egyetlen megjelenítő sem tud értelmezni
Hogyan fogyasztja el az ApplyPageMap a laptervet?
Az ApplyPageMap pontosan azt a tömböt kapja, amelyet a lapterve már validált: NewPageNumbers, a régi lap mínusz eggyel indexelve, amely az új egyalapú lapszámot tartja, vagy nullát, amikor az a lap nem élte túl. Visszafelé járja az elemtömböt, hogy egy részfa törlése sosem érvénytelenítsen egy még be nem járt indexet, és a tevékenységét a RemappedDestinationCount és a RemovedDanglingItemCount révén jelenti
var
NewPageNumbers: array of Integer;
Report: TPdfOutlineValidationReport;
I: Integer;
begin
// Egy bejegyzés az EREDETI dokumentum lapjanként
SetLength(NewPageNumbers, OriginalPageCount);
for I := 0 to OriginalPageCount - 1 do
NewPageNumbers[I] := 0; // 0 == ez a lap eldobásra került
NewPageNumbers[0] := 1; // régi 1. lap -> új 1. lap
NewPageNumbers[1] := 2;
NewPageNumbers[9] := 3; // régi 10. lap -> új 3. lap
// True: a teljes függő részfa törlése. False: az elem megtartása, a céljának leválasztása
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;
A DeleteDangling jelző dönt a nullára képezett cél politikájáról, és mindkét ág szándékos. True esetén a PDFiumPas törli az elemet és a teljes részfáját, mert egy eltűnt célú vázlatcsomópont általában olyan fejezetet vezetett, amely együtt tűnt el. False esetén az elem túléli a címével és hierarchiájával épségben, de /Dest és /A nélkül, ami az, amit akkor szeret, amikor egy ember fogja átirányítani lektori átvizsgálás közben. A ténylegesen rosszul formált bemenet továbbra is hangosan hibázik a foltozás helyett: negatív bejegyzés vagy a szolgáltatott térképen túlnyúló cél False-ot ad, IssueKind értéke poviInvalidPageMap
Átlátszatlan bejegyzések és az őszinte kompromisszum
Nem minden vázlatelemhez tartozik olyan lapszám, amelyről a PDFiumPas tud érvelni. Három fajta érintetlenül kerül át: a nevesített célok, a nem /S /GoTo műveletek, és az ismeretlen szótárkulcsok, amelyeket a fájl előállítója hozzátett. Ezek PageNumber értéke nullával töltődnek be, megtartják eredeti bájtaikat az elemben, és szó szerint visszaíródnak, hacsak Ön nem hív rajtuk kifejezetten Retarget-et
- A nevesített cél a dokumentum névfájának kulcsa, ezért helyes átrendezése azt jelenti, hogy feloldja a fát és újraírja a célbejegyzést, nem találgat a vázlat szintjén
- Egy
/URI,/Launchvagy JavaScript műveletnek egyáltalán nincs lapszemantikája, és nem csendesíthető át Go-To-vá - A gyártóspecifikus kulcsok és a struktúracélok megőrzésre kerülnek, mert az ismeretlen eldobása az, ahogyan a round-tripek adatot veszítnek
Az ára valódi, és érdemes egyenesen kimondani: az ApplyPageMap ezeket az elemeket teljesen kihagyja, így egy olyan dokumentum, amelynek könyvjelzői mind nevesített célokat használnak, egy laptörlésen strukturálisan érvényes, de szemantikailag elavult vázlattal megy keresztül. Ez a szándékos döntés — egy elavult hivatkozás, amelyet a lektor észrevesz, jobb, mint egy magabiztosan téves, amelyet senki nem vesz észre. Ha a beérkező fájlokat szerkesztés előtt szűri, egy leltárjárás egy PDF intake átvizsgáló munkapadon megmondja, mely dokumentumok esnek ebbe a vödörbe
Mentés: inkrementális revízió, majd független újratöltés
A TPdfOutlineEditor.SaveIncremental ritka inkrementális revíziót fűz hozzá a fájlhoz, nem pedig újraírja azt. A betöltött elemek megtartják eredeti indirekt objektumhivatkozásukat a pontos generációval együtt, így a meglévő kereszthivatkozások érvényesek maradnak; csak a hozzáadott elemek kapnak friss számot, amelyet a revízió maximális objektumszáma után egytől osztanak ki. A katalógus ugyanabban a revízióban frissül, és hiányzó /Outlines bejegyzés hozzáadódik hozzá, ha a forrásnak egyáltalán nem volt vázlata
Az írás után történő az a rész, amelyet érdemes átvenni. A PDFiumPas teljesen független szerkesztővel nyitja újra a célstreamet, és összehasonlítja az újratöltött fát a memóriabelivel — elemszám, címek, lapszámok, cél-utótagok, művelet-közvetlen cél forma, stílusok, kinyitott állapot és szülői viszonyok. Bármilyen eltérés vagy bármilyen betöltési hiba törli a célstreamet, és poviVerificationFailure-t ad, hitelesnek tűnő fájl helyett. A titkosított források előre visszautasításra kerülnek poviEncryptedInput-tal, mivel az új címek és célok olyan sztringtartalmat hoznak létre, amely az /Encrypt trailer előremásolásával nem állítható elő
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;
Kezelje a vázlatot annak, ami — egy saját invariánsokkal rendelkező láncolt objektumgráf —, és a lap törlése már nem könyvjelző-katasztrófa, hanem egy laptérkép, amelyet egyetlen metódushívásnak ad át. A TPdfOutlineEditor, az ApplyPageMap és az ellenőrzött inkrementális író a PDFiumPasban a v3.98.0-tól szállít Delphi, C++Builder és Lazarus alá; a teljes API-t áttekintheti és próbaverziót tölthet le a PDFium Delphi Component termékoldalon