2010/08/02

[C#] 用 iTextSharp 讀寫 pdf 檔中的 XMP (metadata) 資訊

由於工作上需要將 pdf 檔案依政府的要求, 將分類檢索的資訊加註於 XMP 中, 所以才會開始研究這部分.
不太懂分類檢索的人可以參考 此網頁 中的資訊.
開發工具與環境:

  • iTextSharp: Java 最常用於操作 PDF 的套件是 iText, 此套件也有 .Net 版, 叫做 iTextSharp. ( 下載點)
    因為 iTextSharp 的功能跟 Java 版的 iText 大同小異, 所以文件的部分可參考 iText 的 document. (online版)
  • Visual Studio 2008
  • XMP: 請參考 Adobe 的 網頁說明.
  1. 下載 iTextSharp 並解壓縮後, 會有一個 itextsharp.dll 檔, 記得在專案中將此 dll 加入參考.
    因為本文所用到的類別與函式並不多, 所以只需要 using 以下的命名空間:
    using System;
    using System.IO;
    using System.Text;
    using System.Xml;
    using iTextSharp.text.pdf;
    using iTextSharp.text.xml.xmp;
  2. 首先是讀取 pdf 中的 XMP 資訊:
    1. 使用 PdfReader 這個類別.
    2. 轉換後的字串可以利用 XmlDocument 做其他利用.
    3. 若要對 XmlDocument 做進一步的查詢, 可參考 此篇 做 XmlNamespaceManager 的初始設定.
    4. 需注意有可能無法取得 pdf 中的 XMP 資訊, 此時的 Metadata 會是 null.
    PdfReader pdfSourceReader = new PdfReader(@"c:\source.pdf");
    XmlDocument docSource = new XmlDocument();
    if (pdfSourceReader.Metadata != null)
    {
        //用UTF-8編碼, 以避免XML的亂碼
        string strMetadata = Encoding.UTF8.GetString(pdfSourceReader.Metadata);
        //載入至XmlDocument
        docSource.LoadXml(strMetadata);
    }
  3. 將 XMP 資訊寫入至 pdf 檔中:
    1. 使用 PdfStamper 這個類別, 做 pdf 檔的複製.
    2. 由於分類檢索主要是 Dublin Code 的資訊, 所以利用 iTextSharp 的 DublinCoreSchema 類別進行設定.
    3. 利用 XmpWriter 這個類別包裝新的 XMP 資訊.
    4. 若要對 XmlDocument 做進一步的查詢, 又不太知道 xmlns 的設定值為何, 可參考 此篇 做 XmlNamespaceManager 的初始設定.
      我在這部分寫了一個 GetNs() 函式來處理, 如下:
      public static XmlNamespaceManager GetNs(XmlDocument doc)
      {
          XmlNamespaceManager mgr = new XmlNamespaceManager(doc.NameTable);
          XmlNodeList lstNodes = doc.SelectNodes("//child::*");
          foreach (XmlNode node in lstNodes)
          {
              foreach (XmlAttribute attr in node.Attributes)
              {
                  if (attr.Prefix.Equals("xmlns"))
                  {
                      if (!mgr.HasNamespace(attr.LocalName))
                      {
                          mgr.AddNamespace(attr.LocalName, attr.Value);
                      }
                  }
              }
          }
          return mgr;
      }
    5. 要從 XmlDocumentA 複製一個 XmlNode 到 XmlDocumentB, 可參考 此網頁 的說明.
    pdf 的 XMP 與複製的 Sample Code 如下:
    //建立一個設定分類檢索資訊的DublinCoreSchema物件
    DublinCoreSchema dcSchema = new DublinCoreSchema();
    dcSchema.SetProperty(DublinCoreSchema.TITLE, new LangAlt("RSS 詮釋資料基準範例"));
    dcSchema.SetProperty(DublinCoreSchema.CREATOR, new LangAlt("行政院研究發展考核委員會"));
    dcSchema.SetProperty(DublinCoreSchema.SUBJECT, new LangAlt("RSS 詮釋資料基準範例"));
    dcSchema.SetProperty(DublinCoreSchema.DESCRIPTION, new LangAlt("RSS 詮釋資料基準範例"));
    dcSchema.SetProperty(DublinCoreSchema.CONTRIBUTOR, new LangAlt("行政院研究發展考核委員會"));
    dcSchema.SetProperty(DublinCoreSchema.TYPE, new LangAlt("text/pdf"));
    dcSchema.SetProperty(DublinCoreSchema.FORMAT, new LangAlt("pdf"));
    dcSchema.SetProperty(DublinCoreSchema.SOURCE, new LangAlt("行政院研究發展考核委員會"));
    dcSchema.SetProperty(DublinCoreSchema.LANGUAGE, new LangAlt("中文"));
    dcSchema.SetProperty(DublinCoreSchema.COVERAGE, new LangAlt("2009-03-31~2015-12-20"));
    dcSchema.SetProperty(DublinCoreSchema.PUBLISHER, new LangAlt("行政院研究發展考核委員會"));
    dcSchema.SetProperty(DublinCoreSchema.DATE, new LangAlt("2009-03-31T15:49:18+08:00"));
    dcSchema.SetProperty(DublinCoreSchema.IDENTIFIER, new LangAlt("341000000A"));
    dcSchema.SetProperty(DublinCoreSchema.RIGHTS, new LangAlt("行政院研究發展考核委員會"));
    dcSchema.SetProperty("Category.Theme", new LangAlt("000"));
    dcSchema.SetProperty("Category.Cake", new LangAlt("000"));
    dcSchema.SetProperty("Category.Service", new LangAlt("I60"));
    //建立一個存放最後要輸出的XmlDocument物件
    XmlDocument docTarget = new XmlDocument();
    //利用MemoryStream做XmpWriter的輸出Stream
    using (MemoryStream ms = new MemoryStream())
    {
      XmpWriter xmpWriter = new XmpWriter(ms);
      //將存放分類檢索資訊的DublinCoreSchema透過XmpWriter輸出到Stream
      xmpWriter.AddRdfDescription(dcSchema);
      //最後記得要呼叫Close(), 不然XMP的資訊會不完整
      xmpWriter.Close();
      //將XMP的資訊載入至欲輸出的XmlDocument
      docTarget.LoadXml(Encoding.UTF8.GetString(ms.ToArray()));
    }
    //如果要把原始pdf中的XMP資訊也append到新的pdf檔中, 可參考以下的Code:
    //---------- Import Start ----------
    //設定原始XMP中的NamespaceManager
    XmlNamespaceManager mgrSource = GetNs(docSource);
    //取得原始XMP的<rdf:RDF>
    XmlNode nodeSourceRdf = docSource.SelectSingleNode("//rdf:RDF", mgrSource);
    //設定新XMP中的NamespaceManager
    XmlNamespaceManager mgrTarget = GetNs(docTarget);
    //取得新XMP的<rdf:RDF>
    XmlNode nodeTargetRdf = docTarget.SelectSingleNode("//rdf:RDF", mgrTarget);
    //如果有原始的XMP資訊,開始進行<rdf:Description>的複製
    if (nodeSourceRdf != null)
    {
      foreach (XmlNode nodeDesc in nodeSourceRdf.ChildNodes)
      {
        nodeTargetRdf.AppendChild(nodeTargetRdf.OwnerDocument.ImportNode(nodeDesc, true));
      }
    }
    //---------- Import End ----------
    //設定要輸出的pdf.(第一個參數是讀取原始pdf檔的PdfReader物件)
    PdfStamper pdfStamper = new PdfStamper(pdfSourceReader, 
      new FileStream(@"c:\stamped.pdf", FileMode.OpenOrCreate, FileAccess.Write));
    //將新的XMP資訊設定至欲輸出的pdf檔
    pdfStamper.XmpMetadata = Encoding.UTF8.GetBytes(docTarget.OuterXml);
    //呼叫Close(), 以完成pdf檔的輸出(複製)程序
    pdfStamper.Close();
由於各家實作出的 XMP 標準不一, 輸出也不太相同, 且有時會受限於 pdf 檔的權限或製作程式, 造成無法讀取或寫入 XMP 資訊.
因此建議程式開發過程中要多一點錯誤判斷 (ex: try…catch), 以避免程式出錯或當掉.
補充: 網路上還有一套可讀寫 XMP 的套件 (C# XMP Toolkit), 有興趣的也可以試試.

沒有留言: