Java实现PDF条码与二维码生成:ZXing与PDFBox实战指南 1. 从一个真实需求说起为什么要在PDF里塞条码做过仓储、物流、票务或者资产管理系统的人大概率都碰到过这种需求后端生成一批单据每张单据上要带一个条码或者二维码扫码枪一扫就能定位到对应记录然后整批单据打包成PDF给前端下载或者直接预览。听起来简单但真动手做的时候坑一个接一个。最常见的场景是这样的某公司做了一套出入库系统每件货物入库时生成一个唯一条码贴在货物外包装上。仓管员用扫码枪扫一下系统就能调出这件货的全部信息。单据需要打印出来存档所以后端要把条码画到PDF上前端点一下按钮就能下载或者在线看。这个需求本身不复杂但涉及三个技术环节的衔接条码/二维码的生成、PDF的绘制、以及前端如何拿到并展示这个PDF。我见过不少项目在这三步里翻车。有人条码生成了但分辨率太低打印出来扫不出来有人PDF里图片位置算错了条码跑到页面外面去了还有人前端拿到PDF之后不知道怎么展示直接让浏览器下载了事用户体验很差。这篇文章就把这三个环节拆开揉碎讲清楚从选型到代码到踩坑尽量让看完的人能直接抄作业。适合谁看后端开发为主尤其是做Java Web的对PDF操作和条码生成不太熟但又有实际需求的人。前端部分也会涉及但不会太深主要是讲怎么把后端返回的PDF流展示出来。如果你正在做类似的功能或者准备做这篇应该能帮你省不少时间。2. 条码和二维码的生成选对工具比写对代码更重要2.1 为什么我最终选了ZXing而不是Barcode4JJava生态里生成条码和二维码的库主要有两个ZXingZebra Crossing和Barcode4J。两个我都用过说下实际感受。Barcode4J是老牌的一维条码生成库支持Code128、EAN、UPC等常见格式输出格式也丰富可以直接输出SVG、PNG、JPEG。它的优点是API比较直观配置灵活尤其是一维条码的样式控制很细。但缺点也很明显二维码支持基本没有而且项目更新频率很低遇到JDK版本升级偶尔会有兼容性问题。ZXing是Google开源的支持一维条码和二维码格式覆盖非常全。Code128、EAN-13、QR Code、Data Matrix、PDF417都支持。社区活跃Maven中央仓库直接拉版本更新也勤快。最关键的是它的API设计对“生成图片”这个场景非常友好几行代码就能出图。所以我的选型结论很明确如果只需要一维条码Barcode4J可以考虑但只要涉及二维码或者想一个库搞定所有格式直接上ZXing。实际项目里我基本都用ZXing省得引入两个库增加复杂度。Maven依赖如下dependency groupIdcom.google.zxing/groupId artifactIdcore/artifactId version3.5.2/version /dependency dependency groupIdcom.google.zxing/groupId artifactIdjavase/artifactId version3.5.2/version /dependencycore是核心生成逻辑javase提供了BufferedImage相关的工具类两个都要引。2.2 生成二维码的核心参数怎么调ZXing生成二维码的代码本身很短但参数没调好出来的图要么扫不出来要么打印模糊。下面这段是我在实际项目里反复调整后稳定使用的配置public static BufferedImage generateQRCode(String content, int width, int height) throws WriterException { MapEncodeHintType, Object hints new HashMap(); // 纠错级别L(7%) M(15%) Q(25%) H(30%) hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H); // 字符编码 hints.put(EncodeHintType.CHARACTER_SET, UTF-8); // 边距单位是像素默认4 hints.put(EncodeHintType.MARGIN, 1); // 版本不指定则自动选择 // hints.put(EncodeHintType.QR_VERSION, 7); QRCodeWriter writer new QRCodeWriter(); BitMatrix matrix writer.encode(content, BarcodeFormat.QR_CODE, width, height, hints); BufferedImage image new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); for (int x 0; x width; x) { for (int y 0; y height; y) { image.setRGB(x, y, matrix.get(x, y) ? 0xFF000000 : 0xFFFFFFFF); } } return image; }几个关键点解释一下。纠错级别二维码的纠错能力分L、M、Q、H四档分别对应7%、15%、25%、30%的容错。级别越高二维码越密但被遮挡或磨损后越容易扫出来。实际项目里我一般用H因为单据打印出来可能会有折痕、污渍H级别容错能力强扫不出来概率低。代价是同样内容下二维码尺寸会大一些但一般不影响。边距MARGINZXing默认边距是4也就是二维码四周留4个模块宽度的白边。这个白边很重要扫码时需要有足够的静区才能识别。但默认4有时候显得太宽尤其是PDF里空间有限的时候。我一般设成1或者2实测下来只要不是紧贴着其他黑色元素扫码没问题。但如果你把边距设成0扫码失败率会明显上升这个坑我踩过。尺寸width和height不要设太小。二维码最小建议不低于200x200像素打印到PDF上如果尺寸是2cm x 2cm按300dpi算需要约236像素所以200是底线。我一般生成300x300或者400x400然后在PDF里按实际尺寸缩放这样打印出来清晰度足够。2.3 一维条码Code128的生成细节一维条码在单据上更常见因为占空间小适合贴在窄边或者标签上。ZXing生成Code128的代码如下public static BufferedImage generateBarcode(String content, int width, int height) throws WriterException { MapEncodeHintType, Object hints new HashMap(); hints.put(EncodeHintType.CHARACTER_SET, UTF-8); hints.put(EncodeHintType.MARGIN, 1); Code128Writer writer new Code128Writer(); BitMatrix matrix writer.encode(content, BarcodeFormat.CODE_128, width, height, hints); BufferedImage image new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); for (int x 0; x width; x) { for (int y 0; y height; y) { image.setRGB(x, y, matrix.get(x, y) ? 0xFF000000 : 0xFFFFFFFF); } } return image; }Code128有几个需要注意的地方。内容长度Code128能编码的字符集很广数字、字母、常用符号都行。但内容太长的话条码会变得很宽打印到固定宽度的标签上可能放不下。一般单据上的条码内容控制在20个字符以内比较合适比如订单号、货物编号这种。宽高比一维条码的宽高比很重要。太扁了扫码枪不好扫太高了浪费空间。我一般用3:1到4:1的比例比如宽300像素、高80像素。如果是打印在A4纸上条码宽度建议不低于4cm高度不低于1cm。条码下方的文字ZXing生成的图片只有条码本身没有下方的人类可读文字。实际单据上通常需要在条码下方显示编号方便人工核对。这个文字我一般不在条码图片里生成而是在PDF绘制的时候单独画文字这样字体和排版更可控。2.4 生成图片后的内存管理这里有个容易被忽略的问题BufferedImage是占内存的。如果一批单据有几百上千张每张都生成条码图片内存会涨得很快。我的做法是生成完图片后在绘制到PDF之后及时释放引用不要缓存这些图片对象。如果并发量高可以考虑用对象池或者限制单次处理的单据数量。另外如果条码内容是固定的比如同一批货物用同一个条码可以缓存生成的BufferedImage避免重复生成。但缓存要设上限不然就是内存泄漏。3. PDF绘制iText和PDFBox怎么选图片怎么摆3.1 iText 5 vs iText 7 vs PDFBox我的实际选择Java操作PDF的库主要有三个iText 5、iText 7、Apache PDFBox。iText 5是老版本API简单网上资料多但License是AGPL商业项目用需要买授权。iText 7是重写版本功能更强API更现代但学习曲线陡一些License同样是AGPL。PDFBox是Apache的License是Apache 2.0商业友好但API相对底层画图、排版都要自己算坐标。我的选择逻辑是这样的如果是商业项目且不想买License直接用PDFBox。虽然API底层一点但画条码图片这个场景其实不复杂就是往指定坐标画图片和文字PDFBox完全够用。如果是内部项目或者个人项目iText 5用起来最顺手资料也多。下面我以PDFBox为例讲因为License问题在实际项目中是绕不过去的。iText的用法我会在关键地方对比说明。PDFBox的Maven依赖dependency groupIdorg.apache.pdfbox/groupId artifactIdpdfbox/artifactId version2.0.29/version /dependency3.2 用PDFBox把条码图片画到PDF上PDFBox绘制图片的核心类是PDPageContentStream通过drawImage方法把PDImageXObject画到指定位置。完整流程如下public void createPdfWithBarcode(String outputPath, ListOrderInfo orders) throws IOException { PDDocument document new PDDocument(); for (OrderInfo order : orders) { PDPage page new PDPage(PDRectangle.A4); document.addPage(page); PDPageContentStream contentStream new PDPageContentStream(document, page); // 画标题 contentStream.beginText(); contentStream.setFont(PDType1Font.HELVETICA_BOLD, 16); contentStream.newLineAtOffset(50, 780); contentStream.showText(Order: order.getOrderNo()); contentStream.endText(); // 生成条码图片 BufferedImage barcodeImage generateBarcode(order.getOrderNo(), 400, 100); PDImageXObject pdImage LosslessFactory.createFromImage(document, barcodeImage); // 画条码参数图片对象, x坐标, y坐标, 宽度, 高度 contentStream.drawImage(pdImage, 50, 700, 200, 50); // 画条码下方的文字 contentStream.beginText(); contentStream.setFont(PDType1Font.HELVETICA, 10); contentStream.newLineAtOffset(50, 685); contentStream.showText(order.getOrderNo()); contentStream.endText(); contentStream.close(); } document.save(outputPath); document.close(); }这段代码有几个关键点。坐标系统PDFBox的坐标原点在页面左下角x向右y向上。A4纸的尺寸是595 x 842点1点1/72英寸。所以y780大概在页面顶部往下一点的位置。这个和很多前端框架的坐标原点在左上角是反的第一次用容易搞混。图片尺寸drawImage的width和height参数是绘制到PDF上的实际尺寸单位是点。上面代码里条码画成200x50点换算成厘米大概是7cm x 1.76cm打印出来扫码枪能扫。如果画太小比如50x12点打印出来可能只有1.7cm宽扫码枪识别率会下降。图片质量LosslessFactory.createFromImage是无损方式嵌入图片适合条码这种黑白分明的图。如果条码图片很大PDF文件也会变大。可以在生成条码图片时控制尺寸不要生成过大的图再缩放。3.3 二维码在PDF里的定位和尺寸计算二维码在PDF里的绘制逻辑和条码一样但尺寸计算更讲究。二维码是正方形的画的时候宽高要一致不然会变形导致扫不出来。假设我要在A4纸的右上角画一个3cm x 3cm的二维码。3cm换算成点是3 / 2.54 * 72 ≈ 85点。所以drawImage的width和height都设85。BufferedImage qrImage generateQRCode(order.getQrContent(), 300, 300); PDImageXObject qrPdImage LosslessFactory.createFromImage(document, qrImage); // A4纸宽595点右边距50点二维码宽85点所以x 595 - 50 - 85 460 contentStream.drawImage(qrPdImage, 460, 700, 85, 85);这里有个经验二维码在PDF里的实际尺寸不要小于2cm x 2cm。小于这个尺寸打印出来扫码枪或者手机扫起来会比较费劲尤其是纠错级别低的时候。如果空间实在有限可以适当降低纠错级别到M但不要低于M。3.4 多页PDF和批量生成的性能问题一批单据可能有几十上百页每页都要生成条码图片、画到PDF上。如果串行处理耗时会比较长。我实测过生成100页带条码的PDF串行大概需要3-5秒主要时间花在图片生成和PDF绘制上。优化思路有几个图片生成并行化用线程池并行生成条码图片然后按顺序绘制到PDF。注意PDFBox的PDDocument不是线程安全的绘制阶段必须单线程。复用PDDocument不要每页都新建PDDocument一个文档加多页。控制图片尺寸条码图片不要生成太大300x300的二维码足够了生成1000x1000再缩放是浪费。流式输出如果前端是直接下载可以用ServletOutputStream边生成边写减少内存占用。// 并行生成图片的示例 ExecutorService executor Executors.newFixedThreadPool(4); ListFutureBufferedImage futures new ArrayList(); for (OrderInfo order : orders) { futures.add(executor.submit(() - generateBarcode(order.getOrderNo(), 400, 100))); } // 然后按顺序取结果绘制 for (int i 0; i orders.size(); i) { BufferedImage img futures.get(i).get(); // 绘制到PDF... } executor.shutdown();注意PDFBox 2.x的PDDocument在并发环境下有已知问题绘制阶段务必单线程操作。图片生成可以并行但PDF写入不要并行。4. 前端展示PDF流怎么传、怎么显示4.1 后端返回PDF流的两种方式后端生成PDF后给前端的方式主要有两种一种是返回PDF文件的URL前端直接打开这个URL另一种是直接返回PDF的字节流前端用Blob处理。方式一返回URL。后端把PDF存到文件服务器或者本地磁盘返回一个可访问的URL。前端用window.open(url)或者iframe srcurl展示。这种方式简单但需要处理文件存储和清理而且如果PDF包含敏感信息URL被泄露会有安全问题。方式二返回字节流。后端直接把PDF字节流写到HTTP响应里前端用responseType: blob接收然后创建Blob URL展示。这种方式不需要存储文件安全性更好是我更推荐的做法。后端代码示例GetMapping(/export/pdf) public void exportPdf(HttpServletResponse response) throws IOException { response.setContentType(application/pdf); response.setHeader(Content-Disposition, inline; filenameorders.pdf); PDDocument document new PDDocument(); // ... 生成PDF内容 ... document.save(response.getOutputStream()); document.close(); }注意Content-Disposition用inline表示浏览器内嵌展示用attachment表示下载。如果希望前端自己控制展示方式可以统一用inline前端拿到Blob后自己决定是展示还是下载。4.2 前端用Blob和iframe展示PDF前端接收PDF流的典型代码axios.get(/api/export/pdf, { responseType: blob }).then(response { const blob new Blob([response.data], { type: application/pdf }); const url URL.createObjectURL(blob); // 方式一新窗口打开 window.open(url); // 方式二嵌入iframe // document.getElementById(pdfFrame).src url; // 注意使用完后要释放 // URL.revokeObjectURL(url); });这里有几个坑。Blob类型必须指定new Blob([response.data], { type: application/pdf })如果不指定type浏览器可能当成二进制流下载而不是展示。URL.revokeObjectURL的时机createObjectURL创建的URL会占用内存不需要的时候要revoke。但如果在iframe里展示revoke太早会导致PDF加载失败。我的做法是在iframe的onload事件之后再revoke或者干脆不revoke等页面关闭自动释放。移动端兼容性移动端浏览器对iframe内嵌PDF的支持参差不齐。iOS的Safari对iframe展示PDF支持还行但Android部分浏览器会直接下载。如果移动端是重点建议用PDF.js这类前端渲染库把PDF渲染成canvas展示兼容性更好。4.3 用PDF.js做更可控的展示如果对展示效果有更高要求比如需要自定义工具栏、翻页、缩放可以用PDF.js。它把PDF渲染成canvas前端完全可控。基本用法import * as pdfjsLib from pdfjs-dist; pdfjsLib.GlobalWorkerOptions.workerSrc pdf.worker.min.js; axios.get(/api/export/pdf, { responseType: arraybuffer }) .then(response { const loadingTask pdfjsLib.getDocument({ data: response.data }); loadingTask.promise.then(pdf { // 渲染第一页 pdf.getPage(1).then(page { const canvas document.getElementById(pdfCanvas); const context canvas.getContext(2d); const viewport page.getViewport({ scale: 1.5 }); canvas.width viewport.width; canvas.height viewport.height; page.render({ canvasContext: context, viewport: viewport }); }); }); });PDF.js的优点是展示完全可控不依赖浏览器内置的PDF查看器。缺点是引入了一个比较大的JS库而且需要额外配置worker文件。如果只是简单展示用iframe就够了如果需要精细控制再上PDF.js。4.4 前端展示时的常见问题排查问题一PDF显示空白。最常见的原因是Blob的type没设对或者后端返回的Content-Type不是application/pdf。检查后端响应头和前端Blob构造。问题二中文乱码。PDFBox默认的Helvetica字体不支持中文如果PDF里有中文需要嵌入中文字体。PDFBox加载字体的方式PDType0Font font PDType0Font.load(document, new File(fonts/simsun.ttf)); contentStream.setFont(font, 12);字体文件需要打包到项目里或者从系统字体目录加载。注意字体版权问题商业项目要用有授权的字体。问题三条码在PDF里模糊。检查生成条码图片时的尺寸和PDF里绘制的尺寸。如果图片生成时是200x50PDF里画成400x100就会模糊。原则是图片生成的像素尺寸要大于等于PDF里绘制的点尺寸。比如PDF里画200x50点图片至少生成200x50像素最好2倍即400x100像素。问题四多页PDF只显示第一页。iframe展示PDF时浏览器内置查看器通常支持翻页。如果只显示第一页可能是PDF本身只有一页或者查看器的问题。用PDF.js的话需要自己实现翻页逻辑。5. 完整链路串起来从请求到展示的端到端实现5.1 后端服务层的完整代码结构把前面的片段串起来一个完整的后端服务大概长这样Service public class PdfExportService { public void exportOrdersPdf(ListOrderInfo orders, OutputStream outputStream) throws IOException { PDDocument document new PDDocument(); try { for (OrderInfo order : orders) { PDPage page new PDPage(PDRectangle.A4); document.addPage(page); try (PDPageContentStream cs new PDPageContentStream(document, page)) { // 标题 cs.beginText(); cs.setFont(PDType1Font.HELVETICA_BOLD, 16); cs.newLineAtOffset(50, 780); cs.showText(Order Detail); cs.endText(); // 条码 BufferedImage barcodeImg BarcodeUtil.generateBarcode(order.getOrderNo(), 400, 100); PDImageXObject barcodePd LosslessFactory.createFromImage(document, barcodeImg); cs.drawImage(barcodePd, 50, 700, 200, 50); // 条码文字 cs.beginText(); cs.setFont(PDType1Font.HELVETICA, 10); cs.newLineAtOffset(50, 685); cs.showText(order.getOrderNo()); cs.endText(); // 二维码 BufferedImage qrImg BarcodeUtil.generateQRCode(order.getQrContent(), 300, 300); PDImageXObject qrPd LosslessFactory.createFromImage(document, qrImg); cs.drawImage(qrPd, 460, 680, 85, 85); // 订单信息 cs.beginText(); cs.setFont(PDType1Font.HELVETICA, 12); cs.newLineAtOffset(50, 600); cs.showText(Customer: order.getCustomerName()); cs.newLineAtOffset(0, -20); cs.showText(Date: order.getCreateTime()); cs.endText(); } } document.save(outputStream); } finally { document.close(); } } }Controller层RestController RequestMapping(/api) public class ExportController { Autowired private PdfExportService pdfExportService; GetMapping(/orders/pdf) public void exportOrders(HttpServletResponse response) throws IOException { ListOrderInfo orders orderService.getPendingOrders(); response.setContentType(application/pdf); response.setHeader(Content-Disposition, inline; filenameorders.pdf); pdfExportService.exportOrdersPdf(orders, response.getOutputStream()); } }5.2 前端页面的完整交互前端页面一个简单的实现button idpreviewBtn预览PDF/button button iddownloadBtn下载PDF/button iframe idpdfFrame stylewidth:100%; height:600px; border:1px solid #ccc;/iframe script document.getElementById(previewBtn).addEventListener(click, function() { fetch(/api/orders/pdf) .then(response response.blob()) .then(blob { const url URL.createObjectURL(blob); document.getElementById(pdfFrame).src url; }); }); document.getElementById(downloadBtn).addEventListener(click, function() { fetch(/api/orders/pdf) .then(response response.blob()) .then(blob { const url URL.createObjectURL(blob); const a document.createElement(a); a.href url; a.download orders.pdf; a.click(); URL.revokeObjectURL(url); }); }); /script预览和下载的区别在于预览是把Blob URL赋给iframe的src下载是创建一个a标签触发下载。两个按钮可以复用同一个接口只是前端处理方式不同。5.3 性能与体验的平衡点实际项目里如果单据数量很大比如超过500页一次性生成PDF可能会让用户等太久。我的做法是分页生成如果前端支持分页预览可以按页请求每次只生成当前页的PDF。异步生成通知如果必须一次性生成可以改成异步任务生成完成后通过WebSocket或者轮询通知前端下载。进度条如果生成时间在3秒以上前端加个loading或者进度条避免用户以为卡死了。另外PDF文件大小也要关注。100页带条码的PDF如果每页都有图片文件可能到几MB。如果前端是移动端下载流量要考虑。优化方式是控制图片尺寸和压缩质量条码图片用无损压缩但尺寸不要过大。6. 那些文档里不会写的踩坑记录6.1 条码扫不出来的几个真实原因原因一边距设成0。前面提过ZXing的MARGIN默认是4我为了省空间设成0结果打印出来扫码枪死活扫不出来。后来查资料才知道条码四周需要留白静区没有静区扫码设备无法识别条码的起始和结束。改回1之后问题解决。原因二图片被拉伸变形。有一次PDF里条码画成了宽200高30比例严重失调扫码枪识别率极低。一维条码的宽高比很重要太扁了不行。后来改成200x50识别率恢复正常。原因三打印分辨率不够。屏幕上看条码很清楚打印出来模糊。原因是生成条码图片时尺寸太小比如只生成了100x25像素打印到纸上被放大边缘模糊。解决办法是生成足够大的图片一般400x100像素起步。原因四二维码纠错级别太低。有一次二维码内容比较多用了L级别打印出来稍微有点折痕就扫不出来。改成H级别后即使二维码中间被遮挡一部分也能扫出来。6.2 PDFBox字体加载的坑PDFBox用PDType1Font.HELVETICA显示英文没问题但显示中文会报错或者显示成方块。必须加载支持中文的TrueType字体。加载字体时要注意字体文件要放在项目资源目录或者可访问的路径。字体文件不能太大中文字体动辄十几MB打包到jar里会让jar变大。可以考虑从系统字体目录加载但不同操作系统字体路径不同兼容性差。字体版权问题商业项目要用有授权的字体比如思源黑体这类开源字体。// 从classpath加载字体 PDType0Font font PDType0Font.load(document, getClass().getClassLoader().getResourceAsStream(fonts/SourceHanSans.ttf));6.3 前端Blob处理的坑坑一responseType没设对。用axios请求PDF流时必须设responseType: blob或者arraybuffer否则axios默认按JSON解析拿到的数据是乱的。坑二Blob type没设。new Blob([data])不指定type的话浏览器可能当成未知类型展示时直接下载而不是预览。必须指定{ type: application/pdf }。坑三revokeObjectURL时机不对。在iframe的src赋值后立即revoke会导致PDF加载失败。正确做法是在iframe的onload事件里revoke或者干脆不revoke。坑四跨域问题。如果前端和后端不同域需要后端设置CORS头允许前端域名访问。PDF流请求同样受跨域限制。6.4 并发场景下的线程安全问题PDFBox的PDDocument不是线程安全的。如果多个请求同时生成PDF每个请求应该创建自己的PDDocument实例不要共享。ZXing的Writer实例倒是可以复用但为了简单每次new一个也没啥性能问题。另外如果用了字体加载PDType0Font.load每次都要读字体文件频繁调用会有IO开销。可以考虑缓存字体字节但PDType0Font实例本身是和PDDocument绑定的不能跨文档复用。所以缓存字体文件的字节数组每次load时用ByteArrayInputStream减少磁盘IO。7. 一些可以继续优化的方向如果这个功能已经跑通了想再进一步优化有几个方向可以考虑。条码内容加密如果条码里包含敏感信息比如订单金额、客户信息直接明文编码有安全风险。可以对内容做加密或者签名扫码后先验证再解析。批量打印的排版优化如果一批单据要打印到一张纸上比如A4纸打4个标签需要计算好每个条码的位置和间距PDFBox的坐标计算要更精细。可以考虑用表格布局的方式先算好每个单元格的位置再绘制。支持更多条码格式ZXing支持很多格式如果业务需要EAN-13、UPC-A、Data Matrix等只需要换Writer类就行整体流程不变。前端PDF.js的深度集成如果需要更复杂的交互比如在PDF上做标注、搜索、高亮PDF.js提供了丰富的API。可以基于PDF.js做一个自定义的PDF查看器体验比浏览器内置的好很多。异步导出消息通知对于大批量导出改成异步任务用户提交后可以关闭页面生成完成后通过站内信或者邮件通知下载。后端用线程池或者消息队列处理避免请求超时。我在实际项目里最后落地的方案是ZXing生成条码和二维码PDFBox绘制PDF后端返回字节流前端用iframe预览Blob下载。这套组合稳定跑了两年多中间遇到的主要问题就是前面提到的那些解决之后基本没再出过幺蛾子。如果让我重新选一次可能还是会选PDFBox虽然API底层一点但License省心而且画条码这个场景本身不复杂多写几行坐标计算的事。