PDFium Component предоставя един основен метод за разделяне на PDF: ImportPages. Всичко останало — независимо дали отделяте една страница, разделяте документа по произволни граници или следвате собствената му структура от отметки — е просто различен начин да определите кои номера на страници да попаднат във всеки изходен файл. Самата последователност от операции остава една и съща. Ако разберете това от самото начало, ще си спестите много погрешни подходи
Как работи цикълът за разделяне
Шаблонът е един и същ независимо как разделяте изходния документ. Създайте нов екземпляр на TPdf, извикайте CreateDocument, за да инициализирате празен PDF в паметта, импортирайте желаните страници чрез ImportPages, запишете резултата и след това задайте Active на False преди следващата итерация. Именно последната стъпка често се пропуска: CreateDocument не затваря автоматично документа, който още е в паметта, затова трябва изрично да запишете изходния файл и да изпълните Active := False, преди да го извикате отново. Това нулиране поддържа състоянието чисто и еднозначно. Външният екземпляр на TPdf се използва повторно във всички итерации, което намалява натоварването от заделяне на памет при големи задачи
Ето как изглежда разделянето страница по страница, сведено до основните операции:
procedure SplitIntoPages(Source: TPdf; const OutputDir: string);
var
I: Integer;
PdfOut: TPdf;
OutFile: string;
begin
PdfOut := TPdf.Create(nil);
try
for I := 1 to Source.PageCount do
begin
PdfOut.CreateDocument;
// Range is a 1-based page number string; insertion point 1 = first position
if not PdfOut.ImportPages(Source, IntToStr(I), 1) then
raise Exception.CreateFmt('Failed to import page %d', [I]);
OutFile := OutputDir + '\page_' + Format('%.4d', [I]) + '.pdf';
if not PdfOut.SaveAs(OutFile) then
raise Exception.Create('Failed to save ' + OutFile);
PdfOut.Active := False; // reset before next CreateDocument
end;
finally
PdfOut.Free;
end;
end;
Параметърът Range на ImportPages използва същия текстов формат, който PDFium прилага вътрешно: списък от номера на страници, разделени със запетаи, или диапазони, разделени с тире, като всички номера започват от 1. '3' импортира страница 3. '1-5' импортира последователно страници от 1 до 5. '2,5,8' импортира тези три страници. Третият параметър е позицията за вмъкване в целевия документ, също започваща от 1; стойност 1 винаги поставя импортираните страници в началото на иначе празния файл, което е желаното поведение тук
Разделяне по диапазони от страници
Когато извикващият код подаде списък като 1-12,13-24,25-36, го преобразувате в двойки начална и крайна страница и изпълнявате същия цикъл, като изграждате текста на диапазона от всяка двойка:
procedure SplitByRanges(Source: TPdf; const RangeList: array of string;
const OutputDir: string);
var
I: Integer;
PdfOut: TPdf;
OutFile: string;
begin
PdfOut := TPdf.Create(nil);
try
for I := 0 to High(RangeList) do
begin
PdfOut.CreateDocument;
if not PdfOut.ImportPages(Source, RangeList[I], 1) then
raise Exception.Create('Invalid page range: ' + RangeList[I]);
OutFile := Format('%s\section_%d.pdf', [OutputDir, I + 1]);
if not PdfOut.SaveAs(OutFile) then
raise Exception.Create('Failed to save ' + OutFile);
PdfOut.Active := False;
end;
finally
PdfOut.Free;
end;
end;
Тук е важно да валидирате входа, преди да стигнете до ImportPages. Методът ImportPages връща False, когато номер на страница в текста на диапазона надвишава Source.PageCount, но не предизвиква изключение и не създава частичен изходен файл, който да разпознаете само по името. Проверявайте върнатата стойност на SaveAs и записвайте неуспехите отделно; диапазон, който създава празен изходен файл, не изглежда очевидно неправилен, докато някой не го отвори
Разделяне по границите на отметките
Третият подход използва собствената структура на документа вместо външно подаден списък. Всяка отметка от най-горно ниво съдържа номер на целева страница; разделът, който тя определя, започва от тази страница и завършва една страница преди целта на следващата отметка, а при последната отметка — в края на документа
procedure SplitByBookmarks(Source: TPdf; const OutputDir: string);
var
Bm: TBookmarks;
I, StartPage, EndPage: Integer;
PdfOut: TPdf;
RangeStr, OutFile, SafeTitle: string;
begin
Bm := Source.Bookmarks;
if Length(Bm) = 0 then
Exit;
PdfOut := TPdf.Create(nil);
try
for I := 0 to High(Bm) do
begin
StartPage := Bm[I].PageNumber;
if I < High(Bm) then
EndPage := Bm[I + 1].PageNumber - 1
else
EndPage := Source.PageCount;
if (StartPage < 1) or (EndPage < StartPage) then
Continue;
RangeStr := Format('%d-%d', [StartPage, EndPage]);
PdfOut.CreateDocument;
if not PdfOut.ImportPages(Source, RangeStr, 1) then
begin
PdfOut.Active := False;
Continue; // skip a malformed section instead of writing an empty file
end;
SafeTitle := StringReplace(Bm[I].Title, '/', '_', [rfReplaceAll]);
SafeTitle := StringReplace(SafeTitle, ':', '_', [rfReplaceAll]);
OutFile := Format('%s\%02d_%s.pdf', [OutputDir, I + 1, SafeTitle]);
if not PdfOut.SaveAs(OutFile) then
raise Exception.Create('Failed to save ' + OutFile);
PdfOut.Active := False;
end;
finally
PdfOut.Free;
end;
end;
Документ без отметки не представлява грешка, която си струва да показвате на потребителя; този режим просто няма структура, по която да извърши разделянето. Проверката Length(Bm) = 0 обработва случая без съобщение. За сметка на това трябва да се отчете отметка с номер на страница извън обхвата на документа. Това се случва при повредени файлове, в които структурата не е обновена след изтриване на страници. Проверката на границите за StartPage и EndPage пропуска тези записи, вместо да подава невалиден диапазон на ImportPages
Имена на изходните файлове и нулиране на Active
Безопасността на имената, получени от заглавия на отметки, изисква изрична проверка. Заглавието на отметка може да съдържа знаци, които са валидни в PDF текст, но не и във файлова пътека. Като минимум заменете наклонената черта, обратната наклонена черта и двоеточието, преди да изградите изходната пътека. В Windows са забранени също *, ?, ", <, > и |; прост цикъл през фиксиран набор от знаци ги обработва без необходимост от регулярен израз
Редът Active := False в края на всяка итерация заслужава специално внимание, защото е единственото неочевидно изискване в този шаблон. CreateDocument не затваря автоматично отворения документ. Ако Active още е True при следващото извикване на CreateDocument, документът в паметта не е бил затворен или записан правилно и поведението в това състояние не е еднозначно. Затова записвайте и нулирайте изрично, преди да започнете следващия документ. Приемайте това като допълнение към try/finally: блокът finally освобождава външния обект, а Active := False нулира вътрешното състояние на документа между итерациите
При този подход използваната памет остава приблизително постоянна дори при голяма задача за разделяне, защото във всеки момент държите само един изходен документ в паметта. Документът източник остава отворен само за четене през цялото време; ImportPages копира данните на страниците в новия документ, без да променя източника. Ако PDF източникът е шифрован, отворете го с паролата преди цикъла. Копираните страници във всеки изходен файл ще бъдат нешифровани, което обикновено е подходящото поведение, когато разделените файлове се предоставят на различни получатели
Още една важна подробност за SaveAs: методът връща Boolean. Несъществуваща изходна директория, пътека със забранени от операционната система знаци или запълнен диск ще накарат SaveAs да върне False, без да предизвика изключение. При пакетна задача, която разделя документ от 200 страници на 200 едностранични файла, тих неуспех на страница 147 лесно остава незабелязан. Проверявайте резултата от всяко извикване и след края на цикъла сравнявайте броя на успешно записаните файлове с очаквания общ брой
Показаните тук методи ImportPages и CreateDocument са част от PDFium Component за Delphi и C++Builder