Дві хвилини на копіювання трьох сторінок із 40-сторінкового PDF не є проблемою налаштування продуктивності. Це сигнал, що використовується неправильний шлях API. Коли я вперше побачив цей час у прикладі копіювання сторінок HotPDF Component, моїм інстинктом було спершу поглянути на структуру документа, а потім на код. Цей порядок виявився важливим
Що насправді було повільним
Розглянутий PDF був 40-сторінковим довідковим документом із нетривіальним деревом сторінок: кілька проміжних вузлів /Pages замість одного плоского масиву. Оригінальний код прикладу викликав LoadFromFile, потім створював новий документ за допомогою BeginDoc, виконував цикл за вибраними номерами сторінок і на кожній ітерації знову завантажував вихідний документ з диска, щоб витягти сторінку. Це вартість повного аналізу, помножена на кількість потрібних вам сторінок. Файл розміром 12 МБ звертався до диска шість разів для вилучення трьох сторінок, оскільки ніхто не перевірив, чи потрібно тримати файл відкритим між ітераціями
Другий фактор був невидимим у коді: LoadFromFile компонента HotPDF дозволяє розв'язати всю таблицю перехресних посилань і розпаковує кожен потік об'єктів під час завантаження. Це правильна поведінка для документа, який ви збираєтеся змінити, але це більше роботи, ніж потрібно, якщо ви хочете отримати лише кількість сторінок і підмножину сторінок. Для доступу до структури лише для читання DAOpenFileReadOnly уникає десеріалізації повного дерева об'єктів, що має значення для стиснених файлів із великими ресурсами зображень
Жодне з цього не є помилкою бібліотеки. В обох випадках користувачі вибирають API, розроблений для одного завдання, і використовують його для іншого
Використання InsertPagesFromDocument для вилучення сторінок
Правильним шляхом для копіювання діапазону сторінок з одного документа HotPDF в інший є InsertPagesFromDocument, викликаний після LoadFromFile для джерела. Ви завантажуєте джерело один раз, завантажуєте або створюєте місце призначення один раз, переміщуєте сторінки та зберігаєте. Джерело залишається в пам'яті під час усіх вставок сторінок:
procedure ExtractPages(const SourceFile, DestFile: string;
const PageRange: string);
var
Source, Dest: THotPDF;
begin
Source := THotPDF.Create(nil);
Dest := THotPDF.Create(nil);
try
// Load source once: full parse happens here and only here
Source.LoadFromFile(SourceFile);
// Build a minimal destination document
Dest.FileName := DestFile;
Dest.BeginDoc;
// Copy the requested range; '1-3' inserts pages 1 through 3
// starting at position 1 in the destination
Dest.InsertPagesFromDocument(Source, PageRange, 1);
Dest.EndDoc;
finally
Source.Free;
Dest.Free;
end;
end;
Параметр PageRange приймає той самий формат, що й приклад командного рядка: список номерів сторінок або діапазонів, розділених комами, наприклад '1-3' або '1,5,7-9'. Нумерація сторінок починається з 1. InsertPagesFromDocument копіює потоки вмісту, словники ресурсів і геометрію сторінок, не зачіпаючи метадані, закладки або вбудовані файлові вкладення, якщо на них немає посилань зі скопійованих сторінок. Для вилучення трьох сторінок із 40-сторінкового документа це невеликий робочий набір
Час виконання на тому самому файлі розміром 12 МБ, який раніше займав дві хвилини: менше 1.5 секунди з цим шаблоном. Більша частина цього часу припадає на єдиний виклик LoadFromFile. Структура документа не має значення після першого вирішення таблиці об'єктів
Коли LoadFromFile занадто багато: API Direct File
Якщо вам потрібно лише підрахувати сторінки, перевірити інформацію про документ або скопіювати файл, не торкаючись його вмісту, API Direct File дозволяє повністю уникнути повного аналізу. DAOpenFileReadOnly відображає таблицю перехресних посилань без розпакування потоків об'єктів, тому кількість сторінок становить O(розмір xref), а не O(розмір файлу):
procedure InspectPDF(const FileName: string);
var
Pdf: THotPDF;
Handle, PageCount: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Handle := Pdf.DAOpenFileReadOnly(FileName, '');
if Handle <= 0 then
Exit;
try
PageCount := Pdf.DAGetPageCount(Handle);
Writeln('Pages: ', PageCount);
// DACopyFile is a byte-preserving copy, no re-serialization
Pdf.DACopyFile(FileName, 'archive-copy.pdf');
finally
Pdf.DACloseFile(Handle);
end;
finally
Pdf.Free;
end;
end;
Застереження: DAOpenFileReadOnly приймає параметр пароля, але повертається до повного аналізу для зашифрованих вхідних даних, оскільки розшифрування вимагає дерева об'єктів для розв'язання словника шифрування. Якщо ваші вихідні файли зашифровані, спочатку розшифруйте їх за допомогою DecryptFile, щоб отримати незашифровану копію, а потім відкрийте її за допомогою API Direct File. Функція рівня файлу DecryptFile використовує шлях прямого перезапису AES-256 для стандартного шифрування і працює швидше, ніж LoadFromFile з наступним SaveLoadedDocument для великих файлів, оскільки вона не створює повну об'єктну модель у пам'яті
Пам'ять під час обробки великих пакетів
Пакетні завдання, які обробляють десятки файлів у циклі, мають шаблон, який виглядає правильним, але накопичує пам'ять: створення THotPDF всередині циклу, виклик LoadFromFile, виконання роботи, виклик Free. Структурно це нормально. Проблема виникає, коли внутрішня робота виділяє тимчасові об'єкти, перехоплює винятки і залишає ці тимчасові об'єкти живими на шляхах помилок. Менеджер пам'яті Delphi не виконує ущільнення, тому сотня витоків на шляху помилок під час пакетного запуску може підняти обсяг пам'яті настільки високо, щоб уповільнити виділення для всього іншого
Виправлення не є екзотичним. Кожен THotPDF і кожен проміжний TStream або TBitmap, який бере участь у роботі з PDF, належить до блоку try/finally, де Free є останнім оператором. Встановіть локальні покажчики на nil перед try, щоб гілка finally могла безпечно використовувати if Assigned(x) then x.Free, коли ініціалізація не вдається на півдорозі. Це стандартна дисципліна володіння Delphi, і це вся історія для цього класу проблем
Ще одна річ, яку слід перевірити в контексті пакетів: AddImage реєструє зображення у внутрішньому списку, який зберігається протягом усього часу існування екземпляра THotPDF. Якщо ви повторно використовуєте один екземпляр у багатьох документах, багаторазово викликаючи LoadFromFile, реєстрації зображень із попередніх документів залишаються в списку. Або створюйте новий екземпляр для кожного документа, або викличте метод очищення списку зображень між документами
Вимірювання перед зміною будь-чого
Перш ніж тягнутися до будь-якого з цих шаблонів, виміряйте. TStopwatch з System.Diagnostics у Delphi обгортає QueryPerformanceCounter і є достатньо точним для профілювання файлового вводу-виводу за реальним часом. Обгорніть один лише LoadFromFile і подивіться, скільки часу він займає. Якщо це 90% загального часу, рішенням буде API Direct File або зменшення кількості разів, коли ви аналізуєте той самий файл. Якщо це менше 20%, вузьке місце знаходиться в іншому місці, і ви шукаєте не там
Двохвилинне вилучення, з якого почалася ця стаття, виявилося повністю наслідком шаблону повторного завантаження. Структура документа нічого не змінила; плоске дерево сторінок працювало б так само. Перехід на єдиний LoadFromFile із наступним одним викликом InsertPagesFromDocument скоротив час до 1.3 секунди на тому самому обладнанні, не торкаючись нічого іншого
API маніпулювання сторінками, показаний тут, є частиною HotPDF Component для Delphi та C++Builder