Műszaki cikk

Betöltött PDF-metaadatok szerkesztése újraírás nélkül

Tízezer szerződéses PDF-ed van egy tucat különböző előállítótól, és a jogi osztály azt akarja, hogy mindegyiken a helyes Author, egy javított Producer sztring, valamint olyan megnyitási mód legyen, amely induláskor a könyvjelzőpanelt jeleníti meg. A naiv megoldás az, hogy minden fájlt betöltesz, újrarendezed az oldalakat, majd friss dokumentumot írsz. Ha ezt teszed, eldobod az összes meglévő objektumszámot, az inkrementális frissítési előzményeket, minden digitális aláírást, és az eredeti eszköz által létrehozott gondosan hangolt xrefet. Az oldalak ugyanúgy néznek ki, de a fájl szerkezetileg már egy másik dokumentum. Metaadat-szerkesztésnél ez a rossz csere

A helyes út az, ha a betöltött dokumentumot helyben módosított objektumgráfként kezeled: belenyúlsz az Info szótárba, a /Metadata streambe és a Katalógusba, átírod azt a néhány bejegyzést, amely tényleg számít, majd visszaírod az eredményt. A HotPDF, a Delphihez és C++Builderhez készült natív VCL PDF komponens, pontosan ezt a felületet adja a betöltött dokumentum írási API-n keresztül. Ez a cikk arról szól, hogyan használd helyesen, és arról az egy hibáról, amit szinte mindenki elkövet: az Info szótárt szerkeszti, miközben elfelejti, hogy ugyanaz a metaadat egy második példányban XMP-ben is ott van

Ugyanaz a metaadat két helyen él, és nem feltétlenül egyeznek

A PDF két párhuzamos helyen tárol dokumentuminformációt, és ez a forrása a legtöbb "átírtam a címet, de az Acrobat még mindig a régit mutatja" hibajegynek. Az első a dokumentuminformációs szótár, a klasszikus /Info objektum a /Title, /Author, /Subject, /Keywords, /Creator és /Producer kulcsokkal, amelyeket az ISO 32000-1 §14.3.3 határoz meg. A második egy XMP csomag, egy XML dokumentum streamként a Katalógus alatt, a /Metadata bejegyzésben tárolva, amelyet a §14.3.2 ír le, és az Adobe XMP adatmodellre épül

Mindkettő tartalmazhat címet. A specifikáció semmire sem kényszeríti őket, hogy egyezzenek. A modern megjelenítők és a legtöbb PDF/A ellenőrző az XMP csomagot részesíti előnyben, ha jelen van, és csak akkor tér vissza az Info szótárhoz, ha nincs XMP. Ha tehát csak a /Info értékeit frissíted, ami a PDF metaadatok beállítására szolgáló kódok nagy többségére igaz, az XMP-ben bízó olvasó továbbra is a régi értéket mutatja, a PDF/A ellenőrző pedig eltérést jelez. Azoknál a fájloknál, amelyekben már van XMP csomag, a helyes eljárás kettős írás: az Info bejegyzés módosítása és az XMP újragenerálása, hogy a két nézet összhangban maradjon. A HotPDF mindkét felét adja ennek a műveletnek, de az, hogy együtt használd őket, már rajtad múlik

Az Info szótár szerkesztése

Az Info-oldali segédmetódusok vékonyak és kiszámíthatók. A SetLoadedTitle, SetLoadedAuthor, SetLoadedSubject, SetLoadedKeywords, SetLoadedCreator és SetLoadedProducer mind egyetlen AnsiString értéket vesznek át, és a megfelelő kulcsot írják a betöltött Info szótárba, a meglévő értéket felülírva, vagy ha a kulcs hiányzik, hozzáadva azt. Ha egy kulcsot teljesen el akarsz távolítani, például egy szivárgó /Creator bejegyzést, amely a belső eszköztáradat nevezi meg, akkor hívd a RemoveLoadedInfoKey metódust a nyers kulcsnévvel. Ezek egyik sem nyúl az XMP-hez, kizárólag ahhoz a /Info objektumhoz dolgoznak, amelyet a LoadFromFile a fájl beolvasásakor megtalált

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;

Egy részletet érdemes tisztán tartani: ezek AnsiString típusúak. ASCII címeknél ez nem gond, de a nem latin karaktereket igénylő PDF szövegsztringeket a specifikáció szerint kell kódolni, UTF-16BE byte-order markkal vagy PDFDocEncodinggel, mielőtt átadod őket. A könyvtár az általad adott bájtokat írja be a sztringobjektumba, nem találgatja ki helyetted a kódolást. Ha a címeid sima angol szövegek, ezt hagyd figyelmen kívül. Ha ékezetes vagy CJK karaktereket tartalmaznak, kódold őket tudatosan, és teszteld valódi megjelenítőben

Az XMP csomag újraírása

A SetLoadedXMPMetadata a kettős írás másik fele. Add át neki a teljes XMP csomagot AnsiString formában, és kétféleképpen viselkedik: ha a Katalógus már hivatkozik egy /Metadata streamre, akkor annak tartalmát helyben cseréli, ugyanazt az objektumszámot megtartva; ha nincs metadata stream, létrehoz egyet, /Type /Metadata és /Subtype /XML jelöléssel, objektumszámot foglal, majd a Katalógusból rákapcsolja. Mindkét esetben egy érvényes metaadatobjektumot kapsz, amelyet a megjelenítők beolvasnak

Az XML-t te adod meg, tehát te irányítod a sémát is, például a dc:title, dc:creator, xmp:CreatorTool elemeket. Ebben egyszerre van erő és felelősség: a könyvtár nem elemzi és nem is ellenőrzi a csomagot, a bájtokat tömörítés nélkül, szűrő alkalmazása nélkül írja ki. Egy hibás csomag gond nélkül átmegy a híváson, majd később törött metaadatként bukkan fel. Az XML-t gondosan építsd fel, és pontosan ugyanazokat az értékeket tükrözd vissza benne, mint amelyeket az Info szótárba írtál, hogy a két nézet soha ne mondjon ellent egymásnak

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;

Ez a sorrend, előbb Info, aztán XMP, végül mentés, az a minta, amit érdemes fejben tartani. A két hívás független egymástól, az összhang csak azért jön létre, mert ugyanazokat a sztringeket adtad át nekik. Ha egy olyan fájlon kihagyod az XMP-hívást, amelynek már van XMP csomagja, visszatérsz ahhoz a csendes elavulási hibához, amelyet ez az egész rész meg akar előzni

Diagram showing a PDF Info dictionary and an XMP metadata stream both holding title and author, edited in place alongside the bookmark outline tree
A metaadat két helyen él, az Info szótárban és az XMP streamben, mellettük a Katalógus szintű megnyitási jelzések és a vázlatfa. A helyben végzett szerkesztés mindegyiket érinti anélkül, hogy újraépítené a dokumentumot.

Hogyan szabályozd, hogyan nyíljon meg a dokumentum

Három Katalógus-bejegyzés dönti el, mit lát az olvasó abban a pillanatban, amikor a dokumentum megnyílik, és mindhárom egyetlen soros módosítás a betöltött gráfon. A SetLoadedPageMode a /PageMode értékét állítja be névobjektumként: a 'UseOutlines' megjeleníti a könyvjelzőpanelt, a 'UseThumbs' a bélyegképsávot, a 'FullScreen' a bemutató módot, a 'UseAttachments' pedig a mellékletek panelt mutatja meg (ISO 32000-1 §7.7.3.1, 28. táblázat). A SetLoadedPageLayout ugyanígy írja a /PageLayout értékét, például 'SinglePage', 'OneColumn', 'TwoColumnLeft' és a többi lehetőséget. Mindkettő a kezdő perjel nélküli nevet várja, a perjelet a könyvtár adja hozzá kimenetkor

A SetLoadedLanguage a Katalógus /Lang bejegyzését írja, amely a dokumentum egészének természetes nyelvi címkéje, például 'en-US', 'de-DE' vagy más BCP 47 tag. Itt van egy típusbeli különbség, amely sokakat megzavar: a /PageMode és a /PageLayout PDF name objektumok, a /Lang viszont string. A HotPDF ezt belül helyesen kezeli, de ha valaha megvizsgálod a kimenetet, akkor a /PageMode /UseOutlines és a /Lang (en-US) formát fogod látni, és már érted is, miért. A /Lang bejegyzés fontosabb, mint amilyennek látszik: ezt olvassa ki a kisegítő technológia a kiejtéshez, és a PDF/UA akadálymentességi megfeleléshez kötelező is

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;

Könyvjelzők átnevezése a fa megbolygatása nélkül

A könyvjelzőcímek javítása rutinmunka, egy elgépelés egy címsorban vagy egy fejezet újraszámozása a vázlat elkészülte után. A SetLoadedOutlineTitle egy nullától indexelt pozíciót vár a legfelső szintű vázlatbejegyzések között és egy új címet, végigjárja a Katalógus → /Outlines/First/Next láncot addig a pontig, majd lecseréli a bejegyzés /Title sztringjét. Csak a címet módosítja, a cél, a nyitott/zárt állapot és a gyermekstruktúra érintetlen marad

if Pdf.LoadFromFile('report.pdf', '') > 0 then
begin
  Pdf.SetLoadedOutlineTitle(0, 'Executive Summary');
  Pdf.SetLoadedOutlineTitle(1, 'Financial Results');
  Pdf.SaveLoadedDocument('report-renamed.pdf');
end;

Az átnevezés azért biztonságos, mert soha nem nyúl a szerkezeti számlálókhoz. A vázlatbejegyzés törlése az a helyzet, ahol könnyű hibázni, és akkor is érdemes megérteni, ha éppen csak átnevezel, mert megmutatja, mit nem szabad kézzel szerkeszteni. Minden vázlatcsomópont tartalmaz egy /Count értéket, és az ISO 32000-1 §12.3.3 szerint ez nem a közvetlen gyermekek számát jelenti. Ez a látható leszármazottak teljes száma: ha a /Count pozitív és N, akkor N leszármazott jelenleg meg van nyitva, ha pedig negatív, akkor a csomópontnak vannak leszármazottai, de össze van csukva. Amikor egy legfelső szintű bejegyzést eltávolítasz, a /Outlines gyökérszintű számlálóját nem lehet egyszerűen eggyel csökkenteni; újra kell számolni úgy, hogy minden megmaradó felső szintű csomópontra összeadod az "egy a csomópontért plusz a pozitív /Count értéke", miközben kihagyod a bármely összehajtott, vagyis negatív számlálójú csomópont leszármazottait. Ha ezt elrontod, az olvasó által mutatott könyvjelzőszám elcsúszik, és minden törlésnél többel változik eggyel. Az átnevezés mindezt megkerüli, ami még egy ok arra, hogy célzott segédfüggvényt használj ahelyett, hogy magát a szótárt piszkálnád

Hogyan marad helyben a mentés

Minden fenti módosítás memóriában lévő objektumokat változtat meg, a lemezre semmi sem kerül, amíg a SaveLoadedDocument le nem fut. Azért olcsó ez a megközelítés, mert a mentés nem generálja újra a dokumentumot, hanem megőrzi a meglévő objektumszámokat és azt a szerkezetet, amelyet a HotPDF betöltéskor elemzett, és ugyanazt a gráfot írja vissza a néhány módosított vagy újonnan lefoglalt objektummal együtt. Ez az oka annak, hogy egy metaadat-passz nem írja újra a teljes fájlt, és ugyanaz a helyben frissítő mechanizmus teszi lehetővé az objektumstreamek és az inkrementális frissítések működését. Ha a forrásfájlok Wordből vagy más irodai csomagból származnak, az objektumelrendezésüknek megvannak a maga sajátosságai, amelyeket érdemes ismerni még a szerkesztés előtt; a hibrid hivatkozású xref streamekről Office PDF-ekben szóló cikk azt mutatja meg, hogyan épülnek fel ezek a fájlok, és mi marad meg egy oda-vissza kör után

Két határt kell tiszteletben tartani. Először is ez helyben szerkesztő modell, nem redakciós vagy tisztítási eszköz: egy Info kulcs eltávolítása azt a kulcsot törli, de nem súrolja le a korábbi inkrementális frissítési generációban esetleg megmaradó régebbi értékeket. Ha valódi érzékeny metaadat-törlés a követelmény, az egy másik, nehezebb művelet. Másodszor az XMP írás szó szerinti, a könyvtár megbízik az XML-ben, és nem ellenőrzi azt, ezért PDF/A vagy szigorú ellenőrző céljára készülő fájloknál ismert jó sablonból generáld a csomagot, majd ellenőrizd a kimenetet. Ezeken a határokon belül a helyben végzett metaadatszerkesztés pontosan megfelelő eszköz: a néhány hibás bájtot javítja ki, és a fájl kilencvenkilenc százalékát úgy hagyja, ahogy az eredeti előállító megírta

Az itt bemutatott betöltött dokumentum írási API a szokásos HotPDF Component része Delphire és C++Builderre, a metaadat-, vázlat- és Katalógus-szerkesztő metódusok teljes készletével együtt