關於 C++ 打印 PDF 打印及 PDF 轉圖片、合併

原文: http://www.aqcoder.com/post/content?id=42

pdf(Portable Document Format 的簡稱,意爲“便攜式文檔格式”),是由 Adobe Systems 用於與應用程序、操作系統、硬件無關的方式進行文件交換所發展出的文件格式。PDF 文件以 PostScript 語言圖象模型爲基礎,無論在哪種打印機上都可保證精確的顏色和準確的打印效果,即 PDF 會忠實地再現原稿的每一個字符、顏色以及圖象。

PDF 設計的初衷是爲了解決多平臺的打印問題,所以他不像 WORD 那樣具有文檔流結構,編輯方面沒有 WORD 那樣強大。

打印

需要打印 PDF 自然需要一個打印程序,可以打印 PDF 的程序有很多。Adobe 自家的有 PDF Reader、 Acrobat。商用的有金山 WPS、Foxit Reader。開源的有 xpdf reader 等。

當然,作爲程序猿我們說的打印自然不是討論如何使用這些軟件打印 PDF。而是這麼使用程序打印 PDF。

應用場景:第三方給你的系統提供了 PDF 格式的文件,在你的系統中有個打印按鈕,點擊之後去打印這個 PDF。

實現方法一

在客戶端上安裝一個 PDF 渲染程序(上面的幾個 PDF 瀏覽器/編輯器程序都含有渲染功能),然後使用命令行調用打印。各種語言啓動進程的方法這裏就不贅述了。

顯然,這種方法有個很大的缺點,要求客戶機上必須安裝一個渲染程序,或者吧他的程序包在你的程序包裏,這樣會有版權問題,還會使你的程序包變大。

實現方法二

講方法二前,我們先來說說網傳的一種方法

if (OpenPrinterA("Microsoft XPS Document Writer", &hPrinter, NULL))
{
    if (StartDocPrinterA(hPrinter, 1, (LPBYTE)&di))
    {
        if (StartPagePrinter(hPrinter))
        {
            bSuccess = WritePrinter(hPrinter, buffer, length, &dwWritten);
            EndPagePrinter(hPrinter);
        }
        EndDocPrinter(hPrinter);
    }
    ClosePrinter(hPrinter);
}
if (bSuccess == false)
{
    dwError = GetLastError();
}

這是 C++ 的實現網上還有一個 C# 的實現。

這種方法是行不通的 ,你會發現有的人在帖子裏回覆何以,有的人回覆不可以。這是爲什麼呢。

因爲打印機一般只識別打印格式,如 PostScript 格式,而 PDF 格式是在 PostScript 格式之上的。但是有些新型的打印機也會識別 PDF 格式。這就是爲什麼有的人測試不可以,有的人測試可以的原因。

現在我們來討論方法二。直接發送 PDF 文件到打印機是行不通的,所以只能乖乖的使用打印機 GDI 接口,吧 PDF 渲染到打印機 DC 上。

所以使用這個方法的前提是需要一個 PDF 渲染器,pdfium 是 Google Chromium 的項目的一部分,也是一個 PDF 渲染器,我們可以使用 pdfium 解析 PDF 文件,並渲染到打印機 DC 上。

由於這種方法工程量有點大,所以我最終選擇了第三種方法。

方法三

既然使用一個 PDF 渲染器工程量比較大,那麼我們是否可以吧 PDF 文件變成圖片呢。把圖片渲染到打印機 DC 上就很容易了。

 -----       ------        -------------
| pdf | --> | image | --> | print DC API |
 -----       ------        -------------

由於我們後端是用 JAVA 實現的,所以轉化這一步自然也可以挪到後端去做,後端我們選擇了 Apache PDFBox 作爲轉化庫。PDFbox 轉化爲圖片就很容易了:

public static List<BufferedImage> toImageList(InputStream pdf)
      throws InvalidPasswordException, IOException {
    int dpi = 96;
    PDDocument document = PDDocument.load(pdf);
    PDFRenderer renderer = new PDFRenderer(document);
    renderer.setSubsamplingAllowed(true);
    List<BufferedImage> ret = new ArrayList<BufferedImage>();
    for (int i = 0; i < document.getNumberOfPages(); i++) {
      BufferedImage bi = renderer.renderImageWithDPI(i, dpi, ImageType.RGB);
      ret.add(bi);
    }
    document.close();
    return ret;
  }

通過以上代碼我們發現 PDFBox 也是一個 PDF 渲染器,但是客戶端程序一般不會去用 JAVA 做,若果你的客戶端程序是 JAVA 寫的,哪可直接使用打印功能了。

那麼在 C++ 裏打印圖片功能如何實現呢:

// data 爲圖片數據,這裏我們使用圖片 base64 數據,注意要去掉 base64 頭部。
bool PrintImage(const std::string& printer, const std::string& data) {
  bool bAtoFit = true;
  char szDriver [16] = "WINSPOOL";
  char szPrinter [1024];
  DWORD cchBuffer = 255;
  HDC hdcPrint = NULL;
  HANDLE hPrinter = NULL;
  PRINTER_INFO_2A * pPrinterData;
  BYTE pdBuffer [102400];
  BOOL bReturn = FALSE;

  DWORD cbBuf = sizeof(pdBuffer);
  DWORD cbNeeded = 0;
  pPrinterData =(PRINTER_INFO_2A *)&pdBuffer[0];

  if (printer.empty())
      GetDefaultPrinterA(szPrinter, &cchBuffer);
  else
      strcpy_s(szPrinter, printer.c_str());

  if(!OpenPrinterA(szPrinter, &hPrinter, NULL))
  {
      return false;
  }

  if(GetPrinterA(hPrinter, 2, &pdBuffer[0], cbBuf,&cbNeeded))
      ClosePrinter(hPrinter);
  else
  {
      callback->Failure(-1, "get printer fail.");
      return false;
  }

  hdcPrint = CreateDCA(szDriver, szPrinter, pPrinterData->pPortName, NULL);
  CDC dc;
  if (!dc.Attach(hdcPrint)) {
      callback->Failure(-1, "No printer found!");
      return false;
  }

  dc.m_bPrinting = TRUE;
  DOCINFO di;
  // Initialise print document details
  ::ZeroMemory (&di, sizeof (DOCINFO));
  di.cbSize = sizeof (DOCINFO);
  di.lpszDocName = L"cef_print_image";
  // Begin a new print job
  BOOL bPrintingOK = dc.StartDoc(&di);
  // Get the printing extents
  // and store in the m_rectDraw field of a 
  // CPrintInfo object
  CPrintInfo Info;
  // just one page
  Info.SetMaxPage(1);
  int maxw = dc.GetDeviceCaps(HORZRES);
  int maxh = dc.GetDeviceCaps(VERTRES);
  Info.m_rectDraw.SetRect(0, 0, maxw, maxh);
  for (UINT page = Info.GetMinPage(); page <= Info.GetMaxPage() && bPrintingOK; page++)
  {
      // begin new page
      dc.StartPage();
      Info.m_nCurPage = page;

      // get bitmap
      CefRefPtr<CefBinaryValue> pData = CefBase64Decode(data);
      size_t lenDes = pData->GetSize();
      char* pDes = new char[lenDes];
      pData->GetData(pDes, lenDes, 0);

      BITMAP bm;
      CBitmap* pbmp = NULL;
      HBITMAP Hbitmap = NULL;
      HGLOBAL hMem = ::GlobalAlloc(GMEM_MOVEABLE, lenDes);
      LPVOID pImage = ::GlobalLock(hMem);
      memcpy(pImage, pDes, lenDes);
      IStream* pStream = NULL;
      ::CreateStreamOnHGlobal(hMem, FALSE, &pStream);
      Gdiplus::Bitmap gdi(pStream);
      gdi.GetHBITMAP(NULL, &Hbitmap);
      pbmp = CBitmap::FromHandle(Hbitmap);
      pbmp->GetBitmap(&bm);

      int w = bm.bmWidth;
      int h = bm.bmHeight;
      float rate = (float)maxw / w;
      // create memory device context
      CDC bmpDC;
      bmpDC.CreateCompatibleDC(&dc);
      bmpDC.SetMapMode(dc.GetMapMode());
      CBitmap *pBmp = bmpDC.SelectObject(pbmp);

      dc.SetStretchBltMode(STRETCH_DELETESCANS);
      // now stretchblt to maximum width on page
      if (bAutoFit)
          dc.StretchBlt(0, 0, maxw, maxh, &bmpDC, 0, 0, w, h, SRCCOPY);
      else
          dc.StretchBlt(0, 0, maxw, int(h * rate), &bmpDC, 0, 0, w, h, SRCCOPY);
      // clean up
      bmpDC.SelectObject(pBmp);
      pStream->Release();
      GlobalUnlock(hMem);
      GlobalFree(hMem);
      delete[] pDes;
      bPrintingOK = (dc.EndPage() > 0);   // end page
  }

  if (bPrintingOK)
  {
      dc.EndDoc(); // end a print job
      return true;
  }
  else
  {
      dc.AbortDoc();
      return false;
  }
}

用 C# 實現打印圖片的功能估計會更容易一些。

合併問題

有時候我們會遇到需要將多個 PDF 合併到一起,PDFbox 有合併的功能:

public static void mergePdf(List<InputStream> pdfList, OutputStream out) throws IOException {
  PDFMergerUtility merger = new PDFMergerUtility();
  merger.addSources(pdfList);
  merger.setDestinationStream(out);
  merger.mergeDocuments(MemoryUsageSetting.setupMainMemoryOnly());
}

假設有兩份 PDF 文件 pdf1,pdf2 這兩份 PDF 文件都只含有一頁,並且內容只有一行文字:

pdf1

圖 2-1 pdf1

pdf2

圖 2-2 pdf1

合併以後將會得到一份兩頁的 PDF 文件。這是因爲 PDF 並不是 流式文檔結構 的。

文檔結構

摘自百度百科

PDF文件結構主要可以分爲四個部分:

首部

用文本編輯器打開的時候就可以看到:%PDF-1.4 這樣的字眼,其中最後一位就是PDF文件格式版本號,軟件的版本號總要比文件格式的版本號高1,比如說Read 5能打開的內容就是4。
文件體

裏面由若干個的obj對象來組成,類似這種形式:

3 0 obj
<<
/Type /Pages
/Count 1
/Kids [4 0 R]
>>
endobj

第一個數字稱爲對象號,來唯一標識一個對象的,第二個是產生號,是用來表明它在被創建後的第幾次修改,所有新創建的PDF文件的產生號應該都是0,即第一次被創建以後沒有被修改過。上面的例子就說明該對象的對象號是3,而且創建後沒有被修改過。

對象的內容應該是包含在<< 和>>之間的,最後以關鍵字endobj結束。

交叉引用表

用來索引各個obj 對象在文檔中的位置,以實現隨機訪問,它的形式是:

xref
0 8
0000000000 65535f
0000000009 00000n
0000000074 00000 n
0000000120 00000 n
0000000179 00000 n
0000000322 00000 n
0000000415 00000 n
0000000445 00000 n

xref說明一個交叉引用表的開始,交叉引用表的第一行0 8 說明下面各行所描述的對象號是從0開始,並且有8個對象。

0000000000 65535f,一般每個PDF文件都是以這一行開始交叉應用表的,說明對象0的起始地址爲0000000000,產生號(generation number)爲65535,也是最大產生號,不可以再進行更改,而且最後對象的表示是f, 表明該對象爲free, 這裏,大家可以看到,其實這個對象可以看作是文件頭。

0000000009 00000n就是表示對象1,0000000009是其偏移地址,00000爲5位產生號(最大爲65535),0表明該對象未被修改過, n表示該對象在使用,區別與自由對象(f),可以更改。

尾部

Trailer
<<
/Size 8
/Root 1 0 R
>>
startxref
553
%%EOF

trailer 說明文件尾 trailer對象的開始。

/Size 8說明該PDF文件的對象數目。

/Root 1 0 R說明根對象的對象號爲1。

Startxref

553說明交叉引用表的偏移地址,從而可以找到PDF文檔中所有的對象的相對地址,進而訪問對象。

%%EOF爲文件結束標誌。

圖片合併

那麼我們怎麼才能得到我們想要的合併的效果呢,這裏有個很土的方法。使用繪製圖片的方法,倒着遍歷圖片,發現像素不是 (255,255,255) 則記錄位置,講下一張圖片從這個偏移點開始繪製。最終得到的效果如下:

merge

圖 2-3 合併圖片

public static List<BufferedImage> mergeImage(List<BufferedImage> imgList, MergeImageOptions options)
      throws IOException {
  if (options == null) {
    options = new MergeImageOptions();
  }

  List<BufferedImage> ret = new ArrayList<BufferedImage>();
  BufferedImage mergeImg = null;
  int imgWidth = options.getWidth();
  int imgHeight = options.getHeight();
  int offset = 0;
  for (int p = 0; p < imgList.size(); p++) {
    BufferedImage bi = imgList.get(p);
    int width = bi.getWidth();
    int height = bi.getHeight();
    int minx = bi.getMinX();
    int miny = bi.getMinY();
    int realHeight = height;
    boolean done = false;
    for (int j = height - 1; j > miny; j--) {
      for (int i = minx; i < width; i++) {
        int pixel = bi.getRGB(i, j);
        int r = (pixel & 0xff0000) >> 16;
        int g = (pixel & 0xff00) >> 8;
        int b = (pixel & 0xff);
        if (r != 255 || g != 255 || b != 255) {
          realHeight = j;
          done = true;
          break;
        }
      }
      if (done) {
        break;
      }
    }
    if (p == 0) {
      imgWidth = Math.max(imgWidth, width);
      imgHeight = Math.max(imgHeight, height);
    }

    if (mergeImg == null || (offset + realHeight) > imgHeight) {
      mergeImg = new BufferedImage(imgWidth, imgHeight, BufferedImage.TYPE_INT_RGB);
      ret.add(mergeImg);
      offset = 0;
    }

    Graphics g = mergeImg.getGraphics();
    g.drawImage(bi, minx, offset, null);
    offset += realHeight;
    g.dispose();
  }
  return ret;
}

介紹幾個關於 PDF 的實用庫

ravenq

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章