У вас есть десять тысяч договорных PDF от дюжины разных генераторов, и юристы хотят, чтобы в каждом из них был правильный Author, исправленная Producer строка и режим чтения, при котором при запуске открывается панель закладок. Наивное решение - загрузить каждый файл, заново сверстать страницы и записать новый документ. Сделайте так, и вы потеряете все существующие номера объектов, историю инкрементальных обновлений, любую цифровую подпись и тщательно настроенный xref, который выдал исходный инструмент. Страницы выглядят одинаково, но по структуре файл уже совсем другой. Для правки метаданных это совершенно неверная сделка
Правильный ход - рассматривать загруженный документ как граф объектов, который вы изменяете на месте: добираться до словаря Info, потока /Metadata и Catalog, менять только нужные записи и записывать результат обратно. HotPDF, нативный VCL PDF-компонент для Delphi и C++Builder, предоставляет именно такой уровень доступа через API записи загруженного документа. Эта статья о том, как использовать его правильно, и об одной ошибке, которую делает почти каждый: правят словарь Info и забывают, что в XMP живет вторая копия тех же метаданных
В двух местах хранится один и тот же набор метаданных, и они расходятся
В PDF сведения о документе хранятся в двух параллельных местах, и именно из-за этого чаще всего возникают тикеты в духе "я поменял заголовок, а Acrobat все еще показывает старый". Первое место - словарь сведений о документе, классический /Info объект с /Title, /Author, /Subject, /Keywords, /Creator, и /Producer ключами, определенными в ISO 32000-1 §14.3.3. Второе - пакет XMP, XML-документ, хранимый как поток, подвешенный к Catalog через /Metadata, определенный в §14.3.2 и построенный на модели данных Adobe XMP
Оба места могут содержать заголовок. Спецификация не заставляет их совпадать. Современные просмотрщики и большинство валидаторов PDF/A отдают предпочтение пакету XMP, если он есть, и переходят к словарю Info, если его нет. Поэтому если вы обновляете только /Info - именно так делает подавляющее большинство кода "set PDF metadata" - просмотрщик, который доверяет XMP, будет и дальше показывать устаревшее значение, а проверка PDF/A отметит расхождение. Правильная операция для любого файла, где уже есть пакет XMP, - двойная запись: изменить запись Info и сгенерировать XMP заново, чтобы оба представления оставались согласованными. HotPDF дает обе половины; дисциплина их совместного использования - на вас
Редактирование словаря Info
Помощники для стороны Info простые и предсказуемые. SetLoadedTitle, SetLoadedAuthor, SetLoadedSubject, SetLoadedKeywords, SetLoadedCreator, и SetLoadedProducer принимают по одному AnsiString и записывают соответствующий ключ в загруженный словарь Info, заменяя значение, если ключ уже есть, и добавляя его, если его нет. Чтобы полностью удалить ключ - скажем, утечку в виде /Creator, который называет ваши внутренние инструменты - вызовите RemoveLoadedInfoKey с голым именем ключа. Ни один из этих методов не трогает XMP; они работают только с объектом /Info, который LoadFromFile находил при разборе файла
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('contract-in.pdf', '') > 0 then
begin
Pdf.SetLoadedTitle('Master Services Agreement 2026');
Pdf.SetLoadedAuthor('Legal Department');
Pdf.SetLoadedSubject('Executed contract, retention 7 years');
Pdf.SetLoadedKeywords('contract; MSA; 2026; executed');
Pdf.SetLoadedProducer('Acme Document Pipeline');
Pdf.RemoveLoadedInfoKey('Creator'); // drop the originating tool name
Pdf.SaveLoadedDocument('contract-out.pdf');
end;
finally
Pdf.Free;
end;
end;
Одна деталь, которую важно не забыть: эти методы принимают AnsiString. Для ASCII-заголовков это не проблема, но текстовые строки PDF, которым нужны нелатинские символы, должны быть закодированы так, как требует спецификация, - UTF-16BE с меткой порядка байтов или PDFDocEncoding - прежде чем вы передадите их. Библиотека записывает в строковый объект те байты, которые вы ей даете; она не угадывает кодировку. Если ваши заголовки на обычном английском, это можно игнорировать. Если они содержат символы с диакритикой или CJK, кодируйте их осознанно и проверяйте в реальном просмотрщике
Перезапись пакета XMP
SetLoadedXMPMetadata - это вторая половина двойной записи. Передайте ей полный пакет XMP как AnsiString и она делает одно из двух: если Catalog уже ссылается на поток /Metadata /Metadata, она заменяет содержимое этого потока на месте, сохраняя тот же номер объекта; если потока метаданных нет, она создает его, помечает его /Type /Metadata и /Subtype /XML, выделяет номер объекта и связывает его из Catalog. В любом случае вы получаете корректный объект метаданных, который просмотрщики смогут прочитать
Вы задаете XML, а значит, сами управляете схемой - dc:title, dc:creator, xmp:CreatorTool, и так далее. В этом и сила, и ответственность одновременно: библиотека не разбирает и не проверяет ваш пакет и записывает байты без сжатия, без применения фильтра потока. Некорректный пакет спокойно пройдет через вызов и позже всплывет как ошибка поврежденных метаданных. Собирайте XML аккуратно и точно повторяйте значения, которые вы записали в словарь Info, чтобы оба представления никогда не противоречили друг другу
const
XMP_TEMPLATE =
'<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>' +
'<x:xmpmeta xmlns:x="adobe:ns:meta/">' +
'<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">' +
'<rdf:Description rdf:about="" xmlns:dc="http://purl.org/dc/elements/1.1/">' +
'<dc:title><rdf:Alt><rdf:li xml:lang="x-default">%s</rdf:li></rdf:Alt></dc:title>' +
'<dc:creator><rdf:Seq><rdf:li>%s</rdf:li></rdf:Seq></dc:creator>' +
'</rdf:Description></rdf:RDF></x:xmpmeta><?xpacket end="w"?>';
begin
// After setting the Info dictionary, mirror the same values into XMP:
Pdf.SetLoadedTitle('Master Services Agreement 2026');
Pdf.SetLoadedAuthor('Legal Department');
Pdf.SetLoadedXMPMetadata(
AnsiString(Format(XMP_TEMPLATE,
['Master Services Agreement 2026', 'Legal Department'])));
Pdf.SaveLoadedDocument('contract-out.pdf');
end;
Этот порядок - сначала Info, затем XMP, потом сохранение - стоит запомнить как шаблон. Эти два вызова независимы; согласованность появляется только потому, что вы подали им одни и те же строки. Пропустите вызов XMP для файла, в котором уже есть пакет XMP, и вы снова вернетесь к тихой проблеме устаревших данных, ради предотвращения которой написан весь этот раздел

Как управлять тем, как просмотрщик открывает файл
Три записи Catalog определяют, что увидит читатель в момент открытия документа, и все три правятся одной строкой в загруженном графе. SetLoadedPageMode записывает /PageMode как объект name: передайте 'UseOutlines' чтобы открыть панель закладок, 'UseThumbs' для боковой полосы миниатюр, 'FullScreen' для режима презентации или 'UseAttachments' чтобы показать панель вложений (ISO 32000-1 §7.7.3.1, Table 28). SetLoadedPageLayout записывает /PageLayout таким же образом - 'SinglePage', 'OneColumn', 'TwoColumnLeft', и остальное. Оба метода принимают имя без начального слеша; библиотека добавляет его на выходе
SetLoadedLanguage записывает в Catalog /Lang запись, языковой тег для всего документа - 'en-US', 'de-DE', тег BCP 47. Обратите внимание на разницу типов, из-за которой люди часто путаются: /PageMode и /PageLayout - это PDF name объекты, а /Lang - это string. HotPDF внутри делает это правильно, но если вы посмотрите на результат, то увидите /PageMode /UseOutlines против /Lang (en-US), и теперь вы понимаете, почему. Запись /Lang важнее, чем кажется: именно ее читает вспомогательная технология, чтобы выбрать произношение, и это жесткое требование для соответствия PDF/UA требованиям доступности
if Pdf.LoadFromFile('handbook.pdf', '') > 0 then
begin
Pdf.SetLoadedPageMode('UseOutlines'); // /PageMode, a name
Pdf.SetLoadedPageLayout('TwoColumnLeft'); // /PageLayout, a name
Pdf.SetLoadedLanguage('en-US'); // /Lang, a string
Pdf.SaveLoadedDocument('handbook-tagged.pdf');
end;
Переименование закладок без нарушения дерева
Заголовки закладок - обычная чистка: опечатка в заголовке, главу перенумеровали после создания структуры. SetLoadedOutlineTitle принимает индекс с нуля для верхнего уровня записей структуры и новый заголовок, проходит по цепочке Catalog → /Outlines → /First → /Next до этой позиции и заменяет /Title строку. Изменяется только заголовок; назначение, состояние раскрытия и дочерняя структура остаются без изменений
if Pdf.LoadFromFile('report.pdf', '') > 0 then
begin
Pdf.SetLoadedOutlineTitle(0, 'Executive Summary');
Pdf.SetLoadedOutlineTitle(1, 'Financial Results');
Pdf.SaveLoadedDocument('report-renamed.pdf');
end;
Переименование безопасно именно потому, что оно никогда не затрагивает структурные счетчики. Удаление записи структуры - вот где скрыта ловушка, и важно понимать этот случай даже тогда, когда вы лишь переименовываете, потому что он показывает, что не следует править вручную. Каждый узел структуры содержит /Count, и - согласно ISO 32000-1 §12.3.3 - этот счетчик не равен числу непосредственных потомков. Это общее число видимых потомков: положительное /Count со значением N означает, что сейчас раскрыто N потомков, а отрицательное значение означает, что у узла есть потомки, но он свернут. Когда удаляется запись верхнего уровня, счетчик корня /Outlines нельзя просто уменьшить на единицу; его нужно пересчитать, суммируя для каждого оставшегося узла верхнего уровня «один за сам узел плюс его положительный /Count,» и пропуская потомков любого свернутого узла с отрицательным счетчиком. Ошибетесь здесь, и итог закладок, который показывает просмотрщик, начнет расходиться - он будет увеличиваться более чем на единицу за каждое удаление. Переименование обходит все это стороной, и это еще одна причина предпочесть целевой вспомогательный метод вместо ручного ковыряния словаря
Как сохраняется правка на месте
Каждая правка выше изменяет объекты в памяти; на диск ничего не попадет, пока SaveLoadedDocument не будет вызван. Причина, по которой этот подход дешевый, в том, что сохранение не пересоздает документ - оно сохраняет существующие номера объектов и структуру, которую HotPDF разобрал при загрузке, и записывает обратно тот же граф с несколькими измененными и новыми объектами. Именно это не дает проходу по метаданным переписывать весь файл, и именно тот же механизм обновления на месте обеспечивает работу потоков объектов и инкрементальных обновленийЕсли ваши исходные файлы приходят из Word или другого офисного пакета, у их структуры объектов есть собственные особенности, о которых стоит знать до редактирования; статья о гибридных потоках перекрестных ссылок в Office PDF объясняет, как устроены эти файлы и что переживает обратное преобразование
Есть две границы, которые нужно соблюдать. Во-первых, это модель правки на месте, а не инструмент для редактирования или санитизации: удаление ключа Info удаляет только этот ключ, но не вычищает старые значения, которые могут оставаться в предыдущем поколении инкрементального обновления того же файла. Если вам нужно именно полное удаление чувствительных метаданных, это другая, более тяжелая операция. Во-вторых, запись XMP выполняется буквально - библиотека доверяет вашему XML и не проверяет его, так что для всего, что идет в PDF/A или строгий валидатор, формируйте пакет из проверенного шаблона и проверяйте результат. При использовании в этих границах правка метаданных на месте - инструмент нужного размера: он исправляет несколько неверных байтов и оставляет девяносто девять процентов файла, которые уже были правильными, ровно такими, какими их записал исходный производитель
Показанный здесь API записи загруженного документа поставляется вместе со стандартным HotPDF Component компонентом для Delphi и C++Builder, а также полным набором методов редактирования метаданных, структуры и Catalog