Zamiana strony 3 zaakceptowanej umowy nie powinna przesuwać spisu treści. Usuń starą stronę, wstaw nową, a każda zakładka, która kiedyś tam wskazywała, ląduje teraz gdzie indziej. Biblioteka PDFlibPas Delphi PDF unika tego, zachowując sam docelowy obiekt strony i przenosząc tylko wpisy niosące treść wizualną
Dlaczego zakładki psują się po zamianie strony PDF?
Zakładki psują się, ponieważ cel PDF nazywa stronę przez odwołanie do obiektu pośredniego, nie przez numer strony. ISO 32000-1 §12.3.2.2 definiuje jawny cel jako tablicę, której pierwszym elementem jest odwołanie pośrednie do obiektu strony. Usuń ten obiekt i dopisz zastępczy, a odwołanie wisi w powietrzu: większość przeglądarek reaguje, lądując czytelnika na stronie 1, co jest dokładnie objawem zgłaszanym po zamianie typu usuń-a-potem-wstaw. Drzewo stron wygląda idealnie, liczba stron jest poprawna, renderowanie jest poprawne, a cała warstwa nawigacji jest po cichu błędna
Nazwane cele też cię nie ratują. §12.3.2.3 kieruje nazwę przez drzewo nazw /Dests w katalogu dokumentu, ale liść, do którego rozwiązuje się ta nazwa, to nadal tablica jawnego celu trzymająca to samo odwołanie do strony. Nadawanie nazw dodaje warstwę pośredniości ponad odwołaniem do strony, nie wokół niego. To samo rozumowanie obejmuje resztę warstwy interaktywnej opisanej w §12.5: adnotacja typu link niesie /Dest lub akcję GoTo /A, której /D jest tą tablicą, każda adnotacja może nieść wpis /P, który jest odwołaniem pośrednim do swojej strony, a widget pola formularza jest adnotacją na dokładnie tych samych zasadach. Jedna naiwna wymiana strony odłącza cztery podsystemy naraz, a jeśli chcesz zobaczyć je wyliczone na prawdziwym pliku, ten sam graf obiektów jest tym, co przechodzi introspekcja konspektu i adnotacji
Które wpisy strony niosą tożsamość, a które wygląd
Słownik strony miesza dwa rodzaje wpisów, a zamiana w miejscu udaje się dokładnie wtedy, gdy je rozdzielisz. Strona wyglądu jest skończona i wyliczalna: /Contents, /Resources, pięć ramek strony /MediaBox, /CropBox, /BleedBox, /TrimBox i /ArtBox, plus /Rotate, /Group, /UserUnit i /BoxColorInfo. Te jedenaście wpisów decyduje o wszystkim, co rasterizer wytwarza dla strony, i nic innego w pliku nie odnosi się do nich po nazwie
Strona tożsamości to to, do czego przywiązała się reszta dokumentu: numer obiektu strony i generacja, wsteczny link /Parent do drzewa stron oraz /Annots. PDFlibPas zachowuje każdy z nich nietknięty. ReplacePageRanges usuwa jedenaście wpisów wizualnych ze słownika docelowej strony i dodaje je ponownie z importowanej strony źródłowej, więc obiekt docelowej strony jest mutowany w miejscu, a nie zastępowany. Struktura drzewa stron wymagana przez §7.7.3 również pozostaje identyczna co do bajtu w kształcie: kolejność /Kids, /Count i każdy przetrwały /Parent są takie same przed i po, ponieważ żaden węzeł nigdy nie został odłączony
Jak PDFlibPas zamienia stronę bez przenumerowania obiektów?
Wywołanie przyjmuje dokument źródłowy, docelową stronę początkową liczoną od 1, wyrażenie zakresu źródłowego i flagę opcji. Oba dokumenty muszą być otwarte w tej samej instancji, a dokument docelowy to ten wybrany. Ponieważ liczba stron docelowych nigdy się nie zmienia, żądany zakres musi zmieścić się w dokumencie zaczynającym się od TargetStartPage, i to jest sprawdzane, zanim cokolwiek zostanie utworzone
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;
Wewnętrznie strony źródłowe nie mogą być po prostu odczytywane w poprzek granic dokumentów, ponieważ każde odwołanie pośrednie w nich należy do numeracji obiektów źródła. Więc zakres źródłowy jest najpierw importowany w zwykły sposób, jako strony tymczasowe dopisane po ostatniej prawdziwej stronie, co uruchamia pełne przemapowanie grafu obiektów: strumienie treści, czcionki, XObjecty, cieniowania i przestrzenie kolorów są wszystkie przenumerowane do dokumentu docelowego. Dopiero wtedy jedenaście wpisów wizualnych jest kopiowanych z każdej tymczasowej strony na jej docelową stronę, i dopiero wtedy strony tymczasowe są odłączane od drzewa stron. Praca przemapowania odbywa się tam, gdzie jest tania i bezpieczna, a destrukcyjna edycja zostaje zredukowana do zamiany na poziomie słownika na stronach, które już istnieją
Ścieżka usuwania, która zniszczyłaby to, co właśnie przeniosłeś
Usuwanie tych tymczasowych stron to krok, który wygląda trywialnie, a nie jest. Zwykła ścieżka usuwania strony w bibliotece robi więcej niż odłączenie węzła: łączy warstwy każdej usuwanej strony, opróżnia pierwszy strumień treści i odzyskuje zasoby niedzielone przez żadną inną stronę. To poprawne zachowanie dla prawdziwego usunięcia i katastrofalne tutaj, ponieważ zanim tymczasowe strony zostaną usunięte, docelowe strony już odwołują się dokładnie do tych strumieni treści i obiektów zasobów. Opróżnienie ich wyczyściłoby stronę, którą właśnie zamieniłeś, a zamiatanie zasobów zebrałoby czcionki i obrazy, które mają teraz żywego właściciela
Rozwiązaniem jest tryb zachowania odwoływanych obiektów na wewnętrznej ścieżce usuwania. Gdy jest ustawiony, usuwanie pomija zarówno zamiatanie niedzielonych zasobów, jak i czyszczenie strumienia treści, i nie robi nic poza odłączeniem stron od drzewa stron i naprawą księgowości drzewa. Przeniesione obiekty przetrwają z nowym właścicielem, a własność obiektów po operacji to coś, co narysowałbyś na tablicy: jeden strumień treści, jedna posiadająca strona, jeden numer obiektu, który się nie poruszył. Powiązane zasady cyklu życia dotyczące tworzenia, usuwania i zmiany kolejności stron są omówione osobno w notatkach o operacjach cyklu życia dokumentu i strony
Kolejność, duplikaty i porażka wszystko-albo-nic
Flaga opcji wybiera sposób interpretacji zakresu źródłowego. 0 sortuje sparsowane numery stron i usuwa duplikaty, co jest rozsądną wartością domyślną, gdy wywołujący przekazuje coś w rodzaju '4-6,2' i po prostu ma na myśli te cztery strony. 1 zachowuje napisaną przez ciebie kolejność i pozwala stronie się powtórzyć, więc '2,1,2' naprawdę oznacza trzy zamiany wzięte z dwóch stron źródłowych. Walidacja jest uruchamiana najpierw i uruchamiana w całości: składnia zakresu, każdy numer strony względem liczby stron źródłowych, sama wartość opcji i pojemność docelowa są wszystkie sprawdzane, zanim powstanie choćby jeden obiekt. Odrzucone wywołanie ustawia LastErrorCode na 412, przywraca poprzednio wybraną stronę i pozostawia dokument dokładnie takim, jaki był
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;
Atomowość rozciąga się poza walidację, aż do samego przenoszenia. Zanim pierwsza strona źródłowa zostanie zaimportowana, jedenaście wpisów wizualnych każdej docelowej strony w zakresie zostaje zrzuconych jako zakodowane wartości. Jeśli import się nie powiedzie lub zaimportowana liczba stron nie zgadza się z żądaną, zrzuty są dekodowane z powrotem na strony docelowe, a strony tymczasowe są usuwane, więc niedokończona w połowie awaria wciąż pozostawia oryginalne wyglądy na miejscu na ich oryginalnych obiektach. Ma to większe znaczenie, niż się wydaje: strona zamieniona w połowie w umowie jest gorsza niż nieudane wywołanie, ponieważ nic w pliku nie oznacza jej jako połowicznie wykonanej
// 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);
Czego zamiana w miejscu wciąż dla ciebie nie robi?
Adnotacje źródłowe, pola formularza źródłowego i konspekty źródłowe celowo nie są importowane. Przeniesienie widgetu bez jego wpisu pola /AcroForm, lub adnotacji niosącej treść oznaczoną bez jej własności drzewa struktury, produkuje na wpół zaimportowany obiekt interaktywny, którego żadna przeglądarka nie potrafi zrozumieć, więc operacja przenosi tylko wygląd. Praktyczną konsekwencją jest to, że jeśli strona zastępcza ma nieść nowe pola formularza lub nowe linki, dodajesz je do docelowej strony potem, do obiektu docelowej strony, który wciąż tam siedzi, czekając na nie
Dwie kolejne granice warto sprawdzić na własnych plikach. Po pierwsze, /Annots jest zachowywane, ale geometria strony nie, więc zamiana strony 220 mm na stronę 320 mm zachowuje prostokąty adnotacji na ich starych współrzędnych wewnątrz inaczej rozmiarowanego /MediaBox; jeśli geometria się zmienia, przemieść zachowane adnotacje. Po drugie, wpisy poza jedenastoma kluczami wizualnymi pozostają przy docelowej stronie z założenia, co jest poprawne dla /Trans lub /AA i nieaktualne dla /Thumb, więc regeneruj miniatury po zamianie. Dokumenty otagowane wymagają jednej dodatkowej myśli: elementy struktury nadal wskazują na poprawny obiekt strony przez /Pg, ale ich identyfikatory treści oznaczonej opisują treść, której już tam nie ma, więc wymiana strony w workflow PDF/UA jest edycją drzewa struktury tak samo, jak edycją treści. Jeśli twoim zadaniem jest naprawdę kompozycja, a nie wymiana — warstwowanie grafiki na stronach, które zachowujesz — podejście zszywania stron i szablonów jest tańszym narzędziem
Wszystko opisane tutaj, włącznie ze składnią wyrażeń zakresu, wartościami opcji i otaczającym API manipulacji stronami, jest dostarczane w standardowej PDFlibPas Delphi PDF Library dla Delphi i C++Builder, której dokumentacja referencyjna zawiera pełny wpis dla wywołania zamiany strony i jego kodów błędów