二奢倉店的靜默打印代理實現
1 二奢倉店面臨的問題
1.1 什么是二奢倉店
1.2 有什么問題
2 調用打印機的限制
2.1 js的局限
2.2 想要靜默打印
3 做一個定制化的打印代理
3.1 java調起windows打印機
3.2 我該怎么生成打印數據
3.3 多場景的自助代理打印
3.4 可執行文件的獨立部署
4 打印偏移的通用解決方案
5 總結
1.二奢倉店面臨的問題
1.1 什么是二奢倉店
二奢倉店是轉轉新開業的一家線下實體店,主營二手奢侈品和其他品類,以倉店的形式營業。
所謂倉店,即是作為一個店鋪,提供商品和收銀臺等常規線下店鋪的購物功能,用戶可以正常進店購物。同時作為倉庫,管理進出庫,陳列有大量貨物,一方面用于存儲,另一方面也方便用戶挑選。
1.2 有什么問題
作為一個同時有店鋪屬性和倉庫屬性的實體店,同時面對著門店的商品管理和收銀問題,也存在倉庫的貨物管理和收發貨問題。在這種場景下,很多環節都會需要靠打印各種標記來區分和管理實物。同時由于可能存在一臺電腦會參與多個場景,對打印管理也有一定要求。主要是如下的幾個條件:
- 需要打印的內容為以下幾類:收銀小票、商品吊牌、不干膠吊牌、配件簽,再根據商品的分類和貨源區分
- 現場操作人員統一使用windows系統進行操作,用到的操作后臺都是網頁,調起打印機進行打印的觸發也是通過網頁上的按鈕
- 為了減少操作人員的不必要操作,打印過程盡可能的簡單
- 一臺電腦可能連接著多個打印機,需要根據打印場景自動選擇使用哪個打印機
2.調用打印機的限制
2.1 js的局限
由于操作后臺都是網頁,這里首先想到的是通過網頁js來調用windows打印機進行打印。一般是通過js調用window.print()來打印,而window.print()打印的是整個網頁的內容,實際需要打印的內容僅僅是條碼或收銀數據,所以可以創建一個臨時的iframe,在iframe內創建打印內容,再調用window.print()來打印。
但不論是否創建iframe來打印,這種做法都會有一個問題,就是會觸發一個windows的打印預覽彈窗,而在倉店的實際操作中這個彈窗是沒有必要的,所以這屬于需要解決的問題,即需要實現靜默打印。
2.2 想要靜默打印
網上對于js實現靜默打印的辦法一般有兩種:
第一種是在chrome下可以通過修改chrome://flags下的參數來實現,但是操作復雜,而且不同版本的支持情況可能也不同,屬于不穩定的解決方法,不適合放在線下由店員來操作。
第二種辦法是使用一個代理程序,將js調起打印的操作轉化為js通知代理程序,代理程序調用打印機,這樣即可繞過瀏覽器和windows的一般流程,實現不彈預覽窗口的靜默打印。
這個打印代理程序在網上能搜到一些,但是對于轉轉二奢倉店也存在一些特例場景不一定能滿足的情況,而且這個代理程序實現起來也不難,所以還是自己做一個方便定制化。
3.做一個定制化的打印代理
3.1 java調起windows打印機
在java中調起Windows打印機,可以通過java的打印服務API實現。首先需要引入javax.print包,然后通過PrintServiceLookup查找可用的打印服務。對于Windows系統,可以根據名稱選擇特定的打印機。
以windows10為例,“打印機和掃描儀”里的打印機名稱就是java程序能識別到的服務名稱
以windows10為例,“打印機和掃描儀”里的打印機名稱就是java程序能識別到的服務名稱
示例代碼如下:
private PrintService findPrintService(String name) {
// 查找全部的打印服務
PrintService targetPrintService = null;
PrintService[] allPrintServices = PrintServiceLookup.lookupPrintServices(null, null);
if (null != allPrintServices) {
for (PrintService printService : allPrintServices) {
if (printService.getName().equals(name)) {
targetPrintService = printService;
break;
}
}
}
if (null == targetPrintService) {
if (null != allPrintServices && allPrintServices.length > 0) {
List<String> allPrintServiceNameList = Arrays.stream(allPrintServices)
// 過濾掉window自帶的打印服務
.filter(s -> !s.getName().contains("OneNote") && !s.getName().contains("Microsoft"))
.map(PrintService::getName)
.collect(Collectors.toList());
log.info("沒有找到目標打印機, 目標打印機:{}, 全部打印機:{}", name, GsonUtil.toJson(allPrintServiceNameList));
} else {
log.info("沒有找到目標打印機, 目標打印機:{}", name);
}
returnnull;
}
return targetPrintService;
}通過這種方式,可以靈活控制當前需要使用的打印機,如果同時連接了多個同一型號的打印機,可以靠修改windows中的打印機名稱來做區分。同樣的,如果更換了其他型號的打印機但是打印內容沒有變化,可以靠修改打印機名稱來匹配原有邏輯進行打印。
3.2 我該怎么生成打印數據
倉店打印的場景中,存在多種需要打印的內容,例如收銀小票、商品吊牌、配件簽等等。對于不同的打印內容,需要考慮對應的打印數據獲取方式和打印方式。 一般而言可以分為幾種方式: 一種是使用模板文件,提前將模板畫好,在實際打印時傳入需要打印的數據。這種做法適合長期不變的和比較復雜的內容,例如快遞面單等。
以JasperSoft為例,創建的打印模板
另一種是利用java的java.awt.print.Book類來控制打印文字和坐標,交由打印機進行打印。這種做法適合樣式簡單文字量大的內容,例如收銀小票等。
Book book = new Book();
book.append((graphics, pageFormat, pageIndex) -> {
try {
// 繪制一段字符串
graphics.setFont(new Font("Default", Font.BOLD, 12));
graphics.drawString("十二號字測試行", 0, 30);
graphics.setFont(new Font("Default", Font.BOLD, 14));
graphics.drawString("十四號字測試行", 0, 60);
graphics.setFont(new Font("Default", Font.BOLD, 10));
graphics.drawString("十號字測試行", 0, 90);
graphics.setFont(new Font("Default", Font.BOLD, 8));
graphics.drawString("八號字測試行", 0, 120);
graphics.setFont(new Font("Default", Font.BOLD, 6));
graphics.drawString("六號字測試行", 0, 150);
// 繪制一個條線條
graphics.drawLine(20, 155, 185, 155);
} catch (Exception e) {
log.error("繪制打印內容異常", e);
}
return PAGE_EXISTS;
}, pf);還有一種做法是把需要打印的內容生成圖片,調打印機的時候直接打印圖片。這種做法相對靈活,但是對打印機的打印精度有要求,精度太低的在打印復雜圖片的時候可能會丟失細節導致打印內容不清晰,尤其是涉及到文字相關的時候。
public PageFormat getPageFormat(int imageHeight, int imageWidth) {
// 傳入圖片的長寬比
double aspectRatio = getAspectRatio(imageHeight, imageWidth);
Paper paper = new Paper();
// 打印紙的頁面大小
paper.setSize(130.4D, 225.4D);
// 可打印區域大小
int areaWidth = 91 - 6;
// 設置打印坐標,固定寬度,根據長寬比計算打印長度
paper.setImageableArea(17D + 7, 168D + 2, areaWidth, areaWidth * aspectRatio);
PageFormat pageFormat = new PageFormat();
pageFormat.setPaper(paper);
pageFormat.setOrientation(PageFormat.PORTRAIT);
return pageFormat;
}倉店打印最后選擇的傳入圖片打印的方式。之所以選擇傳圖打印,主要有以下幾點考慮:
第一是打印內容多以條碼為主,這種情況下傳圖比較方便。
第二是更改需要打印的內容時不需要更新打印程序,只需要調整傳入的圖片即可。這樣來說對于倉店打印這種通過網頁操作的場景,可以在不需要實際操作人員介入的情況下完成更新,免去更新打印程序的步驟,更靈活,也減少實際操作人員的工作。
3.3 多場景的自助代理打印
為了能夠簡單的和操作后臺對接,這個打印代理程序使用web的形式實現,即在倉店操作人員的電腦上啟動一個web服務器,由操作后臺的頁面js調用http接口的形式來傳入打印圖片觸發打印。
所以,首先需要創建web程序。java創建web程序很簡單,這里選用spring boot創建一個僅有單個接口的web程序,這個接口用來接收傳入的打印圖片和場景
@RequestMapping("/print")
@ResponseBody
public WebResponse<Void> print(@RequestParam("type") String type, @RequestParam("file") MultipartFile file)不同的打印場景對應不同的打印參數配置
// 注入不同的打印服務類,key為接口入參的type
@Resource
private Map<String, IPrintService> printServiceMap;
打印服務類實現IPrintService接口,各自處理對應場景的打印參數
觸發打印的流程放到AbstractPrintService里統一處理
@Override
public boolean print(InputStream inputStream) {
PrintService targetPrintService = findPrintService(getName());
if (null == targetPrintService) {
returnfalse;
}
BufferedImage image = null;
try {
ImageIO.setUseCache(false);
// 轉換為圖片類
image = ImageIO.read(inputStream);
if (null == image) {
log.error("打印錯誤,無法獲取需要打印的內容,打印機={}", getName());
returnfalse;
}
// 圖片尺寸
int oriHeight = image.getHeight();
int oriWidth = image.getWidth();
log.debug("name={} oriHeight={} oriWidth={}", getName(), oriHeight, oriWidth);
// 創建打印任務
PrinterJob job = PrinterJob.getPrinterJob();
job.setPrintService(targetPrintService);
// 設置打印頁面配置
PageFormat pageFormat = getPageFormat(oriHeight, oriWidth);
// 縮放比例
double scale = getScale(pageFormat, oriHeight, oriWidth);
// 設置打印內容
job.setPrintable(new ImagePrintable(image, scale), pageFormat);
PrintRequestAttributeSet attributes = new HashPrintRequestAttributeSet();
// 打印1份
attributes.add(new Copies(1));
// 發送打印任務
job.print(attributes);
log.info("打印任務已發送, 打印機={}", getName());
} catch (Exception e) {
log.error("打印過程異常, 打印機={}", getName(), e);
returnfalse;
} finally {
if (null != image) {
image.flush();
}
System.gc();
}
returntrue;
}3.4 可執行文件的獨立部署
為了便于在倉店的Windows電腦上運行打印代理程序,我們選擇將java程序打包為一個獨立的可執行文件,來簡化倉店操作人員的操作。
將java代碼打包成exe文件有多種方式,倉店選擇的是Launch4j,Launch4j可以在代碼編譯時生成exe文件,免去額外操作的步驟。
首先這個打印程序畢竟是個基于java的程序,運行時需要依賴很多依賴包,為了簡化部署的復雜度,需要添加assembly的編譯插件,用來將所有的依賴包打成一個肥包,來保證單文件可運行:
<!-- 編譯時把依賴的jar全都打包到一個jar文件內的插件,用來保證編譯出的exe文件可以獨立運行 -->
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
<id>assembly</id>
<phase>package</phase>
<goals><goal>single</goal></goals>
<configuration>
<descriptors>
<descriptor>src/main/assembly/assembly.xml</descriptor>
</descriptors>
</configuration>
</execution>
</executions>
</plugin>然后添加launch4j的編譯插件,給編譯生成的肥包加exe的外殼:
<!-- Launch4j插件,用來生成exe文件 -->
<plugin>
<groupId>com.akathist.maven.plugins.launch4j</groupId>
<artifactId>launch4j-maven-plugin</artifactId>
<version>${launch4j-maven-plugin.version}</version>
<executions>
<execution>
<id>l4j-clui</id>
<phase>package</phase>
<goals>
<goal>launch4j</goal>
</goals>
<configuration>
<!-- 控制生成的exe文件是命令行顯示還是GUI界面顯示,GUI的話需要自己做一個界面 -->
<headerType>console</headerType>
<!-- 生成的exe需要包含的jar -->
<jar>
${project.build.directory}/${project.build.finalName}.${project.packaging}
</jar>
<!-- 生成的exe的輸出位置 -->
<outfile>${project.build.directory}/printWebserver_1.0.exe</outfile>
<!-- exe程序的圖標,會顯示在例如窗口、任務管理器等地方 -->
<icon>src/main/resources/image/zzPrinter.ico</icon>
<!-- 控制程序以單實例的方式運行 -->
<singleInstance>
<mutexName>warestorePrintWebserver</mutexName>
</singleInstance>
<!-- 啟動的入口類 -->
<classPath>
<mainClass>org.springframework.boot.loader.JarLauncher</mainClass>
<addDependencies>true</addDependencies>
<jarLocation>lib/</jarLocation>
</classPath>
<!-- 執行環境的一些控制參數 -->
<jre>
<minVersion>1.8.0</minVersion>
<initialHeapSize>1024</initialHeapSize>
<maxHeapSize>2048</maxHeapSize>
</jre>
<!-- exe文件信息的一些控制參數,在windows右鍵-屬性-詳細信息里顯示 -->
<versionInfo>
<fileVersion>1.0.0.0</fileVersion>
<txtFileVersion>1.0.0.0</txtFileVersion>
<fileDescription>zhuanzhuan warestore universal printing program</fileDescription>
<copyright>zz_warestore</copyright>
<productVersion>1.0.0.0</productVersion>
<txtProductVersion>1.0.0.0</txtProductVersion>
<productName>printWebserver</productName>
<internalName>printWebserver</internalName>
<originalFilename>printWebserver.exe</originalFilename>
</versionInfo>
</configuration>
</execution>
</executions>
</plugin>配置正常的話,編譯完成之后,在target目錄下會有一個生成的exe文件
編譯生成的exe文件
運行配置文件,程序啟動后會開啟web服務,通過js調用本地的web服務端口即可實現靜默打印
代理打印程序運行畫面
4 打印偏移的通用解決方案
一般而言收銀小票、紙質吊牌、標簽紙這種打印紙都是以卷狀銷售的,對應的打印機也是按照裝入打印紙卷設計的。在實際應用的場景中,難免會因為各種各樣的原因導致出現與預期不一致的情況,例如:
- 紙質吊牌是定制的,出廠有預印刷的圖案在上面,而這個圖案因為公差等等原因在不同批次上會有一定的偏差,導致按照既定打印參數打印出來的內容位置和預期不一致
- 一卷紙打完了換另外一卷,但是因為公差、裝入的錯誤、更換了供貨品牌等等原因導致存在尺寸差異,會導致打印出來的內容位置偏移
這種情況輕度可能僅僅是看起來觀感差一點,嚴重的可能會影響實際效果。例如倉店吊牌打印的是條碼,如果歪了可能導致條碼與預留的打印區域邊框過緊,無法正常的掃描等等。
正常打印的效果,條碼清晰,與邊距有一定距離便于識別
打印偏移的效果,示例圖,打印內容與邊緣貼近無法識別
針對這種情況,一般功能比較完善的打印機驅動會提供微調的功能
打印驅動支持微調
但是不是所有的打印驅動都有這個功能,而且不同品牌的打印機因為驅動不同,修改偏移量的位置也可能不同。為了避免實操人員需要記住各種不同品牌的打印機的調整方法,同時能夠讓微調簡單且通用,倉店用了一個簡易的辦法來實現這個功能:
首先在電腦的固定位置創建一個固定名稱的txt文件,里面填入橫縱坐標的偏移量,作為偏移文件
偏移文件,簡單直觀的控制偏移量
代碼增加一個工具類,在程序啟動時從偏移文件中讀取橫縱的偏移量,將偏移量加到創建打印區域時的參數上,從而實現可以簡單直觀的控制打印位置的功能
@Getter
privatestatic LocalOffsetEntity localOffsetEntity = new LocalOffsetEntity(0.0, 0.0);
// 啟動時加載偏移文件的內容
static {
try {
// 獲取桌面路徑
String desktopPath = System.getProperty("user.home") + "/Desktop";
// 對于Windows系統需要轉義反斜杠
if (System.getProperty("os.name").toLowerCase().contains("win")) {
desktopPath = System.getProperty("user.home") + "\\Desktop";
}
File filePath = new File(desktopPath, "\\打印機偏移參數.txt");
// 讀取文件內容
final String content = FileUtils.readFileToString(filePath, StandardCharsets.UTF_8);
log.info("本地偏移文件內容:" + content);
final String[] offsetArr = content.split(",");
if (offsetArr.length > 1) {
Double xOffset = Double.valueOf(offsetArr[0]);
Double yOffset = Double.valueOf(offsetArr[1]);
localOffsetEntity.setxOffset(xOffset);
localOffsetEntity.setyOffset(yOffset);
}
} catch (IOException e) {
System.err.println("讀取文件失敗:");
}
}// 在生成打印區域的時候把偏移量加進去,來實現調整打印位置的效果
final CacheUtil.LocalOffsetEntity localOffsetEntity = CacheUtil.getLocalOffsetEntity();
paper.setImageableArea(14.2D + localOffsetEntity.getxOffset(), 121.9D + localOffsetEntity.getyOffset(), areaWidth, areaWidth * aspectRatio);5.總結
這個代理打印程序的實現整體功能不復雜,在設計過程主要需要考慮易用性和穩定性。
- 倉店場景因為操作人員使用的后臺都是網頁形式,所以使用web服務來實現代理,簡化和前端交互的邏輯。
- 操作人員需要長時間面對各種操作場景,需要盡量減少的操作步驟和預期外問題
- 應對不可控的場景盡量提供簡單易操作的解決方案
程序員中普遍流傳著一句話,“脫離了業務的程序都是耍流氓”,從實際場景出發,盡可能的符合需求,是程序設計的首要目標。
關于作者:項贏,轉轉java開發工程師























