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毫米,印表機紙張是橫向的,但是內容總是縱著出來,無論怎麼設定,列印模板橫縱切換,還是印表機列印方向橫縱切換,都沒有效果,就是這段程式碼搞的鬼。這是我忽略了設定紙張格式導致的,請大家今後務必注意。
當然,僅在程式碼裡設定紙張格式是不夠的,印表機裡一定要確實有這種紙張格式才行。否則,印表機又會智慧地尋找最相近的紙張來忽悠你了。
解決問題過程中,有參考如下部落格文件,寫的很好,推薦下。