1. 程式人生 > >PDFBox列印PDF A4格式文件和定製規格條碼例項

PDFBox列印PDF A4格式文件和定製規格條碼例項

新接手一個列印終端的專案,要求可以列印A4格式的單據和 70mm * 40mm 規格的條碼。

整體流程可分兩種情況,

一種是將列印模板轉換為pdf文件二進位制陣列,進而生成為pdf文件,儲存到本地,然後再讀取到程式中,列印,最後刪除生成的pdf文件(不然隨著列印次數的增多,本地磁碟豈不爆滿);

另一種是省略儲存中間步驟,直接將列印模板轉換得到的pdf文件二進位制陣列用於程式列印。

顯然,第二種情況較為簡單,專案最後也是採用的這種。

先說列印A4的情況。

列印A4的情況比較簡單,本地有一臺惠普的A4列印,直接上程式碼。

package com.jiuqi.dna.gams.GXH.yndx.printing.util;

import java.awt.print.Book;
import java.awt.print.PageFormat;
import java.awt.print.Paper;
import java.awt.print.PrinterJob;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;

import javax.print.PrintService;

import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.printing.PDFPageable;
import org.apache.pdfbox.printing.PDFPrintable;
import org.apache.pdfbox.printing.Scaling;

import com.jacob.activeX.ActiveXComponent;
import com.jacob.com.Dispatch;
import com.jacob.com.Variant;

import net.sf.json.JSONObject;

/**
 * 自助列印終端列印工具類
 * @author wangjiao01
 *
 */
public class PDFPrintUtil {
	
	/**
	 * 獲取臨時生成的pdf檔案路徑
	 * @param pdfData
	 * @return
	 */
	public static String getNewPDFPath(byte[] pdfData) {
		
		DateFormat df = new SimpleDateFormat("yyyyMMddHHmmss");
		String newPdfName = df.format(new Date());
		String newPdfPath = "E:\\pdf\\" + newPdfName + ".pdf";// 隨具體環境變化
		
		OutputStream outputStream = null;
		try {
			outputStream = new FileOutputStream(newPdfPath);
			outputStream.write(pdfData);
		}catch(IOException e) {
			e.printStackTrace();
		}finally {
			if(outputStream != null) {
				try {
					outputStream.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
		
		return newPdfPath;
	}
	
	/**
	 * 執行列印
	 * @param pdfData pdf文件對應的二進位制陣列
	 * @param printerName 印表機標識
	 * @param copyCount 列印份數
	 * @return
	 * @throws IOException
	 */
	public static String doPrintByPDFBox(byte[] pdfData, String printerName, Integer copyCount) throws IOException {
		String result = null;
		PDDocument document = null;
		try {
			document = PDDocument.load(pdfData);
			PrinterJob printerJob = PrinterJob.getPrinterJob();
			
			// 查詢並設定印表機
			PrintService[] printServices = PrinterJob.lookupPrintServices();
			if(printServices == null || printServices.length == 0) {
				result = getPrintMessage(false, "列印失敗,計算機未安裝印表機,請檢查。");
//				makeSound("列印失敗,計算機未安裝印表機,請檢查。");
				return result;
			}
			PrintService printService = null;
			for(int i = 0; i < printServices.length; i++) {
				if(printServices[i].getName().equalsIgnoreCase(printerName)) {
					System.out.println(printServices[i].getName());
					printService = printServices[i];
					break;
				}
			}
			if(printService != null) {
				printerJob.setPrintService(printService);
			} else {
				result = getPrintMessage(false, "列印失敗,未找到名稱為" + printerName + "的印表機,請檢查。");
//				makeSound("列印失敗,未找到名稱為" + printerName + "的印表機,請檢查。");
				return result;
			}
			
			// 設定紙張
			PDFPrintable pdfPrintable = new PDFPrintable(document, Scaling.ACTUAL_SIZE);
			PageFormat pageFormat = new PageFormat();
			pageFormat.setOrientation(PageFormat.PORTRAIT);
			pageFormat.setPaper(getPaper(printerName));
			// Book 的方式實現列印多張(已測試,可行)
			Book book = new Book();
			book.append(pdfPrintable, pageFormat, document.getNumberOfPages());
			printerJob.setPageable(book);
			// PDFPageable 的方式實現列印多張(未測試,應該也可行)
//			PDFPageable pdfPageable = new PDFPageable(document);
//			pdfPageable.append(pdfPrintable, pageFormat, document.getNumberOfPages());
//			printerJob.setPageable(pdfPageable);
			
			// 測試
			System.out.println(document.getNumberOfPages());
			System.out.println(book.getNumberOfPages());
//			System.out.println(pdfPageable.getNumberOfPages());
			
			// 執行列印
			printerJob.setCopies(copyCount);
			printerJob.print();
			result = getPrintMessage(true, "列印成功。");
//			makeSound("列印成功,請取件。");
		} catch (Exception e) {
			e.printStackTrace();
			result = getPrintMessage(false, "列印失敗:發生異常。");
//			makeSound("列印失敗,列印時發生異常,請檢查。");
		} finally {
			if(document != null) {
				document.close();// 起初檔案刪除失敗,關閉文件之後,刪除成功
			}
		}
		
		return result;
	}

	/**
	 * 獲取列印結果資訊,成功或失敗,用以返回前臺介面
	 * @param isPrintSuccess
	 * @param message
	 * @return
	 */
	public static String getPrintMessage(boolean isPrintSuccess, String message) {
		JSONObject object = new JSONObject();
		if(isPrintSuccess) {
			object.put("code", 1);
		}else {
			object.put("code", 0);
		}
		object.put("message", message);
		System.out.println(message);
		return object.toString();
	}
	
	/**
	 * 刪除列印過程中建立的臨時pdf檔案
	 * @param newPdfPath
	 * @return
	 */
	public static boolean deleteFile(String newPdfPath) {
		File file = new File(newPdfPath);
		if(file.exists()) {
			if(file.isFile()) {
				return file.delete();
			}
		}else {
			System.out.println("檔案 " + newPdfPath + " 不存在!");
		}
		return false;
	}
	
	/**
	 * 列印語音提示:成功或失敗,並提示失敗原因
	 * @param message
	 */
	public static void makeSound(String message) {
		ActiveXComponent sap = new ActiveXComponent("Sapi.SpVoice");
		try {
			// 音量 0-100
			sap.setProperty("Volume", new Variant(100));
			// 語音朗讀速度 -10 到 +10
			sap.setProperty("Rate", new Variant(0));
			// 獲取執行物件
			Dispatch sapo = sap.getObject();
			// 執行朗讀
			Dispatch.call(sapo, "Speak", new Variant(message));
			// 關閉執行物件
			sapo.safeRelease();
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			// 關閉應用程式連線
			sap.safeRelease();
		}
	}
	
	/**
	 * 根據印表機名稱判斷是單據列印還是條碼列印,進而建立對應Paper物件並返回
	 * @param printerName
	 * @return
	 */
	public static Paper getPaper(String printerName) {
		Paper paper = new Paper();
		// 預設為A4紙張,對應畫素寬和高分別為 595, 848
		int width = 595;
		int height = 848;
		// 設定邊距,單位是畫素,10mm邊距,對應 28px
		int marginLeft = 10;
		int marginRight = 0;
		int marginTop = 10;
		int marginBottom = 0;
		if(printerName.contains("bar")) {
			// 雲南大學條碼紙張規格70mm寬*40mm高,對應畫素值為 198, 113
			width = 198;
			height = 113;
		}
		paper.setSize(width, height);
		// 下面一行程式碼,解決了列印內容為空的問題
		paper.setImageableArea(marginLeft, marginRight, width - (marginLeft + marginRight), height - (marginTop + marginBottom));
		return paper;
	}
	

}

經過探索與研究,對PDFBox的一些類有了一定的認識,現記錄如下,以便大家今後使用。

1、PDDocument,對應一個實際的pdf文件,有好多個過載的靜態load方法,用於將實際pdf文件建立到記憶體中。

getNumberOfPages方法可獲取文件的總頁數。

2、PDFPrintable,對應的API類註釋為 Prints pages from a PDF document using any page size or scaling mode.

實際使用舉例:

PDFPrintable pdfPrintable = new PDFPrintable(document, Scaling.ACTUAL_SIZE);

經常用以作為Book或Pageable類的方法引數。

3、PageFormat,API對應的類註釋為 The <code>PageFormat</code> class describes the size and orientation of a page to be printed. 用於格式化列印規格,如設定列印方向(橫向和縱向)、列印紙張等。

實際使用舉例:

pageFormat.setOrientation(PageFormat.PORTRAIT);// 設定列印方向為縱向。PageFormat.PORTRAIT表示縱向,PageFormat.LANDSCAPE表示橫向
pageFormat.setPaper(getPaper(printerName));// 設定紙張規格

具體獲取紙張方法程式碼如下:

/**
	 * 根據印表機名稱判斷是單據列印還是條碼列印,進而建立對應Paper物件並返回
	 * @param printerName
	 * @return
	 */
	public static Paper getPaper(String printerName) {
		Paper paper = new Paper();
		// 預設為A4紙張,對應畫素寬和高分別為 595, 848
		int width = 595;
		int height = 848;
		// 設定邊距,單位是畫素,10mm邊距,對應 28px
		int marginLeft = 10;
		int marginRight = 0;
		int marginTop = 10;
		int marginBottom = 0;
		if(printerName.contains("bar")) {
			// 雲南大學條碼紙張規格70mm寬*40mm高,對應畫素值為 198, 113
			width = 198;
			height = 113;
		}
		paper.setSize(width, height);
		// 下面一行程式碼,解決了列印內容為空的問題
		paper.setImageableArea(marginLeft, marginRight, width - (marginLeft + marginRight), height - (marginTop + marginBottom));
		return paper;
	}

請務必注意呼叫Paper物件的setImageableArea方法,否則你將很驚喜地看到列印內容一片空白的現象。

4、Book,顧名思義,書,可以看做一個有多頁的冊子,每一頁都可以有自己不同的紙張格式。PDFPrintable就是用於它的

append方法的第一個引數。append方法原始碼如下:

/**
     * Appends <code>numPages</code> pages to the end of this
     * <code>Book</code>.  Each of the pages is associated with
     * <code>page</code>.
     * @param painter   the <code>Printable</code> instance that renders
     *                  the page
     * @param page      the size and orientation of the page
     * @param numPages  the number of pages to be added to the
     *                  this <code>Book</code>.
     * @throws NullPointerException
     *          If the <code>painter</code> or <code>page</code>
     *          argument is <code>null</code>
     */
    public void append(Printable painter, PageFormat page, int numPages) {
        BookPage bookPage = new BookPage(painter, page);
        int pageIndex = mPages.size();
        int newSize = pageIndex + numPages;

        mPages.setSize(newSize);
        for(int i = pageIndex; i < newSize; i++){
            mPages.setElementAt(bookPage, i);
        }
    }

book.append(pdfPrintable, pageFormat, document.getNumberOfPages()) 可實現列印多張,倘若用的它的沒有第三個引數過載方法public void append(Printable painter, PageFormat page),那麼你將驚喜地看到明明選擇了三張,卻只打印了一張的現象。

倘若程式碼裡未曾設定紙張格式,則可能會看到內容截斷、跨兩頁紙、紙張橫向內容縱向等奇怪現象。

因為java預設的列印,會從印表機紙張裡尋找相近的紙張進行匹配,如果沒有新增自定義紙張,可能找出來的是別的紙張,而且,java讀取紙張有個限制, 那就是預設紙張 高度 >= 寬度,高度 < 寬度的紙張是讀取不到的。

原因在原始碼裡,請看,

/**
     * {@inheritDoc}
     * 
     * Returns the actual physical size of the pages in the PDF file. May not fit the local printer.
     */
    @Override
    public PageFormat getPageFormat(int pageIndex)
    {
        PDPage page = document.getPage(pageIndex);
        PDRectangle mediaBox = PDFPrintable.getRotatedMediaBox(page);
        PDRectangle cropBox = PDFPrintable.getRotatedCropBox(page);
        
        // Java does not seem to understand landscape paper sizes, i.e. where width > height, it
        // always crops the imageable area as if the page were in portrait. I suspect that this is
        // a JDK bug but it might be by design, see PDFBOX-2922.
        //
        // As a workaround, we normalise all Page(s) to be portrait, then flag them as landscape in
        // the PageFormat.
        Paper paper;
        boolean isLandscape;
        if (mediaBox.getWidth() > mediaBox.getHeight())
        {
            // rotate
            paper = new Paper();
            paper.setSize(mediaBox.getHeight(), mediaBox.getWidth());
            paper.setImageableArea(cropBox.getLowerLeftY(), cropBox.getLowerLeftX(),
                    cropBox.getHeight(), cropBox.getWidth());
            isLandscape = true;
        }
        else
        {
            paper = new Paper();
            paper.setSize(mediaBox.getWidth(), mediaBox.getHeight());
            paper.setImageableArea(cropBox.getLowerLeftX(), cropBox.getLowerLeftY(),
                    cropBox.getWidth(), cropBox.getHeight());
            isLandscape = false;
        }

        PageFormat format = new PageFormat();
        format.setPaper(paper);
        
        // auto portrait/landscape
        switch (orientation)
        {
            case AUTO:
                format.setOrientation(isLandscape ? PageFormat.LANDSCAPE : PageFormat.PORTRAIT);
                break;
            case LANDSCAPE:
                format.setOrientation(PageFormat.LANDSCAPE);
                break;
            case PORTRAIT:
                format.setOrientation(PageFormat.PORTRAIT);
                break;
            default:
                break;
        }
        
        return format;
    }

這是PDFPageable重寫的Book的方法,獲取紙張格式,該方法在紙張寬度大於高度的時候進行了調換,將寬度給了高度,高度給了寬度,相當於順時針或逆時針旋轉了90度。我列印條碼的時候就遇到了這個問題,條碼規格是寬70毫米,高40毫米,印表機紙張是橫向的,但是內容總是縱著出來,無論怎麼設定,列印模板橫縱切換,還是印表機列印方向橫縱切換,都沒有效果,就是這段程式碼搞的鬼。這是我忽略了設定紙張格式導致的,請大家今後務必注意。

當然,僅在程式碼裡設定紙張格式是不夠的,印表機裡一定要確實有這種紙張格式才行。否則,印表機又會智慧地尋找最相近的紙張來忽悠你了。

解決問題過程中,有參考如下部落格文件,寫的很好,推薦下。