İlk üst düzey yer imi 5. sayfayı gösteren 40 sayfalık bir kullanıcı kılavuzu için, "bu yer iminden sonrakine kadar" kesen bir bölücü, toplamı 36 sayfa eden bölüm dosyaları yazar. Kapak, telif sayfası ve iki sayfalık içindekiler bölümü her ana hat girdisinin önünde durur; dolayısıyla hiçbir dosyaya düşmezler. Bir ana hat yalnızca bölümlerin nerede başladığını işaretler. İlk girdiden önce geleni kapsamaz ve biçim, girdilerin sayfa sırasında listelenmesini de gerektirmez; dolayısıyla güvenilir bir bölücü yer imi sayfalarını kesme noktası olarak toplayıp sıralar ve açık bir ön-kısım aralığı ekler
Her bölme kipinin ardındaki tek döngü
PDFium Component, bir belgeyi sınırları ne belirlerse belirlesin, ikinci bir TPdf üzerinde aynı dört adımla böler: CreateDocument bellekte boş bir PDF başlatır, ImportPages kaynaktan bir sayfa aralığını ona kopyalar, SaveAs dosyayı yazar ve Active := False çıktıyı yeniden kapatır. Sayfa sayfa bölme, sabit boyutlu parçalar ve yer imi bölümleri yalnızca ImportPages'e verilen aralık dizgesinde ayrışır
Her geçişin sonundaki sıfırlama isteğe bağlı değildir. CreateDocument, hâlâ aktif olan bir bileşen üzerinde çalışmayı reddeder ve mesajı "Cannot perform this operation on an active" ile başlayan bir EPdfError yükseltir; dolayısıyla Active := False'u unutan bir döngünün ikinci yinelemesi, tam olarak tek dosya yazıldıktan sonra bir istisnayla durur. Sıfırlamayı bir finally bloğuna koymak, başarısız bir içe aktarmanın ya da kaydın çıktıyı sonraki geçişe açık bırakmasını engeller. Tek bir TPdf örneği bütün çalışmayı karşılar ve bellekte hiçbir zaman tek bir çıktı belgesinden fazlası tutulmaz
Kaynağı açmak kendi denetimini hak eder. Active := True ataması asla raise etmez: dosya eksik, bozuk ya da verilmemiş bir parola gerektirdiğinde TPdf yükleme hatasını yakalar ve Active'i False bırakır. ImportPages ayrıca kaynağı kendi başına etkinleştirmeyi dener; bu da hiç açılmamış bir kaynağın yalnızca ilk sayfada False dönüşü olarak görünup neden hakkında ipucu vermemesi demektir. Onu açıkça açıp Active'i denetlemek, hiçbir çıktı var olmadan dosya adını söyleyen bir hataya çevirir:
procedure SplitIntoPages(const SourceFile, OutputDir: string);
var
Source, PdfOut: TPdf;
I: Integer;
OutFile: string;
begin
Source := TPdf.Create(nil);
PdfOut := TPdf.Create(nil);
try
Source.FileName := SourceFile;
Source.Active := True; // bir yükleme başarısızlığı Active = False bırakır
if not Source.Active then
raise Exception.Create('Cannot open ' + SourceFile);
for I := 1 to Source.PageCount do
begin
PdfOut.CreateDocument; // PdfOut hâlâ aktifken raise eder
try
// Aralık 1 tabanlıdır; konum 1 boş belgenin başıdır
if not PdfOut.ImportPages(Source, IntToStr(I), 1) then
raise Exception.CreateFmt('Cannot import page %d', [I]);
OutFile := IncludeTrailingPathDelimiter(OutputDir) +
Format('page_%.4d.pdf', [I]);
if not PdfOut.SaveAs(OutFile) then
raise Exception.Create('Cannot save ' + OutFile);
finally
PdfOut.Active := False; // sonraki geçişten önce çıktıyı kapat
end;
end;
finally
PdfOut.Free;
Source.Free;
end;
end;
ImportPages'in üçüncü parametresi, ilk içe aktarılan sayfanın hedefte düşeceği 1 tabanlı konumdur. Boş bir belgede 1 var tek konumdur. Aralık sözdiziminin kendisi (tek sayfalar, '5-10' gibi kapalı aralıklar, virgül listeleri) ve birleştirilmiş bir paketi tersine çeviren konum yanılgıları, aynı API'nin ters yöne çalışan hali olan birden çok PDF dosyasını tek belgede birleştirmede işlenir
Sabit boyutlu parçalar ve çağıranın verdiği aralıklar
Bir belgeyi her N sayfada kesmek bir sayım problemidir ve özen gerektiren tek durum kuyruktur. 25 sayfalık parçalara bölünen 230 sayfalık bir tarama dokuz tam parça ve beş sayfalık bir onuncu parça verir; dolayısıyla her aralığın sonu Source.PageCount'a kelepçelenir:
procedure SplitEvery(Source: TPdf; ChunkSize: Integer; const OutputDir: string);
var
PdfOut: TPdf;
FirstPage, LastPage, Part: Integer;
Range, OutFile: string;
begin
if ChunkSize < 1 then
raise Exception.Create('ChunkSize must be at least 1');
PdfOut := TPdf.Create(nil);
try
Part := 0;
FirstPage := 1;
while FirstPage <= Source.PageCount do
begin
LastPage := FirstPage + ChunkSize - 1;
if LastPage > Source.PageCount then
LastPage := Source.PageCount; // son parça genellikle daha kısadır
if FirstPage = LastPage then
Range := IntToStr(FirstPage) // tek sayfalık kuyruk düz bir sayı olarak
else
Range := Format('%d-%d', [FirstPage, LastPage]);
Inc(Part);
PdfOut.CreateDocument;
try
if not PdfOut.ImportPages(Source, Range, 1) then
raise Exception.Create('Cannot import pages ' + Range);
OutFile := IncludeTrailingPathDelimiter(OutputDir) +
Format('part_%.3d.pdf', [Part]);
if not PdfOut.SaveAs(OutFile) then
raise Exception.Create('Cannot save ' + OutFile);
finally
PdfOut.Active := False;
end;
FirstPage := LastPage + 1;
end;
finally
PdfOut.Free;
end;
end;
Kullanıcının yazdığı aralıklar başka bir meseledir. ImportPages bozuk bir aralığı dönüş değeriyle bildirir: Source.PageCount'u aşan bir sayfa numarası, bir sıfır, ters çevrilmiş bir aralık ya da ayrışmayan bir metin çağrıyı False döndürtür ve o dizgedeki hiçbir sayfa kopyalanmaz. Döngünün içinde bu başarısızlık, üç bölüm dosyası diskte var olduktan sonra dördüncü girdide gelir. İlk CreateDocument'ten önce her girdiyi Source.PageCount ile denetlemek birkaç satır ayrıştırma demektir ve dördüncü girdideki bir yazım hatasının hiçbir şey yazmamasını sağlar; işi yeniden koşan bir kişinin beklediği davranış da budur
Seçim zaten bir tamsayı dizisinde yaşıyorsa — örneğin küçük resim görünümünde seçilmiş sayfalar — ImportPagesByIndex(Source, PageIndices, InsertAt) dize adımını atlar. Hem indeksler hem InsertAt orada sıfır tabanlıdır; dolayısıyla aralık olarak '1,3-5' okunan aynı seçim [0, 2, 3, 4] olur
Yer imi bölümleri neden eksik ya da hiç çıkar?
Yer imi bölümleri, kod ana hattın derli toplu olduğuna güvendiğinde bozulur. TPdf.Bookmarks, üst düzey girdileri yazarın dizdiği sırada döndürür; her biri bir Title ve 1 tabanlı bir PageNumber taşıyan TBookmark kaydı olarak. Gerçek dosyalardaki dört şey "bu girdiden sonrakine" kuralını bozar:
- Bir ana hat ögesi hedefini bir
/Destgirdisiyle ya da/Aiçindeki bir GoTo eylemiyle taşıyabilir (ISO 32000-1 §12.3.3 ve §12.6.4.2).PageNumber/Dest'ten doldurulur; eylem tabanlı bir ögede0kalır ve sayfaActionPageNumber'dedir - Ana hat sırası, yazarın gördüğü okuma sırasıdır. İkinci sırada duran bir "Dizin" girdisi ya da bölümlerden önce listelenmiş bir ek, "sonraki yer imi eksi bir"i geçerli başlangıçtan küçük yapar ve o bölüm kaybolur
- İki üst düzey girdi aynı sayfaya işaret edebilir; tipik olarak bir bölüm başlığı ile onun ilk bölümü
- Sayfalar ana hattı bırakan bir araçla silinmişse bir girdi son sayfanın ötesine işaret edebilir
Aşağıdaki sürüm ana hattı kesme noktası kaynağı sayar. Her girdinin sayfasını çözer, hiçbir yere ya da sonun ötesine işaret eden girdileri düşürür, paylaşılan sayfadaki ilk başlığı tutar, sayfa numarasına göre sıralar ve ilk kesim 1. sayfadan sonraysa bir "Front matter" bölümü ekler. Ön kısım burada kendi dosyasını alır, bölüm 1'e yapıştırılmaz; çünkü bölüm 1'i alan kişi, dosyanın içindekiler sayfasında değil yer iminin adlandırdığı sayfada açılmasını bekler
procedure SplitByBookmarks(Source: TPdf; const OutputDir: string);
var
Bm: TBookmarks;
Starts: array of Integer;
Titles: array of string;
I, J, K, N, P, EndPage: Integer;
Known: Boolean;
PdfOut: TPdf;
OutFile: string;
begin
Bm := Source.Bookmarks; // yalnızca üst düzey girdiler
if Length(Bm) = 0 then
Exit; // kesilecek bir şey yok
SetLength(Starts, Length(Bm) + 1); // +1 ön kısım için yer bırakır
SetLength(Titles, Length(Bm) + 1);
N := 0;
for I := 0 to High(Bm) do
begin
P := Bm[I].PageNumber; // /Dest'ten çözülür
if P < 1 then
P := Bm[I].ActionPageNumber; // /A'daki GoTo eyleminden çözülür
if (P < 1) or (P > Source.PageCount) then
Continue; // çözümlenmemiş ya da son sayfanın ötesinde
Known := False;
for J := 0 to N - 1 do
if Starts[J] = P then
Known := True; // bir sayfadaki ilk başlık kazanır
if Known then
Continue;
J := N; // sayfa numarasına göre eklemeli sıralama
while (J > 0) and (Starts[J - 1] > P) do
begin
Starts[J] := Starts[J - 1];
Titles[J] := Titles[J - 1];
Dec(J);
end;
Starts[J] := P;
Titles[J] := Bm[I].Title;
Inc(N);
end;
if N = 0 then
Exit;
if Starts[0] > 1 then // kapak, içindekiler ve onlardan önceki her şey
begin
for J := N downto 1 do
begin
Starts[J] := Starts[J - 1];
Titles[J] := Titles[J - 1];
end;
Starts[0] := 1;
Titles[0] := 'Front matter';
Inc(N);
end;
PdfOut := TPdf.Create(nil);
try
for K := 0 to N - 1 do
begin
if K < N - 1 then
EndPage := Starts[K + 1] - 1
else
EndPage := Source.PageCount;
PdfOut.CreateDocument;
try
if not PdfOut.ImportPages(Source,
Format('%d-%d', [Starts[K], EndPage]), 1) then
raise Exception.CreateFmt('Cannot import pages %d-%d',
[Starts[K], EndPage]);
// SafeFileName'i (aşağıda gösteriliyor) bu yordamdan önce bildirin
OutFile := IncludeTrailingPathDelimiter(OutputDir) +
Format('%.2d_%s.pdf', [K + 1, SafeFileName(Titles[K])]);
if not PdfOut.SaveAs(OutFile) then
raise Exception.Create('Cannot save ' + OutFile);
finally
PdfOut.Active := False;
end;
end;
finally
PdfOut.Free;
end;
end;
Başlangıçlar sıralamadan sonra katı biçimde arttığı için hesaplanan her aralık geçerlidir ve ImportPages üzerindeki koruma, okunabilir bir kaynakta asla ateşlenmemelidir. Yer imi olmayan bir belge bu kipte yalnızca hiç dosya üretmez; ona parçalamaya düşülüp düşülmeyeceğine çağıran karar verir. Bir düzey derine, örneğin parçaların içindeki bölümlerde kesmek için BookmarkChildren[Bm[I]], üst düzey bir girdinin çocuklarını aynı TBookmarks biçiminde döndürür ve aynı kesme noktası mantığı birleştirilmiş listeye uygulanır
Her çıktı dosyası neyi taşır
Her çıktı dosyası sayfaların kendisini ve sayfa sözlüklerinin referans verdiği her şeyi taşır: içerik akışları, fontlar, görüntüler ve her sayfanın açıklama listesi. Form widget'ları açıklama olarak gelir; ama yeni dosyada AcroForm yoktur, dolayısıyla artık çalışan alanlar değildirler. Kaynak yalnızca okunur. ImportPages nesneleri yeni belgeye kopyalar ve açık kaynağa dokunmaz; dolayısıyla aynı Source örneği her yinelemeyi besler
Ana hat geride kalır. Yer imleri, belge kataloğunda /Outlines altında yaşar (ISO 32000-1 §7.7.2); hiçbir sayfanın dışındadır, dolayısıyla içe aktarılan sayfalardan kurulan dosya hiç yer imi olmadan başlar. Bölüm dosyaları için bu çoğunlukla sorun değildir. Her parçanın kendi dolaşımına ihtiyaç duyduğunda ana hat, yeni sayfa numaralaması için kurulmalıdır; konusu bir PDF ana hatını düzenlemek ve sayfalarını yeniden eşlemek yazısıdır. İç bağlantılar aynı davranır: 12. sayfadaki bir "bkz. bölüm 7" bağlantısı başka bir dosyaya giren bir sayfayı gösterir ve bu dosyada üzerine düşecek bir şeyi yoktur
Şifreli kaynaklar döngü başlamadan parolayı ister. Bileşen etkin değilken Source.Password'u atayın, sonra Active := True; yanlış parolayla bileşen etkin kalmaz ve SplitIntoPages'in denetimi bunu bir hataya çevirir. SaveAs'in yazdığı çıktı, şifreleme sözlüğü olmayan yeni bir belgedir. Bu, farklı alıcılara gidecek bölünmüş dosyalar için uygundur; her parçanın yeniden korunması gerektiğinde SaveAsEncrypted(FileName, Options), kendi kullanıcı ve sahip parolalarını taşıyan bir TPdfEncryptOptions kaydı alır
Dosya adları, başarısız kayıtlar ve yeniden koşular
Yer imi başlıkları PDF metin dizgeleridir ve Windows'un dosya adlarında reddettiği karakterler dahil her şeyi içerebilir. Kurallar PDF'den değil dosya sisteminden gelir: < > : " / \ | ? * ile denetim karakterleri geçersizdir, sondaki boşluklar ve noktalar Win32 dosya API'lerince soyulur ve CON, PRN, COM1 gibi adlar rezerve aygıtlardır. SplitByBookmarks'teki numaralı önek aygıt adlarını halleder, dosyaları dizin listesinde okuma sırasında tutar ve ikisi de "Summary" başlıklı iki bölümün birbirini ezmesini engeller. Gerisi, kendinizin yazacağı küçük bir işlevdir:
// Okuyucu tarafı yardımcı, PDFium Component'in parçası değildir
function SafeFileName(const Title: string): string;
const
Forbidden = '<>:"/\|?*';
var
I: Integer;
begin
Result := Title;
if Length(Result) > 80 then
SetLength(Result, 80); // tam yolları MAX_PATH'in iyice altında tutar
for I := 1 to Length(Result) do
if (Ord(Result[I]) < 32) or (Pos(Result[I], Forbidden) > 0) then
Result[I] := '_';
while (Result <> '') and
((Result[Length(Result)] = '.') or (Result[Length(Result)] = ' ')) do
SetLength(Result, Length(Result) - 1);
if Result = '' then
Result := 'section';
end;
Dosya adıyla kullanılan SaveAs hedef klasörde geçici bir dosyaya yazar ve kayıt başarılı olduktan sonra onu yerine takar. Hedef bu yüzden ya önceki dosyadır ya da eksiksiz yeni bir dosya; yarım yazılmış hiçbir hâl yoktur, dolayısıyla bir bölme işini var olan bir çıktı klasörü üzerinde yeniden koşmak zararsızdır. Başarısızlıklar iki yoldan gelir. Belge serileştirilemediğinde SaveAs False döndürür ve geçici dosya silinir. Dosya sistemindeki sorunlar — var olmayan bir çıktı klasörü, işletim sisteminin reddettiği bir yol ya da bir görüntüleyicinin kilitlediği hedef — dosya katmanından istisna yükseltir
Bir toplu işin o başarısızlıklara tepkisi bir seçimdir ve bölme için birleştirmeden farklıdır. Eksik parçalı birleştirilmiş bir paket yanlış bir belgedir; orada durmak doğrudur. Bölme çıktıları birbirinden bağımsızdır: section_147.pdf bir iş arkadaşının elinde açık olduğu için yazılamıyorsa öteki bölümler hâlâ sağlamdır. Bölme işleri için döngünün içinde dosya başına istisna yakalayın, dosya adını ve mesajı kaydedin, devam edin ve döngü bittiğinde yazılan dosya sayısını planlanan aralık sayısıyla karşılaştırın. Yukarıdaki örnekler kısalmak için ilk başarısızlıkta raise eder. 200 dosyadan 199'unun yazıldığını söyleyen ve kilitli olanı adlandıran bir günlük tek yeniden koşuşla düzelir; 147'de kesilen bir koşuş ise birilerine hangi 53 dosyanın eksik olduğunu hesaplama işi bırakır
Buradaki bölme döngüsü, yer imi kayıtları ve şifreli kayıt, Delphi ve C++Builder için PDFium tabanlı PDF kitaplığı PDFium Component ile gelir