Artykuł techniczny

Zamiana stron PDF w Delphi bez psucia zakładek

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 PDF Library for Delphi 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

Diagram porównawczy PDF Library for Delphi destynacji zakładki nazwanej odniesieniem pośrednim przeżywającej wymianę strony w miejscu, ale zwisającej po zamianie usuń-i-dodaj
Cele wiążą zakładki, łącza i widżety z numerem obiektu strony, więc mutacja tego obiektu w miejscu utrzymuje nawigację przy życiu, tam gdzie usuń-i-wstaw zrzuca czytników na stronę 1

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. PDF Library for Delphi 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 PDF Library for Delphi 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
    // Dokument, którego zakładki i linki muszą przetrwać
    if Lib.LoadFromFile('contract-final.pdf', '') <> 1 then
      Exit;
    TargetDoc := Lib.SelectedDocument;

    // Strona poprawionej klauzuli, wyrenderowana przez cokolwiek, co ją wyprodukowało
    if Lib.LoadFromFile('clause-7-revised.pdf', '') <> 1 then
      Exit;
    SourceDoc := Lib.SelectedDocument;

    Lib.SelectDocument(TargetDoc);
    // Strona źródłowa 1 nadpisuje wygląd strony docelowej 3.
    // Liczba stron, numer obiektu strony 3, zakładki i adnotacje są zachowane.
    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

PDF Library for Delphi: Anatomia słownika strony rozdzielająca wpisy tożsamości, od których plik zależy, od jedenastu wizualnych wpisów wymienianych przez ReplacePageRanges z zaimportowanej strony źródłowej
ReplacePageRanges czyści jedenaście wizualnych kluczy i dodaje je ponownie z importu, podczas gdy numer obiektu, generacja, /Parent i /Annots pozostają dokładnie takie, jakie były

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ł

PDF Library for Delphi: Trzystopniowy przepływ ReplacePageRanges pokazujący tymczasowy import z remapowaniem obiektów, kopiowanie wpisów wizualnych i odłączanie z zachowaniem odniesień oszczędzające przeniesione zasoby
Import źródła jako stron tymczasowych pozwala zwykłemu remapowaniu biec najpierw, więc destrukcyjna edycja kurczy się do kopiowania wizualnych kluczy i odczepiania węzłów bez odzyskiwania żywych zasobów
var
  Replaced: Integer;
begin
  Lib.SelectDocument(TargetDoc);
  // Options = 1: kolejność źródłowa jest zachowana, a powtórzenia dozwolone, więc
  // strony docelowe 5, 6 i 7 otrzymują odpowiednio strony źródłowe 2, 1 i 2
  Replaced := Lib.ReplacePageRanges(SourceDoc, 5, '2,1,2', 1);
  if Replaced = 0 then
    raise Exception.CreateFmt('Replacement rejected, LastErrorCode = %d',
      [Lib.LastErrorCode]);
  // Po powodzeniu zaznaczeniem jest pierwsza zamieniona strona
  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

// Warunki końcowe warte sprawdzenia w teście regresyjnym
Lib.SelectPage(3);
// Geometria pochodzi teraz ze strony źródłowej
WriteLn(Format('%.2f x %.2f', [Lib.PageWidth, Lib.PageHeight]));
// Adnotacje, które już były na stronie docelowej 3, są nadal dołączone
WriteLn(Lib.AnnotationCount);
// Zakładka utworzona przed zamianą nadal wskazuje na stronę 3
WriteLn(Lib.GetOutlinePage(OutlineID));
// A dokument nadal ma tę samą długość
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 PDF Library for Delphi 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