Изпуснете седем страници от наръчник с 200 страници и всяка закладка каца някъде грешно. Фиксът не е преизграждане на очертанията от плосък списък със заглавия. PDFiumPas излага TPdfOutlineEditor, който зарежда истинското дърво на очертанията, позволява ви да местите и пренасочвате елементи, после пуска ApplyPageMap, за да измести всяка изрична дестинация чрез вашия план за страници
Защо изтриването на страници чупи всяка закладка?
Защото елемент на очертания не съхранява номер на страница. Той съхранява препратка към обект страница, а когато обектите страница се променят, препратката или сочи към страница, която се е преместила, или изобщо към нищо. ISO 32000-1 §12.3.2.2 дефинира изрична дестинация като масив, чийто първи елемент е непряка препратка към речник на страница, последван от име на побиране като /Fit или /XYZ. Изтрийте страницата и оставате с висяща препратка; пренаредите страниците и препратката още е валидна, но вече описва различна глава. PDFiumPas разрешава този масив обратно към номер на страница при зареждане, така че TPdfOutlineItem.PageNumber ви дава едно-базиран индекс на страница, който съответства на публичния API на TPdf, а не номер на обект. Това е цялата идея на абстракцията: вашата логика за ремапване работи в същата координатна система като плана за страници, който вече сте изградили, когато сте разделяли, пренареждали или налагали документа. Ако изграждате този план, същата едно-базирана конвенция минава през разделянето на PDF документи на множество файлове и през n-up налагане и пренареждане на страници
Очертанията са двойно свързано дърво, не списък
Причината да не можете просто да сериализирате плосък масив от заглавия е, че ISO 32000-1 §12.3.3 свързва всеки елемент на очертанията в пет отделни връзки: /Parent, /Prev, /Next, /First и /Last. Преместването на едно-единствено поддърво затова преписва стария родител, новия родител, двамата съседни братя от всяка страна на разреза и точката на вмъкване, и родителския указател на самия преместен възел. Сбъркате една от тях и конформните четци показват отрязано дърво или зациклят. PDFiumPas пази състоянието на редактиране като масив в дълбочина от записи TPdfOutlineItem със стабилно цяло число Id, така че поддърво е непрекъснат отрязък, а братската верига се извежда, никога не се поддържа на ръка. TPdfOutlineEditor.Move вдига този отрязък, вмъква го наново под новия родител на заявания братски индекс и преназначава само корена на блока. Той също отказва двете премествания, които биха повредили графа: преместване на елемент в собственото му поддърво и назоваване на родител, който не съществува
Защо /Count е със знак?
Защото знакът носи разширеното състояние, не размера. Положителен /Count значи, че елементът е отворен, а числото е колко наследници са видими в момента; отрицателен /Count значи, че елементът е свит. PDFiumPas записва броя наследници за всеки елемент, който има деца, и го отрича, когато IsOpen е False, а при зареждане чете състоянието обратно като IsOpen := HasCount and (CountValue > 0). Това е най-често срещаният ръчно изкоден бъг в писателите на очертания: излъчване на брой без знак и тихо принуждаване на цялото дърво да се отвори
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); // става второ дете на корена
Editor.SetTitle(ChapterId, 'Appendix B');
Editor.SetStyle(ChapterId, [posBold, posItalic]);
Editor.SetColor(ChapterId, 0.25, 0.5, 0.75);
Editor.SetExpanded(RootId, False); // записва отрицателен /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 обработва двете форми, които спецификацията позволява. Подадете DestinationInAction като False и PDFiumPas записва директен масив /Dest; подадете True и той записва Go-To действие, /A << /S /GoTo /D [ page ref suffix ] >>, съгласно ISO 32000-1 §12.6.4.2. И в двата случая той първо разсъблича всякакви съществуващи /Dest и /A от елемента, така че двете не могат да съществуват едновременно и да не са съгласни. Суфиксът е /Fit по подразбиране и трябва да започва с PDF име, което е причината празен или деформиран суфикс да хвърля веднага, вместо да произвежда масив дестинация, който никой четец не може да парсне
Как ApplyPageMap консумира план за страници?
ApplyPageMap приема точно масива, който вашият план за страници вече е валидирал: NewPageNumbers, индексиран по стара страница минус едно, носещ новия едно-базиран номер на страница или нула, когато тази страница не е оцеляла. Той обхожда масива с елементи отзад напред, така че изтриването на поддърво никога не анулира индекс, който още предстои да посети, и докладва какво е направил чрез RemappedDestinationCount и RemovedDanglingItemCount
var
NewPageNumbers: array of Integer;
Report: TPdfOutlineValidationReport;
I: Integer;
begin
// Един запис на страница от ОРИГИНАЛНИЯ документ
SetLength(NewPageNumbers, OriginalPageCount);
for I := 0 to OriginalPageCount - 1 do
NewPageNumbers[I] := 0; // 0 == тази страница е изпусната
NewPageNumbers[0] := 1; // стара страница 1 -> нова страница 1
NewPageNumbers[1] := 2;
NewPageNumbers[9] := 3; // стара страница 10 -> нова страница 3
// True: изтрий цялото висящо поддърво. False: задръж елемента, махни целта му
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;
Флагът DeleteDangling решава политиката за дестинация, която се е картирала към нула, и двата клона са умишлени. При True PDFiumPas изтрива елемента и цялото му поддърво, защото възел на очертания, чиято цел е изчезнала, обикновено оглавява глава, изчезнала заедно с него. При False елементът оцелява със запазено заглавие и йерархия, но с махнати /Dest и /A, което е това, което искате, когато човек ще го пренасочва в преглед. Истински деформираният вход пак се проваля шумно, вместо да бъде кръпнат: отрицателен запис или дестинация, сочеща отвъд края на подадения map, връща False с IssueKind зададен на poviInvalidPageMap
Непрозрачни записи и честният компромис
Не всеки елемент на очертания има номер на страница, за който PDFiumPas може да разсъждава. Три вида се прекарват непипнати: именовани дестинации, действия, които не са /S /GoTo, и неизвестни речникови ключове, добавени от това, което е произвело файла. Тези се зареждат с PageNumber равно на нула, запазват оригиналните си байтове в елемента и се записват обратно дословно, освен ако изрично не извикате Retarget върху тях
- Именована дестинация е ключ в дървото от имена на документа, така че коректното ѝ ремапване значи разрешаване на дървото и преписване на целевия запис, а не гадане на ниво очертания
- Действие
/URI,/Launchили JavaScript изобщо няма семантика на страница и не бива тихо да се конвертира в Go-To - Ключове специфични за доставчик и структурни дестинации се запазват, защото изпускането на това, което не разбирате, е начинът обиколките да губят данни
Цената е реална и си заслужава да се каже ясно: ApplyPageMap прескача тези елементи изцяло, така че документ, чиито закладки всички ползват именовани дестинации, ще премине през изтриване на страници с очертания, структурно валидни и семантично остарели. Това е умишленият избор — остаряла връзка, която рецензент може да хване, бие уверена грешна, която никой не забелязва. Ако сортирате входящи файлове, преди да ги редактирате, инвентаризиращо минаване в работна маса за преглед на входящи PDF ще ви каже кои документи попадат в тази кофа
Запазване: инкрементална ревизия, после независимо презареждане
TPdfOutlineEditor.SaveIncremental добавя разредена инкрементална ревизия, вместо да пренаписва файла. Елементи, които са били заредени, запазват оригиналната си непряка обектна препратка, включително точната генерация, така че съществуващите кръстосани препратки остават валидни; само елементи, които сте добавили, теглят свеж номер, алоциран от едно след максималния номер на обект в ревизията. Каталогът се обновява в същата ревизия, а липсващ запис /Outlines се добавя към него, когато източникът изобщо е нямал очертания
Това, което се случва след записа, е частта, която си заслужава да се копира. PDFiumPas отваря наново целевия поток с напълно независим редактор и сравнява презареденото дърво с това в паметта — брой елементи, заглавия, номера страници, суфикси на дестинации, форма действие-срещу-директна дестинация, стилове, разширено състояние и родителски отношения. Всяко несъответствие или провал на зареждане изчиства целевия поток и връща poviVerificationFailure, вместо да ви подаде файл, който изглежда правдоподобно. Криптирани източници се отказват отсреща с poviEncryptedInput, тъй като нови заглавия и дестинации създават низово съдържание, което не може да се произведе чрез копиране напред на трейлъра /Encrypt
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;
Третирайте очертанията като това, което са — свързан обектен граф със свои собствени инварианти — и изтриването на страници спира да е катастрофа за закладките и става карта на страници, която подавате на едно извикване на метод. TPdfOutlineEditor, ApplyPageMap и провереният инкрементален писател се доставят в PDFiumPas от v3.98.0 за Delphi, C++Builder и Lazarus; можете да прегледате пълния API и да изтеглите проба на продуктовата страница на PDFium Delphi Component