Scalanie i dzielenie to dwie operacje na stronach, po które każdy sięga w pierwszej kolejności i które obejmują wiele scenariuszy. Nie obejmują jednak wszystkiego. Istnieje oddzielna rodzina zadań, które zmieniają układ stron zamiast przenosić całe pliki: ułożenie czterech slajdów na jednym arkuszu na potrzeby broszury, przeciągnięcie strony z końca dokumentu na początek lub wyciągnięcie stron 3, 7 i 12 do krótkiego fragmentu bez modyfikowania reszty. Biblioteka PDFium udostępnia trzy metody przeznaczone dokładnie do tego celu, a każda z nich zachowuje się inaczej niż znane już scalanie i dzielenie. W tym artykule przyjrzymy się ich działaniu, punktom wyjściowym i jednemu szczegółowi dotyczącemu własności, który w przeszłości powodował awarie w systemach produkcyjnych.
Te trzy metody to ImportNPagesToOne do impozycji N-up, MovePages do zmiany kolejności w miejscu oraz ImportPagesByIndex do wyodrębniania podzbiorów stron. Scalanie układa dokumenty jeden po drugim, pozostawiając liczbę stron równą sumie dokumentów wejściowych. Dzielenie zapisuje wiele plików wyjściowych z jednego wejściowego. Trzy opisane tutaj operacje plasują się pomiędzy nimi: jedna zmienia liczbę stron źródłowych współdzielących arkusz, druga zmienia kolejność wewnątrz pojedynczego dokumentu, a trzecia kopiuje wybraną garść stron do innego dokumentu. Wiedza o tym, która metoda do czego służy, oszczędza konieczności implementowania skomplikowanego algorytmu scalania i usuwania, gdy wystarczyłoby jedno wywołanie.
Co faktycznie robi impozycja N-up
Impozycja to termin z zakresu DTP oznaczający rozmieszczenie wielu stron źródłowych na jednym większym arkuszu w taki sposób, aby wydrukowany i złożony arkusz czytał się w prawidłowej kolejności. Codzienna wersja to broszura dwustronicowa (2-up), składka czterostronicowa (4-up) lub arkusz styków mieszczący kilkanaście miniatur na stronie. PDFium obsługuje geometrię za pomocą jednego wywołania:
function ImportNPagesToOne(
OutputWidth, OutputHeight: Single;
NumX, NumY : Cardinal): TPdf;
Parametry NumX i NumY opisują siatkę. Wartość 2, 1 umieszcza dwie strony źródłowe obok siebie; 2, 2 pakuje cztery strony w układzie ćwiartkowym; 4, 3 buduje dwunastostronicowy arkusz styków. PDFium odczytuje strony źródłowe po kolei, skaluje każdą z nich w dół, aby dopasować ją do komórki, i wypełnia siatkę od lewej do prawej, od góry do dołu, rozpoczynając nowy arkusz wyjściowy za każdym razem, gdy bieżąca siatka się zapełni. Strony źródłowe nie są modyfikowane. Z powrotem otrzymujesz nowy dokument, którego strony są kompozytami.
Rozmiar wyjściowy podawany jest w punktach, a nie w pikselach
Wartości OutputWidth i OutputHeight to jednostki użytkownika PDF (user units), a jednostka użytkownika PDF to jeden punkt, czyli 1/72 cala. Jednostka ta deklaruje fizyczny rozmiar arkusza wyjściowego i nie ma nic wspólnego z pikselami ekranu czy rozdzielczością renderowania DPI. Jest to najczęstsze miejsce popełniania błędów przy impozycji, ponieważ deweloper przyzwyczajony do map bitowych sięga po liczbę pikseli i kończy z arkuszem wielkości znaczka pocztowego lub bilbordu.
Liczby warte zapamiętania to dwa rozmiary stron, których będziesz używać najczęściej. US Letter to 612 na 792 punkty (ponieważ 8,5 cala pomnożone przez 72 daje 612, a 11 cali pomnożone przez 72 daje 792). Format A4 to w przybliżeniu 595 na 842 punkty, wynikające z jego fizycznych wymiarów 210 na 297 milimetrów. Nagłówek powiązania jasno określa tę regułę (jeden punkt to 1/72 cala), a samo powiązanie udostępnia stałą PointsPerInch równą 72, jeśli wolisz obliczać rozmiar z cali w kodzie zamiast wpisywać wartości bezpośrednio.
const
LetterW = 612.0; // 8.5 in * 72
LetterH = 792.0; // 11 in * 72
var
Source, Composite: TPdf;
begin
Source := TPdf.Create(nil);
Composite := nil;
try
Source.FileName := 'slides.pdf';
Source.Active := True;
// Four source pages per Letter sheet, 2 by 2 grid.
Composite := Source.ImportNPagesToOne(LetterW, LetterH, 2, 2);
if Composite = nil then
raise Exception.Create('PDFium rejected the imposition arguments');
Composite.SaveAs('slides-4up.pdf');
finally
Composite.Free; // see the next section: this is mandatory
Source.Free;
end;
end;
Zwrócony uchwyt należy zwolnić we własnym zakresie
Przeczytaj sygnaturę jeszcze raz. Metoda ImportNPagesToOne zwraca obiekt TPdf, a nie wartość Boolean. Ta zwracana wartość to zupełnie nowy uchwyt dokumentu, przydzielony niezależnie od źródła, a wywołujący staje się jego właścicielem. Źródłowy obiekt TPdf, na którym wywołano metodę, pozostaje nienaruszony i wciąż posiada własny uchwyt; kompozyt to drugi, niezależny obiekt. Jeśli pozwolisz zwróconemu TPdf wyjść poza zakres bez jego zwolnienia, wycieknie cały dokument PDFium.
Bardziej niebezpieczny błąd działa w drugą stronę. Pod maską metoda żąda od PDFium nowego FPDF_DOCUMENT poprzez FPDF_ImportNPagesToOne, a następnie owija ten surowy uchwyt wewnątrz zwracanego TPdf, tak aby czas życia opakowania zarządzał życiem uchwytu. Od tego momentu istnieje dokładnie jeden właściciel uchwytu i dokładnie jedno miejsce, w którym powinien zostać zamknięty: podczas wywołania Free na zwróconym obiekcie. Niestaranne wyczyszczenie błędów, które zarówno zwalnia opakowanie, jak i wywołuje FPDF_CloseDocument na surowym uchwycie, zamknie ten sam dokument PDFium dwukrotnie. Jest to podwójne zwolnienie pamięci (double-free) i to jest właśnie błąd, który przydarzył się jednemu z wywołujących. Zasada zapobiegająca temu jest prosta: zamykaj dokument tylko na jednej ścieżce, zwalniając przekazany obiekt TPdf, i nigdy nie sięgaj poza opakowanie, aby zamknąć uchwyt, który został już przez nie przejęty.
Wynikają z tego dwa wnioski. Po pierwsze, metoda zwraca nil, gdy PDFium odrzuci argumenty (np. zero na którejś osi siatki lub błąd alokacji), dlatego przed użyciem wyniku należy sprawdzić, czy nie jest on równy nil. Po drugie, zainicjuj zmienną wyjściową wartością nil przed blokiem try i zwolnij ją w sekcji finally, tak jak w powyższym przykładzie, aby błąd w środku operacji nie pozostawił Cię ze zwalnianiem niezdefiniowanej referencji lub pominięciem zwalniania w ogóle.
Zmiana kolejności stron bez ich przepisywania
Impozycja buduje nowy dokument. Zmiana kolejności modyfikuje dokument w miejscu. Metoda MovePages pobiera zestaw stron z ich obecnych pozycji i upuszcza je w miejscu docelowym, przesuwając całą resztę wokół przeniesionego bloku, dzięki czemu liczba stron pozostaje bez zmian:
function MovePages(
const PageIndices: array of Integer;
DestPageIndex : Integer): Boolean;
Indeksy są indeksowane od zera. Parametr PageIndices wymienia strony do przeniesienia w kolejności, w jakiej mają się znaleźć, a DestPageIndex to indeks, na którym ląduje pierwsza przeniesiona strona po zakończeniu operacji. Ponieważ PDFium przenosi obiekty stron zamiast kopiować i ponownie kompresować ich zawartość, operacja ta jest szybka i bezstratna: obiekty stron zachowują swoje strumienie, zasoby i wierność. Jest to wywołanie stojące za funkcją przeciągania stron w panelu miniatur, gdzie użytkownik przeciąga miniaturę do nowego gniazda, a Ty zatwierdzasz nową kolejność za pomocą jednego wywołania. Metoda zwraca False, gdy indeks jest poza zakresem, więc należy walidować wynik zamiast zakładać, że zmiana kolejności się powiodła.
var
Doc: TPdf;
begin
Doc := TPdf.Create(nil);
try
Doc.FileName := 'report.pdf';
Doc.Active := True;
// Move the last page (index 4 in a 5-page file) to the very front.
if not Doc.MovePages([4], 0) then
raise Exception.Create('MovePages rejected the index');
Doc.SaveAs('report-reordered.pdf');
finally
Doc.Free;
end;
end;
Wyodrębnianie podzbioru stron według indeksu
Trzecia operacja kopiuje określony zestaw stron z jednego dokumentu do drugiego. Metoda ImportPagesByIndex przyjmuje dokument źródłowy oraz tablicę indeksów oznaczonych od zera i wstawia te strony do dokumentu docelowego na wybranej pozycji:
function ImportPagesByIndex(
Source : TPdf;
const PageIndices: array of Integer;
InsertAt : Integer= 0): Boolean;
Wywołujesz tę metodę na dokumencie docelowym i przekazujesz dokument źródłowy jako pierwszy argument. Parametr PageIndices określa strony źródłowe do pobrania w wybranej kolejności; InsertAt to pozycja w dokumencie docelowym, gdzie trafia pierwsza zaimportowana strona (wartość 0 umieszcza je przed istniejącą pierwszą stroną, a aktualna liczba stron dokumentu docelowego rośnie). Pusta tablica importuje każdą stronę, co czyni to wywołanie pełną kopią, gdy jest ona potrzebna. Zwraca False, jeśli jakikolwiek indeks w dokumencie źródłowym jest poza zakresem.
W tym miejscu ma znaczenie kontrast z dzieleniem dokumentu. Dzielenie zapisuje osobne pliki – jedna operacja generuje wiele plików na dysku. ImportPagesByIndex wykonuje odwrotne zadanie: gromadzi wybrany zestaw stron w jednym dokumencie docelowym w pamięci, który następnie zapisujesz raz. Gdy zadanie brzmi "daj mi strony 3, 7 i 12 jako jeden krótki PDF", jest to najkrótsza droga, która owija wewnętrzną funkcję FPDF_ImportPagesByIndex.
var
Source, Excerpt: TPdf;
begin
Source := TPdf.Create(nil);
Excerpt := TPdf.Create(nil);
try
Source.FileName := 'manual.pdf';
Source.Active := True;
Excerpt.CreateDocument; // start an empty target
// Pull pages 3, 7 and 12 (zero-based 2, 6, 11) into the excerpt.
if not Excerpt.ImportPagesByIndex(Source, [2, 6, 11], 0) then
raise Exception.Create('A requested page index is out of range');
Excerpt.SaveAs('manual-excerpt.pdf');
finally
Excerpt.Free;
Source.Free;
end;
end;
Przejrzyste łączenie elementów w całość
Struktura końcowa jest identyczna dla wszystkich trzech operacji: otwórz źródło poprzez ustawienie FileName i zmianę Active na True, wykonaj operację, zapisz za pomocą SaveAs i zwolnij to, co posiadasz. Jedyną gałęzią wymagającą uwagi jest to, które wywołania przydzielają nowy dokument. MovePages modyfikuje dokument, który już posiadasz, więc do zwolnienia jest jeden obiekt. ImportPagesByIndex zapisuje dane do obiektu docelowego utworzonego samodzielnie, więc zwalniasz źródło oraz otwarty obiekt docelowy. ImportNPagesToOne jest wyjątkiem, ponieważ nowy dokument jest wartością zwracaną przez metodę, a nie czymś, co sam skonstruowałeś, i zapomnienie o tym, że jest to oddzielny uchwyt należący do wywołującego, jest przyczyną zarówno wycieków, jak i podwójnego zwalniania pamięci. Zainicjuj wynik wartością nil, sprawdź go po wywołaniu i zwalniaj na pojedynczej ścieżce.
Jeśli zadanie, które faktycznie masz do wykonania, polega na łączeniu całych plików, a nie na zmianie układu stron, zobacz scalanie wielu plików PDF w jeden dokument. Jeśli jest to zadanie odwrotne – rozbijanie jednego dokumentu na wiele plików – zobacz dzielenie dokumentów PDF na wiele plików. Opisane tutaj metody impozycji i zmiany kolejności są dostarczane jako część komponentu PDFium Component dla Delphi i C++Builder, obok interfejsów API do ładowania, renderowania i edycji omówionych w innych miejscach tego bloga.