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 копіює дані сторінок у новий документ без зміни оригіналу. Якщо вихідний документ зашифрований, відкрийте його за допомогою пароля перед циклом, і скопійовані сторінки у кожному вихідному файлі будуть розшифровані, що зазвичай є правильною поведінкою для розділених вихідних даних, які поширюються серед різних одержувачів
Ще одна річ щодо SaveAs: він повертає Boolean. Вихідний каталог, який не існує, шлях із символами, які відхиляє ОС, або переповнений диск — усе це призведе до того, що SaveAs поверне False без виклику винятку. У пакетному завданні, яке розділяє 200-сторінковий документ на 200 односторінкових файлів, тиху помилку на 147-й сторінці легко не помітити. Перевіряйте значення, що повертається, при кожному виклику і підраховуйте успішні операції відносно очікуваної загальної кількості, коли цикл завершується
Показані тут методи ImportPages та CreateDocument є частиною PDFium Component для Delphi та C++Builder