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 обрабатывает это молча. О чем стоит сообщить, так это о том, что номер страницы закладки находится за пределами диапазона документа, что случается в неправильно сформированных файлах, где контур (outline) никогда не обновлялся после удаления страниц. Проверка границ StartPage и EndPage пропускает эти записи, а не передает мусорный диапазон в ImportPages
Именование выходных файлов и сброс Active
Безопасности имен файлов, полученных из закладок, следует уделить особое внимание. Названия закладок могут содержать символы, допустимые в строке PDF, но не в пути файловой системы. Как минимум, замените прямую косую черту, обратную косую черту и двоеточие перед созданием выходного пути. В Windows *, ?, ", <, > и | также запрещены; простой цикл по фиксированному набору охватывает их без использования регулярных выражений
Строка Active := False в конце каждой итерации заслуживает особого внимания, поскольку это единственное неочевидное требование в шаблоне. CreateDocument не закрывает неявно то, что открыто. Если Active по-прежнему True, когда CreateDocument запускается снова, документ, все еще находящийся в памяти, никогда не был должным образом закрыт или сохранен, и вы не можете полагаться на четко определенное поведение в этом состоянии, поэтому явно сохраните и сбросьте флаг перед началом следующего документа. Думайте об этом как о паре к try/finally: блок finally освобождает внешний объект; Active := False сбрасывает внутреннее состояние документа между итерациями цикла
Использование памяти в процессе масштабного задания по разделению остается неизменным при таком подходе, поскольку вы никогда не держите в памяти более одного выходного документа одновременно. Исходный документ остается открытым и доступным только для чтения на всем протяжении; ImportPages копирует данные страницы в новый документ без изменения исходного. Если источник зашифрован, откройте его с паролем до цикла, и скопированные страницы в каждом выходном файле будут расшифрованы, что обычно является правильным поведением для разделенного вывода, распространяемого разным получателям
Еще одна вещь о SaveAs: метод возвращает логическое значение (Boolean). Несуществующий выходной каталог, путь с символами, которые ОС отклоняет, или состояние переполнения диска — все это приведет к тому, что SaveAs вернет False без создания исключения. В пакетном задании, которое разделяет 200-страничный документ на 200 одностраничных файлов, тихий сбой на странице 147 легко не заметить. Проверяйте возвращаемое значение при каждом вызове и сравнивайте количество успехов с ожидаемым общим итогом после завершения цикла
Показанные здесь методы ImportPages и CreateDocument являются частью PDFium Component для Delphi и C++Builder