Замяната на страница 3 от подписан по договор не бива да мести съдържанието. Изтрийте старата страница, вмъкнете новата и всеки bookmark, сочил дотам преди, вече кацва някъде другаде. PDFlibPas Delphi PDF library избягва това, като запазва самия обект на целевата страница и прехвърля само записите, носещи визуално съдържание
Защо се чупят bookmark-ите след замяна на PDF страница?
Bookmark-ите се чупят, защото PDF дестинация назовава страница чрез indirect референция към обект, не чрез номер на страница. ISO 32000-1 §12.3.2.2 дефинира explicit дестинация като масив, чийто първи елемент е indirect референция към обекта на страницата. Изтрийте този обект и добавете заместник, и референцията виси: повечето viewer-и реагират, като хвърлят читателя на страница 1, което е точно симптомът, докладван след замяна тип изтриване-после-вмъкване. Дървото на страниците изглежда перфектно, броят на страниците е правилен, рендирането е правилно, а целият навигационен слой е тихо грешен
Именуваните дестинации също не ви спасяват. §12.3.2.3 насочва име през name tree /Dests в каталога на документа, но листото, до което името се разрешава, все още е explicit масив за дестинация, държащ същата референция към страницата. Именуването добавя слой непряка връзка над референцията към страницата, не около нея. Същата логика покрива останалата част от интерактивния слой, описан в §12.5: link анотация носи /Dest или GoTo екшън /A, чийто /D е този масив, всяка анотация може да носи запис /P, който е indirect референция към нейната страница, а widget на form поле е анотация на точно същите основания. Една наивна размяна на страница откача четири подсистеми наведнъж, и ако искате да ги видите изброени в реален файл, същият граф от обекти е това, което интроспекцията на outline и анотации обхожда
Кои записи в страницата носят идентичност и кои носят изглед
Речникът на страница смесва два вида записи, и замяна на място успява точно когато ги разделите. Визуалната страна е крайна и изброима: /Contents, /Resources, петте кутии на страницата /MediaBox, /CropBox, /BleedBox, /TrimBox и /ArtBox, плюс /Rotate, /Group, /UserUnit и /BoxColorInfo. Тези единадесет записа решават всичко, което растеризатор произвежда за страницата, и нищо друго във файла не сочи към тях по име
Страната на идентичността е това, към което останалата част от документа се е обвързала: номерът и поколението на обекта-страница, обратната връзка /Parent в дървото на страниците и /Annots. PDFlibPas пази всяко от тях недокоснато. ReplacePageRanges прочиства единадесетте визуални записа от речника на целевата страница и ги добавя отново от импортираната изходна страница, така че обектът на целевата страница се мутира на място, вместо да бъде заменен. Структурата на дървото на страниците, изисквана от §7.7.3, също остава байт-идентична по форма: редът на /Kids, /Count, и всеки оцелял /Parent са същите преди и след, защото никой node никога не е бил откачен
Как PDFlibPas заменя страница без преномериране на обекти?
Извикването приема изходен документ, 1-базирана начална страница на целта, изразяваш обхват на източника и флаг за опции. Двата документа трябва да са отворени в една и съща инстанция, а целевият документ е избраният. Понеже броят страници на целта никога не се променя, поисканият диапазон трябва да се побере в документа, започвайки от TargetStartPage, и това се проверява преди каквото и да е да бъде създадено
var
Lib: TPDFlib;
TargetDoc, SourceDoc: Integer;
begin
Lib := TPDFlib.Create;
try
// The document whose bookmarks and links must survive
if Lib.LoadFromFile('contract-final.pdf', '') <> 1 then
Exit;
TargetDoc := Lib.SelectedDocument;
// The revised clause page, rendered by whatever produced it
if Lib.LoadFromFile('clause-7-revised.pdf', '') <> 1 then
Exit;
SourceDoc := Lib.SelectedDocument;
Lib.SelectDocument(TargetDoc);
// Source page 1 overwrites the visuals of target page 3.
// Page count, page 3 object number, bookmarks and annotations are kept.
if Lib.ReplacePageRanges(SourceDoc, 3, '1', 0) = 1 then
Lib.SaveToFile('contract-final.pdf');
finally
Lib.Free;
end;
end;
Вътрешно изходните страници не могат просто да бъдат прочетени през границите на документа, защото всяка indirect референция вътре в тях принадлежи на номерацията на обектите на източника. Затова диапазонът на източника първо се импортира по обичайния начин, като временни страници, добавени след последната реална страница, което изпълнява пълното премапиране на графа от обекти: content streams, шрифтове, XObjects, shadings и цветови пространства всички се преномерират в целевия документ. Едва тогава единадесетте визуални записа се копират от всяка временна страница върху нейната целева страница, и едва тогава временните страници се откачат от дървото на страниците. Работата по премапирането се случва там, където е евтина и безопасна, а деструктивната редакция се свежда до размяна на ниво речник върху страници, които вече съществуват
Пътят за изтриване, който би унищожил това, което току-що сте прехвърлили
Премахването на тези временни страници е стъпката, която изглежда тривиална и не е. Обикновеният път за изтриване на страница в библиотеката прави повече от откачане на node: той комбинира слоевете на всяка изтривана страница, изпразва първия content stream и прибира ресурси, които никоя друга страница не споделя. Това е правилно поведение за истинско изтриване, и катастрофално тук, защото към момента, в който временните страници се премахват, целевите страници вече реферират точно тези content streams и ресурсни обекти. Изпразването им би заличило страницата, която току-що сте заменили, а прочистването на ресурси би прибрало шрифтове и изображения, които сега имат жив собственик
Поправката е режим за запазване на реферираните обекти на вътрешния път за изтриване. Когато е зададен, изтриването пропуска както прочистването на несподелените ресурси, така и изчистването на content stream, и не прави нищо друго освен да откачи страниците от дървото на страниците и да оправи счетоводството на дървото. Прехвърлените обекти оцеляват с нов собственик, а собствеността върху обектите след операцията е това, което бихте начертали на дъска: един content stream, една притежаваща страница, един номер на обект, който никога не се е местил. Свързаните правила за жизнения цикъл при създаване, изтриване и пренареждане на страници са разгледани отделно в бележките за операции по жизнения цикъл на документ и страници
Ред, дубликати и провал от типа "всичко или нищо"
Флагът за опции избира как се интерпретира диапазонът на източника. 0 сортира разчетените номера на страници и премахва дубликатите, което е разумната стойност по подразбиране, когато извикващият подаде нещо като '4-6,2' и просто иска тези четири страници. 1 запазва реда, който сте написали, и позволява страница да се повтори, така че '2,1,2' реално означава три замени, взети от две страници на източника. Валидацията върви първо и върви напълно: синтаксисът на диапазона, всеки номер на страница спрямо броя страници на източника, самата стойност на опцията и капацитетът на целта — всички се проверяват преди да бъде създаден и един обект. Отхвърлено извикване задава LastErrorCode на 412, възстановява предишно избраната страница и оставя документа точно такъв, какъвто е бил
var
Replaced: Integer;
begin
Lib.SelectDocument(TargetDoc);
// Options = 1: source order is preserved and repeats are allowed, so
// target pages 5, 6 and 7 receive source pages 2, 1 and 2 respectively
Replaced := Lib.ReplacePageRanges(SourceDoc, 5, '2,1,2', 1);
if Replaced = 0 then
raise Exception.CreateFmt('Replacement rejected, LastErrorCode = %d',
[Lib.LastErrorCode]);
// On success the selection is the first replaced page
Assert(Lib.SelectedPage = 5);
end;
Атомарността продължава отвъд валидацията в самото прехвърляне. Преди първата изходна страница да бъде импортирана, единадесетте визуални записа на всяка целева страница в диапазона се снимат като кодирани стойности. Ако импортът се провали или броят на импортираните страници не съвпада с поискания, снимките се декодират обратно върху целевите страници, а временните страници се премахват, така че провал по средата все пак оставя оригиналните визуализации на място върху оригиналните им обекти. Това има по-голямо значение, отколкото звучи: полузаменен диапазон от страници в договор е по-лош от неуспешно извикване, защото нищо във файла не го маркира като полу-свършено
// Post-conditions worth asserting in a regression test
Lib.SelectPage(3);
// Geometry now comes from the source page
WriteLn(Format('%.2f x %.2f', [Lib.PageWidth, Lib.PageHeight]));
// Annotations that were already on target page 3 are still attached
WriteLn(Lib.AnnotationCount);
// The bookmark created before the replacement still resolves to page 3
WriteLn(Lib.GetOutlinePage(OutlineID));
// And the document is still the same length
WriteLn(Lib.PageCount);
Какво замяната на място все още не прави за вас?
Анотациите на източника, form полетата на източника и outline-ите на източника умишлено не се импортират. Пренасянето на widget без записа му в поле в /AcroForm, или анотация, носеща marked content, без притежанието ѝ в дървото на структурата, произвежда полу-импортиран интерактивен обект, който никой viewer не може да осмисли, така че операцията прехвърля само визуалния изглед. Практическото следствие е, че ако заменящата страница трябва да носи нови form полета или нови връзки, вие ги добавяте към целевата страница след това, върху обекта на целевата страница, който все още стои и ги чака
Още две граници си струва да проверите върху собствените си файлове. Първо, /Annots се запазва, но геометрията на страницата не се, така че замяната на страница 220 мм с 320 мм оставя правоъгълниците на анотациите на старите им координати вътре в по-различно оразмерен /MediaBox; ако геометрията се променя, преместете запазените анотации. Второ, записи извън единадесетте визуални ключа остават с целевата страница по замисъл, което е правилно за /Trans или /AA и остаряло за /Thumb, така че регенерирайте миниатюрите след замяна. Тагнатите документи заслужават още едно съображение: елементите на структурата все още сочат към правилния обект-страница чрез /Pg, но техните идентификатори на marked content описват съдържание, което вече го няма, така че размяна на страница вътре в PDF/UA workflow е едновременно редакция на дървото на структурата и на съдържанието. Ако задачата ви наистина е композиция, а не размяна — наслагване на артуърк върху страници, които запазвате — подходът с зашиване на страници и шаблони е по-евтиният инструмент
Всичко описано тук, включително синтаксисът на израза за диапазон, стойностите на опциите и заобикалящото API за манипулация на страници, се доставя в стандартната PDFlibPas Delphi PDF Library за Delphi и C++Builder, чиято референтна документация носи пълния запис за извикването за замяна на страници и неговите кодове за грешка