Tekninen artikkeli

Ladattujen PDF-metatietojen muokkaaminen Delphissä ilman uudelleenkirjoittamista

Sinulla on kymmenentuhatta sopimus-PDF-tiedostoa kymmenestä eri ohjelmasta, ja lakiosasto haluaa, että jokainen niistä sisältää oikean Author-arvon, korjatun Producer-merkkijonon ja lukutilan, joka avaa kirjanmerkkipaneelin käynnistyksen yhteydessä. Naiivi tapa korjata tämä on ladata jokainen tiedosto, asettaa sivut uudelleen ja kirjoittaa kokonaan uusi asiakirja. Tällöin olet kuitenkin juuri heittänyt pois kaikki olemassa olevat objektinumerot, inkrementaalisen päivityshistorian, mahdolliset digitaaliset allekirjoitukset ja alkuperäisen työkalun tuottaman huolellisesti optimoidun ristiviittaustaulukon (xref). Sivut näyttävät identtisiltä, mutta tiedosto on rakenteellisesti vieras. Metatietojen muokkausta varten tämä on täysin väärä vaihtokauppa

Oikea tapa toimia on käsitellä ladattua asiakirjaa objektikaaviona, jota muokataan paikan päällä: kurotetaan Info-sanakirjaan (Info dictionary), /Metadata-virtaan ja luetteloon (Catalog), muutetaan ne muutamat merkinnät, joista välitetään, ja kirjoitetaan lopputulos takaisin tiedostoon. HotPDF, joka on alkuperäinen VCL PDF -komponentti Delphille ja C++Builderille, tuo esiin juuri tämän rajapinnan ladatun asiakirjan kirjoitus-API:nsa kautta. Tässä artikkelissa käsitellään tämän API:n oikeaa käyttöä sekä sitä yhtä virhettä, jonka lähes kaikki tekevät: Info-sanakirjan muokkaamista unohtaen, että samojen metatietojen toinen kopio elää XMP-muodossa

Samat metatiedot tallennetaan kahteen paikkaan, ja ne ovat ristiriidassa

PDF kuljettaa asiakirjan tietoja kahdessa rinnakkaisessa sijainnissa, mikä on syynä useimpiin 'vaihdoin otsikon, mutta Acrobat näyttää yhä vanhan' -tukipyyntöihin. Ensimmäinen on asiakirjan tietosanakirja (document information dictionary), eli perinteinen /Info-objekti, jossa on avaimet /Title, /Author, /Subject, /Keywords, /Creator ja /Producer, määriteltynä ISO 32000-1 §14.3.3 -standardissa. Toinen on XMP-paketti, eli XML-asiakirja, joka on tallennettu virtana (stream) luettelon (Catalog) alle avaimella /Metadata, määriteltynä kohdassa §14.3.2 ja perustuen Adoben XMP-tietomalliin

Molemmat voivat sisältää otsikon. Mikään standardissa ei pakota niitä olemaan samaa mieltä. Nykyaikaiset katseluohjelmat ja useimmat PDF/A-validaattorit suosivat XMP-pakettia silloin, kun se on olemassa, ja turvautuvat Info-sanakirjaan vasta sen puuttuessa. Joten jos päivität vain /Info-osion – mitä suurin osa 'aseta PDF-metatiedot' -koodista tekee – XMP:hen luottava lukija näyttää edelleen vanhan arvon ja PDF/A-tarkistus raportoi ristiriidasta. Oikea tapa toimia missä tahansa tiedostossa, jossa on jo XMP-paketti, on kirjoittaa molempiin: muuttaa Info-merkintää ja luoda XMP uudelleen, jotta nämä kaksi pysyvät yhdenmukaisina. HotPDF tarjoaa molemmat puolet; velvollisuus käyttää niitä yhdessä on sinulla

Info-sanakirjan (Info dictionary) muokkaaminen

Info-puolen apuohjelmat ovat kevyitä ja ennalta arvattavia. Metodit SetLoadedTitle, SetLoadedAuthor, SetLoadedSubject, SetLoadedKeywords, SetLoadedCreator ja SetLoadedProducer ottavat kukin yhden AnsiString-argumentin ja kirjoittavat vastaavan avaimen ladattuun Info-sanakirjaan korvaten arvon, jos avain on olemassa, ja lisäten sen muuten. Jos haluat poistaa jonkin avaimen kokonaan – esimerkiksi tietoturvalle vaarallisen /Creator-avaimen, joka paljastaa sisäiset työkalusi – kutsu metodia RemoveLoadedInfoKey pelkällä avaimen nimellä. Mikään näistä ei koske XMP-tietoon; ne toimivat pelkästään /Info-objektissa, jonka LoadFromFile löysi jäsentäessään tiedoston

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;

Yksi rehellinen yksityiskohta: nämä ottavat AnsiString-tyypin. ASCII-otsikoille tämä ei ole ongelma, mutta PDF-tekstimerkkijonot, jotka tarvitsevat muita kuin latinalaisia merkkejä, on koodattava standardin mukaisesti – joko UTF-16BE-muodossa tavujärjestysmerkillä (BOM) tai PDFDocEncoding-muodossa – ennen kuin välität ne eteenpäin. Kirjasto kirjoittaa sille antamasi tavut merkkijono-objektiin; se ei arvaa koodausta puolestasi. Jos otsikkosi ovat pelkkää englantia, voit ohittaa tämän. Jos ne sisältävät aksenttimerkkejä tai CJK-merkkejä (kiina, japani, korea), koodaa ne tarkoituksella oikein ja testaa todellisessa katseluohjelmassa

XMP-paketin uudelleenkirjoittaminen

SetLoadedXMPMetadata on tuplakirjoituksen toinen puoli. Anna sille täysi XMP-paketti AnsiString-muodossa, ja se tekee toisen kahdesta asiasta: jos luettelossa (Catalog) on jo viittaus /Metadata-virtaan, it korvaa kyseisen virran sisällön paikoillaan säilyttäen saman objektinumeron; jos metatietovirtaa ei ole, se luo sellaisen, merkitsee sen tyypeillä /Type /Metadata ja /Subtype /XML, varaa objektinumeron ja linkittää sen luettelosta. Molemmissa tapauksissa saat tulokseksi kelvollisen metatieto-objektin, jonka katseluohjelmat osaavat lukea

Sinä toimitat XML-koodin, mikä tarkoittaa, että hallitset skeemaa – dc:title, dc:creator, xmp:CreatorTool ja niin edelleen. Siinä on valtaa ja vastuuta samassa paketissa: kirjasto ei jäsennä tai validoi pakettiasi, ja se kirjoittaa tavut pakkaamattomana ilman mitään virta-suodatinta (stream filter). Väärin muotoiltu paketti menee kutsusta läpi ilman virhettä, mutta aiheuttaa myöhemmin valituksia rikkinäisistä metatiedoista. Rakenna XML huolellisesti ja peilaa siihen tarkalleen ne arvot, jotka kirjoitit Info-sanakirjaan, jotta nämä kaksi näkymää eivät koskaan ole ristiriidassa

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;

Tämä järjestys – ensin Info, sitten XMP ja lopuksi tallennus – on kaava, joka kannattaa sisäistää. Nämä kaksi kutsua ovat itsenäisiä; yhtenäisyys on olemassa vain siksi, että annoit niille samat merkkijonot. Jos ohitat XMP-kutsun tiedostossa, jossa on XMP-paketti, päädyt takaisin siihen hiljaiseen vanhentumisvirheeseen, jonka ehkäisemiseksi tämä koko osio on olemassa

Diagram showing a PDF Info dictionary and an XMP metadata stream both holding title and author, edited in place alongside the bookmark outline tree
Metatiedot elävät kahdessa paikassa – Info-sanakirjassa ja XMP-virrassa – plus Catalog-tason lukuvihjeissä ja jäsennyspuussa. Paikan päällä tehtävä muokkaus koskee jokaiseen ilman asiakirjan uudelleenrakentamista.

Sen ohjaaminen, miten katseluohjelma avaa tiedoston

Kolme luettelomerkintää (Catalog entries) määrittää, mitä lukija näkee heti asiakirjan avautuessa, ja kaikki kolme ovat yhden rivin muokkauksia ladattuun kaavioon. SetLoadedPageMode kirjoittaa avaimen /PageMode nimiobjektina: välitä arvo 'UseOutlines' avataksesi kirjanmerkkipaneelin, 'UseThumbs' esikatselukuvakiskoa varten, 'FullScreen' esitystilaa varten tai 'UseAttachments' näyttääksesi liitetiedostopaneelin (ISO 32000-1 §7.7.3.1, Taulukko 28). SetLoadedPageLayout kirjoittaa avaimen /PageLayout samalla tavalla – 'SinglePage', 'OneColumn', 'TwoColumnLeft' ja niin edelleen. Molemmat ottavat nimen ilman alkavaa vinoviivaa; kirjasto lisää sen tulosteeseen

SetLoadedLanguage kirjoittaa luettelon (Catalog) /Lang-merkinnän, eli koko asiakirjan luonnollisen kielen tunnisteen – esimerkiksi 'en-US', 'de-DE', eli BCP 47 -kielitunnisteen. Huomaa tyyppiero, joka sekoittaa ihmisiä: /PageMode ja /PageLayout ovat PDF-nimiobjekteja (name objects), kun taas /Lang on merkkijono (string). HotPDF tekee tämän sisäisesti oikein, mutta jos koskaan tarkastelet raakatulostetta, näet merkinnät /PageMode /UseOutlines ja /Lang (en-US), ja nyt tiedät miksi. Kielimerkinnällä /Lang on enemmän merkitystä kuin miltä näyttää: avustava teknologia lukee sen valitakseen oikean ääntämisen, ja se on ehdoton vaatimus PDF/UA-esteettömyysstandardin noudattamiselle

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;

Kirjanmerkkien nimeäminen uudelleen häiritsemättä puurakennetta

Kirjanmerkkien otsikot ovat rutiininomaista siivottavaa – kirjoitusvirhe otsikossa tai luvun uudelleennumerointi jäsennyspuun rakentamisen jälkeen. SetLoadedOutlineTitle ottaa nollapohjaisen indeksin ylätason kirjanmerkkeihin ja uuden otsikon, käy läpi polun Catalog → /Outlines/First/Next kyseiseen paikkaan ja korvaa kohteen /Title-merkkijonon. Se muuttaa vain otsikon; kohde, auki/kiinni-tila ja lapsirakenne säilyvät ennallaan

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

Otsikon muuttaminen on turvallista juuri siksi, ettei se koskaan koske rakenteellisiin laskureihin. Kirjanmerkkikohteen poistaminen on se tapaus, joka aiheuttaa ongelmia, ja se on syytä ymmärtää silloinkin, kun vain nimeät otsikoita uudelleen, sillä se kertoo, mihin ei pidä koskea käsin. Jokainen kirjanmerkkisolmu sisältää /Count-avaimen, ja – standardin ISO 32000-1 §12.3.3 mukaisesti – tämä määrä ei ole välittömien lapsisolmujen määrä. Se on näkyvien jälkeläisten kokonaismäärä: positiivinen /Count arvolla N tarkoittaa, että N jälkeläistä on tällä hetkellä näkyvissä, kun taas negatiivinen arvo tarkoittaa, että solmulla on jälkeläisiä, mutta se on suljettuna (collapsed). Kun ylätason kohde poistetaan, /Outlines-juuren laskuria ei voi vain vähentää yhdellä; se on laskettava uudelleen laskemalla yhteen jokaisen jäljelle jääneen ylätason solmun kohdalta 'yksi solmulle itselleen plus sen positiivinen /Count', ohittaen suljettujen (negatiivinen laskuri) solmujen jälkeläiset. Tee tämä väärin, ja lukijan näyttämä kirjanmerkkien kokonaismäärä heittää – se hyppää enemmän kuin yhdellä poistoa kohden. Otsikon uudelleennimeäminen ohittaa tämän kaiken, mikä on jälleen yksi syy suosia kohdennettua apumetodia sen sijaan, että sorkkisi sanakirjaa itse

Miten tallennus säilyy paikoillaan

Jokainen yllä oleva muokkaus muuttaa objekteja muistissa; mikään ei päädy levylle ennen kuin SaveLoadedDocument suoritetaan. Syy siihen, miksi tämä lähestymistapa on edullinen, on se, ettei tallennus luo asiakirjaa uudelleen – se säilyttää olemassa olevat objektinumerot ja rakenteen, jonka HotPDF jäsenti ladattaessa, kirjoittaen takaisin saman kaavion ja vain ne muutamat muuttuneet ja uudet varatut objektit. Tämä estää metatietopäivitystä kirjoittamasta koko tiedostoa uusiksi, ja se on sama paikan päällä tehtävän päivityksen (in-place update) koneisto, joka saa objektivirrat ja inkrementaaliset päivitykset toimimaan. Jos lähdetiedostosi ovat peräisin Word-ohjelmasta tai muusta toimisto-ohjelmistosta, niiden objektiasettelussa on omat erikoisuutensa, jotka on hyvä tietää ennen muokkaamista; artikkeli toimisto-PDF-tiedostojen hybridi-ristiviittausvirroista käsittelee näiden tiedostojen rakennetta ja sitä, mikä selviää edestakaisesta kierroksesta

Kaksi rajaa, joita on kunnioitettava. Ensinnäkin tämä on paikan päällä muokkaava malli, ei mikään poisto- (redaction) tai puhdistustyökalu (sanitization tool): Info-avaimen poistaminen poistaa kyseisen avaimen, mutta se ei pyyhi vanhempia arvoja, jotka saattavat säilyä tiedoston aiemmissa inkrementaalisissa päivityksissä. Jos vaatimuksesi on arkaluontoisten metatietojen todellinen poistaminen, se on toinen, raskaampi operaatio. Toiseksi XMP-kirjoitus on kirjaimellinen – kirjasto luottaa XML-koodiisi eikä validoi sitä – joten jos tuote menee PDF/A- tai muuhun tiukkaan validointiin, luo paketti tunnetusti toimivasta pohjasta ja varmista lopputulos. Näissä rajoissa käytettynä paikan päällä tapahtuva metatietojen muokkaus on oikean kokoinen työkalu: se korjaa ne muutamat tavut, jotka ovat väärin, ja jättää loput 99 prosenttia tiedostosta täsmälleen sellaiseksi kuin alkuperäinen tuottaja sen kirjoitti

Tässä esitetty ladatun asiakirjan kirjoitus-API toimitetaan standardin HotPDF-komponentti-tuotteen mukana Delphille ja C++Builderille yhdessä metatieto-, jäsennys- ja Catalog-muokkausmetodien täydellisen sarjan kanssa