Wymiary strony PDF są ustalone w momencie tworzenia strony, więc nie można po prostu przeskalować zawartości w miejscu, tak jak zmienia się rozmiar obrazu. Modelem biblioteki, który czyni pomniejszanie praktycznym, jest model przechwytywania i ponownego rysowania (capture-and-redraw): przenieś zawartość każdej ze stron z dokumentu do uchwytu, stwórz nową, pustą stronę w oryginalnym rozmiarze obszaru roboczego (media size), a następnie narysuj przechwyconą zawartość z powrotem na mniejszym obszarze ograniczającym (reduced bounding box). Otaczająca to pusta przestrzeń staje się marginesem. Przy skalowaniu na poziomie 70% na stronie formatu A4, na przykład, 15% szerokości przypada na każdą stronę i taka sama część na górze i na dole, co jest dokładnie tym, co produkuje poniższa arytmetyka granic
Jak działa CapturePage
CapturePage przyjmuje numer strony, promuje jej zawartość do rezydującego w pamięci obiektu przechwytywania (capture object) i usuwa stronę z drzewa stron dokumentu. To usunięcie jest intencjonalne i jest powodem, dla którego pętla zawsze wybiera stronę 1 niezależnie od indeksu iteracji: po tym jak strona 1 zostanie przechwycona i usunięta, to co było stroną 2 staje się nową stroną 1, i tak dalej. Jeśli będziesz inkrementował selektor strony (page selector) razem z licznikiem pętli (loop counter), ominiesz co drugą stronę i skończysz z połową oczekiwanego wyniku
Uchwyt przechwytywania zwrócony przez CapturePage nie jest referencją do strony; przypomina on bardziej migawkę zawartości (content snapshot). Pozostaje ważny aż do wywołania DrawCapturedPage lub jednoznacznego (explicit) jego zwolnienia. DrawCapturedPage przyjmuje ten uchwyt oraz docelowy prostokąt, zdefiniowany jako przesunięcie od lewej, przesunięcie od dołu, szerokość i wysokość, z czego wszystkie parametry wyrażono w punktach. Biblioteka skaluje przechwyconą zawartość tak, by precyzyjnie pasowała do tego prostokąta, przy czym zachowuje proporcje (aspect ratio) tylko wtedy, gdy wymiary utworzonego przez ciebie prostokąta przypadkowo zgadzają się z oryginalnymi proporcjami. Dla zapewnienia jednolitego proporcjonalnego skalowania, powinieneś posłużyć się prostokątem wyliczonym w oparciu o oryginalny rozmiar pomnożony przez współczynnik skali i wyśrodkować go odpowiednio na docelowej zredukowanej stronie
Matematyka centrowania
Przy współczynniku skali równym 70%, pozostałe 30% każdego wymiaru jest dzielone równo między dwie strony. W związku z tym wcięcie poziome to pageWidth * (1.0 - 0.70) / 2, co stanowi 15% szerokości, a wcięcie pionowe wykorzystuje tę samą formułę z użyciem wysokości strony. Docelowy prostokąt dla DrawCapturedPage zaczyna się zatem w (horizBorder, vertBorder) i rozciąga się na pageWidth - 2 * horizBorder przez pageHeight - 2 * vertBorder. Ta arytmetyka nie jest specyficzna dla biblioteki; to po prostu geometria symetrycznego wpasowania mniejszego prostokąta w większy
Jedna rzecz warta odnotowania: SetOrigin(1) umieszcza początek układu współrzędnych (coordinate origin) w lewym górnym rogu zamiast w lewym dolnym. Wartości obramowań, które przekazujesz do DrawCapturedPage, są mierzone od ustawionego przez ciebie początku układu, więc jeśli przełączysz tryby początku układu (origin modes) między wczytywaniem a rysowaniem, centrowanie będzie nieprawidłowe
Przykład w C#
Poniższy kod przetwarza każdą stronę pliku Pages.pdf poprzez cykl przechwytywania i ponownego rysowania i zapisuje wynik do newpages.pdf. PDFL to obiekt opakowujący ActiveX/COM (wrapper object) dodany do projektu z pliku PDFlibDLL64.dll
private void ScalePages_Click(object sender, EventArgs e)
{
File.Delete("newpages.pdf");
double pageWidth, pageHeight, horizBorder, vertBorder;
double scaleFactor = 0.70;
int capturedPageId, ret;
PDFL.LoadFromFile("Pages.pdf", "");
PDFL.SetOrigin(1);
int numPages = PDFL.PageCount();
for (int i = 1; i <= numPages; i++)
{
// Always select page 1: CapturePage removes the page, so page 2
// becomes page 1 on the next iteration.
PDFL.SelectPage(1);
pageWidth = PDFL.PageWidth();
pageHeight = PDFL.PageHeight();
horizBorder = pageWidth * (1.0 - scaleFactor) / 2;
vertBorder = pageHeight * (1.0 - scaleFactor) / 2;
capturedPageId = PDFL.CapturePage(1);
PDFL.NewPage();
PDFL.SetPageDimensions(pageWidth, pageHeight);
ret = PDFL.DrawCapturedPage(
capturedPageId,
horizBorder, vertBorder,
pageWidth - 2 * horizBorder,
pageHeight - 2 * vertBorder);
}
PDFL.SaveToFile("newpages.pdf");
}
Przykład w Delphi
Wersja w Delphi używa bezpośrednio obiektu TPDFlib, a nie przez warstwę COM, ale sekwencja wywołań jest identyczna. Jedną z praktycznych różnic jest strażnik pliku wyjściowego (output file guard): FileExists plus DeleteFile zamiast File.Delete, ponieważ metoda SaveToFile zakończy się niepowodzeniem, jeśli miejsce docelowe jest zablokowane przez otwarty z poprzedniego przebiegu i wciąż działający proces przeglądarki
procedure TForm1.ScalePagesClick(Sender: TObject);
var
PDFLib: TPDFlib;
pageWidth, pageHeight, horizBorder, vertBorder: Double;
scaleFactor: Double;
capturedPageId, ret, numPages, i: Integer;
begin
if FileExists('newpages.pdf') then
DeleteFile('newpages.pdf');
scaleFactor := 0.70;
PDFLib := TPDFlib.Create;
try
PDFLib.LoadFromFile('Pages.pdf', '');
PDFLib.SetOrigin(1);
numPages := PDFLib.PageCount();
for i := 1 to numPages do
begin
PDFLib.SelectPage(1);
pageWidth := PDFLib.PageWidth();
pageHeight := PDFLib.PageHeight();
horizBorder := pageWidth * (1.0 - scaleFactor) / 2;
vertBorder := pageHeight * (1.0 - scaleFactor) / 2;
capturedPageId := PDFLib.CapturePage(1);
PDFLib.NewPage();
PDFLib.SetPageDimensions(pageWidth, pageHeight);
ret := PDFLib.DrawCapturedPage(
capturedPageId,
horizBorder, vertBorder,
pageWidth - 2 * horizBorder,
pageHeight - 2 * vertBorder);
end;
PDFLib.SaveToFile('newpages.pdf');
finally
PDFLib.Free;
end;
end;
Co tak naprawdę kontroluje współczynnik skali
Wartość 0.70 w tym przypadku oznacza, że wyrenderowana zawartość zajmuje 70% wymiarów każdej strony, a nie, że plik osiągnie wielkość 70% swojego oryginalnego rozmiaru w bajtach. Rozmiar pliku po tej operacji zależy od złożoności oryginalnej zawartości; strona z dużymi obrazami nie zmniejszy się wprost proporcjonalnie w objętości dyskowej, ponieważ dane pikseli są po prostu odmalowywane (redrawn) w tej samej rozdzielczości na znacznie mniejszym obszarze. Jeśli celem jest kompresja na poziomie bajtów, właściwym podejściem jest użycie funkcji LinearizeFile lub ponowny zapis z kompresją strumieniową (stream compression), a nie skalowanie geometryczne
Podana wartość 0.70 nie oznacza również twardego limitu (hard limit). Dowolna wartość od 0.0 do 1.0 zadziała poprawnie, a parametry wkraczające ponad próg 1.0 powiększą zawartość wykraczając absolutnie poza oryginalne granice u strony (original page boundary), co obetnie i dopasuje krawędzie w ramach narzuconego obszaru roboczego (media box edge), chyba że dla zrównoważenia jednocześnie po prostu odpowiednio powiększysz wymiary samej tej strony. Dokumenty o mieszanych rozmiarach stron (mixed-size documents) są obsługiwane i akceptowane tu w sposób naturalny, ponieważ atrybuty właściwości PageWidth oraz PageHeight są odpytywane oddzielnie i wyciągane bezpośrednio (queried) dla każdej pojedynczej unikalnej strony tuż wprost przed nałożonym etapem pod proces wymierzenia obliczeń przy ramce brzegowej (border calculation), więc ostateczny wygenerowany tu dokument, gdzie dla przykładu strony o numerach nieparzystych bazują na kartkach formatu A4, a te dla wartości parzystych są narzuconym do wydruku ujęciem w formacie A3, wyda prawidłowo wyśrodkowany naturalny układ wyjściowy (correctly centered output) narzucony obozem pod precyzyjne odmalowanie co do zasady precyzyjnie niezależnie w rozmiarze absolutnie na każdej jednej rzutowanej z na obozie wielkości arkusza do pod na rozmiarze strony, bez uciekania we we specjalnie wydzielone odgórnie oddzielne od nakazów bloki specjalnych uwarunkowań wyjątkowych czy w po we z z za we dodatkową specyficzną tu obsługę od przypadków (without any special casing)
Co może pójść nie tak
W praktyce ujawniają się o dziwo dwa główne tryby błędów o awariach (failure modes). Pierwszy wyłania się w naturalny sposób jako zwyczajny problem na plik wyjściowy (output file), pozostawiony jako otwarty w przeglądarce PDF z poprzedniego uruchomienia (previous run): funkcja SaveToFile zakończy się niepowodzeniem lub zapisze zero bajtów (write zero bytes) w zależności od platformy (platform), a nowy wynik nigdy nie zapisać docelowo. Widoczny w kodzie strażnik usuwający plik (file-delete guard) na górze funkcji obsługuje to dla celów deweloperskich (for development), ale w potoku produkcyjnym (production pipeline) zapis do ścieżki tymczasowej (temporary path) i zmiana nazwy (renaming) po sukcesie jest bezpieczniejsze
Druga awaria to błąd ujęty w postaci wymierzania niezgodności w liczniku stron (page count mismatch). Ponieważ CapturePage usuwa strony z dokumentu w trakcie ich przetwarzania (as it processes them), odczyt licznika (count) z funkcji PageCount() przed pętlą jest odpowiednim limitem operacji iteracji (correct bound to iterate against). Wywołanie PageCount() wewnątrz pętli zwróciłoby zmniejszającą się wartość przy każdym przejściu (on each pass) i doprowadziło do przedwczesnego wyjścia (exit early), pozostawiając ostatnie strony nieprzetworzone (unprocessed). Zmienna pętli użyta w przykładach służy tu więc wyłącznie za postać licznika pozostałych iteracji (remaining-iterations counter); nigdy nie jest używana do wybierania strony, ponieważ strona do wyboru to zawsze 1 z powodów wyjaśnionych wcześniej (for the reason explained earlier)
Pokazane tutaj wywołania manipulacji stroną, w tym CapturePage, DrawCapturedPage oraz SetPageDimensions, są częścią losLab PDF Library dla języków Delphi, C#, VB.NET i C++