相关文章推荐
淡定的匕首  ·  byte buddy - Why does ...·  1 年前    · 
忧郁的作业本  ·  oracle jdbc 时区-掘金·  1 年前    · 
奔放的松树  ·  how to redirect the ...·  1 年前    · 
飘逸的炒饭  ·  创作你的创作 - 简书·  1 年前    · 
跑龙套的大脸猫  ·  javascript - ...·  1 年前    · 

原文: 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);
      strcpy_s(szPrinter, printer.c_str());
  if(!OpenPrinterA(szPrinter, &hPrinter, NULL))
      return false;
  if(GetPrinterA(hPrinter, 2, &pdBuffer[0], cbBuf,&cbNeeded))
      ClosePrinter(hPrinter);
      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);
          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;
      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 文件都只含有一页,并且内容只有一行文字:

图 2-1 pdf1

图 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 对象在文档中的位置,以实现随机访问,它的形式是:

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
%%EOF

trailer 说明文件尾 trailer对象的开始。

/Size 8说明该PDF文件的对象数目。

/Root 1 0 R说明根对象的对象号为1。

Startxref

553说明交叉引用表的偏移地址,从而可以找到PDF文档中所有的对象的相对地址,进而访问对象。

%%EOF为文件结束标志。

那么我们怎么才能得到我们想要的合并的效果呢,这里有个很土的方法。使用绘制图片的方法,倒着遍历图片,发现像素不是 (255,255,255) 则记录位置,讲下一张图片从这个偏移点开始绘制。最终得到的效果如下:

图 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 的实用库

原文: http://www.aqcoder.com/post/content?id=42pdf(Portable Document Format 的简称,意为“便携式文档格式”),是由 Adobe Systems 用于与应用程序、操作系统、硬件无关的方式进行文件交换所发展出的文件格式。PDF 文件以 PostScript 语言图象模型为基础,无论在哪种打印机上都可保证精确的颜色和准确的打印效果...
C++库: 1,PDF类库 PoDoFo   http://podofo.sourceforge.net/  PoDoFo 是一个用来操作 PDF 文件格式的 C++ 类库。它还包含一些小工具用来解析、修改和创建 PDF 文 2,Xpdf    http://www.foolabs.com/xpdf/download.html   Xpdf是一个开放源代码的PDF档案浏览器,Xpdf 可解码...
文件包括课程源码和pdf教材,可直接打印学习。 这是 C/C++学习指南(语法篇) 对应的视频教材! 面向零基础的初学者。同时,书中还讲解了初级程序员所必须掌握的知识和技术,如“单步调试”,“编码规范”, ANSI函数库,文件操作,标准模板库STL的使用。 学习方法:看视频 -> 抄例子 -> 做题库 -> 看书复习, 反复迭代推进! (视频是主线,视频内容约占书中内容的70% )
cef-pdf cef-pdf是用于从HTML内容创建PDF文档的命令行实用程序(具有嵌入式HTTP服务器作为可选模式)。 它使用Google Chrome浏览器的库进行所有内部工作。 加载网址,呈现HTML和CSS页面以及进行PDF打印,因此,它可以生成完美,准确,优质的PDF文档。 cef-pdf [options] --url=<url>|--file=<path> [output] Options: --help -h This help screen. --url=<url> URL to load, may be http, file, data, anything supported by Chromium. --file=<path> File path to load using file:// scheme. May
由于近期项目开发需要,需要打印PDF文件。 前提需要下载软件Adobe Acrobat Reader DC。该打印方法是使用Adobe Acrobat Reader DC中提供的控件(OCX)来实现打印功能。 Adobe Acrobat Reader DC下载链接:https://get.adobe.com/cn/reader/ 安装成功之后即可开始创建Demo来实现功能。 本人使用的开发环境为 VS 2010。 首先要下载一个adobe reader 用adobe reader中的ocx来实现打印 下载地址自行百度 或者用本人的下载地址(adobe加源代码):https://download.csdn.net/download/immotal_xiaoqiang/11049468 开发环境联想E440笔记本+ vs2013 测试环境win7 64 首先新... 本次是利用ghostscript的api接口实现打印pdf文件的功能; 安装包和源码地址:https://github.com/ArtifexSoftware/ghostpdl-downloads/releases API文档:https://www.ghostscript.com/doc/9.27/API.htm 命令行参数说明:https://www.ghostscript.com/... 用户在后台上传pdf图册文件,前台可以进行pdf浏览,浏览方式为左右翻页模式(默认pdf是从上到下的),还有其他玩法,本质是花样看图(翻页电子书)。 后续又产生了付费需求:可以预览前5页,后面图册浏览需要付费查阅。 选型与过程 基于上述业务需求,我们简单进行需求拆解。 第一,pdf文件大小:需考量文件上传速度及下载速度;第二,浏览方式:需考量灵活性,图片化。 基于上述考量,以及交互方式,我们选定了第一种方案: 文件存储采用阿里云oss存储,前端服务直接跟oss存储交互,实 善良的YWJ: 博主我想问一下,我下载一些包,引入,比如mongoose,我下载在当前项目目录,引入的时候采用如下语句 const mongoose = require('mongoose'); 然后主表左键点击跳转,会跳转到C盘下的microsoft文件夹下的typesctipt文件夹里面的 mongoose文件夹 无法定位到当前目录下node_module文件夹下的 mongoose 这个有深恶设置方法么 vscode-goto-node-modules 一个快速定位 node 模块的 vscode 插件 darkhorse00000111: 非常有帮助,谢谢博主 markdown-it-vue 一个 Markdown 的 Vue 组件库 xiaoShengSanYue: 您好,我按照上面的示例写出来之后,只有预览,没有编辑的窗口显示.可以指点下应该怎么办吗