Имате десет хиляди договорни PDF файла от дузина различни генератори, а юристите искат всеки от тях да носи правилния Author, коригиран Producer низ и режим на четене, който отваря панела с отметките при стартиране. Наивното решение е да заредите всеки файл, да пренаредите страниците и да запишете нов документ. Направите ли това, просто изхвърляте всеки съществуващ номер на обект, историята на инкременталните обновявания, всяка цифрова сигнатура и внимателно настроения xref, който оригиналният инструмент е генерирал. Страниците изглеждат еднакви, а файлът по структура е чужденец. За едно редактиране на метаданни това е грешната сделка
Правилният ход е да третирате заредения документ като обектен граф, който променяте на място: влезте в Info речника, /Metadata stream-а и 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 - което е това, което прави огромното мнозинство код за задай PDF метаданни - четец, който се доверява на 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 поток, той заменя съдържанието на този поток на място, запазвайки същия номер на обект; ако няма поток с метаданни, той създава такъв, маркира го /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, после save - е моделът, който си струва да запомните. Двете извиквания са независими; последователността съществува само защото сте им подали едни и същи низове. Пропуснете XMP извикването върху файл, който има XMP пакет, и пак се връщате към тихия проблем със застарялата стойност, който цялата тази секция съществува, за да предотврати

Насочване на това как прегледачът отваря файла
Три записа в Catalog решават какво вижда читателят в момента, в който документът се отвори, и и трите са едноредови редакции върху заредения граф. SetLoadedPageMode записва /PageMode като name обект: подайте 'UseOutlines' за да отворите панела с отметките, 'UseThumbs' за лентата с миниатюри, 'FullScreen' за режим на презентация или 'UseAttachments' за да покажете панела с прикачени файлове (ISO 32000-1 §7.7.3.1, Таблица 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;
Преименуване на отметките без да се нарушава дървото
Заглавията на отметките са рутинно почистване - правописна грешка в заглавие, преименувана глава след като outline-ът е бил построен. SetLoadedOutlineTitle приема индекс, започващ от нула, в top-level записи на outline-а и ново заглавие, проследява веригата 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;
Преименуването е безопасно точно защото никога не засяга структурните броячи. Изтриването на запис в outline-а е случаят, който хапе, и си струва да го разберете, дори когато само преименувате, защото ви показва какво не трябва да редактирате ръчно. Всеки възел на outline-а носи /Count, а според ISO 32000-1 §12.3.3 този брой не е броят на непосредствените деца. Той е общият брой на видими потомци: положителен /Count от N означава, че N потомъка в момента са показани, а отрицателна стойност означава, че възелът има потомци, но е свит. Когато се премахне запис на най-горно ниво, /Outlines броячът на корена не може просто да бъде намален с едно; той трябва да се преизчисли, като за всеки оцелял запис на най-горно ниво се сумира едно за самия възел плюс неговите положителни /Count, като се пропускат потомците на всеки свит (с отрицателен брой) възел. Ако сгрешите тук, общият брой отметки, който читателят вижда, се разминава - скача с повече от едно при всяко изтриване. Преименуването заобикаля всичко това, което е още една причина да предпочетете насочения помощник пред това да човъркате речника сами
Как запазването остава на място
Всички редакции по-горе променят обекти в паметта; нищо не стига до диска, докато SaveLoadedDocument не бъде извикан. Причината този подход да е евтин е, че запазването не прегенерира документа - то запазва съществуващите номера на обектите и структурата, която HotPDF е анализирал при зареждането, и записва обратно същия граф с вашите няколко променени и новодобавени обекта. Това е онова, което не позволява един пас за метаданни да пренапише целия файл, и е същият механизъм за обновяване на място, който прави object streams and incremental updates работи. Ако изходните ви файлове идват от Word или друг офис пакет, техният object layout има свои особености, които си струва да знаете преди да ги редактирате; статията за hybrid-reference cross-reference потоците в Office PDF файлове описва как са структурирани тези файлове и какво оцелява след двупосочно преобразуване
Две граници, които трябва да уважавате. Първо, това е модел за редакция на място, не инструмент за заличаване или дезинфекция: премахването на Info ключ премахва този ключ, но не изчиства по-старите стойности, които може да останат в предишна инкрементална генерация на същия файл. Ако изискването ви е истинско премахване на чувствителни метаданни, това е различна и по-тежка операция. Второ, записът на XMP е буквален - библиотеката се доверява на вашия XML и не го валидира - така че за всичко, предназначено за PDF/A или строг валидатор, генерирайте пакета от добре познат шаблон и проверете изхода. Използван в тези граници, редактирането на метаданни на място е правилният инструмент с подходящ размер: то поправя малкото байтове, които са грешни, и оставя деветдесет и девет процента от файла, който вече е правилен, точно такъв, какъвто първоначалният производител го е записал
Показаният тук API за запис върху зареден документ се доставя със стандартния HotPDF Component за Delphi и C++Builder, заедно с пълния набор методи за редакция на метаданни, outline и Catalog