java静默打印PDF(可实现生产环境下服务器写入PDF模板,然后调用客户端打印机打印)

java静默打印PDF可实现生产环境下服务器写入PDF模板,然后调用客户端打印机打印

一、简

需求

写这个的原因主要是因为当时项目中的打印功能是用户打印标签时,每次点击打印是通过把PDF文件下载到客户端浏览器,然后需要通过浏览器去点击打印机实现打印,就非常麻烦,每次都步骤非常复杂,而且每次参数都要重新设置。于是就想着怎么通过java实现自己调用打印机,用户只需要输入需要写入pdf模板的参数,提前配置好打印参数,然后后台自己去调用打印不需要通过浏览器去单个打印。

具体实现把文字、二维码、条形码、图片实现通过模板写入pdf文件,然后再到打印机打印处理

实现步骤

  • 先大致介绍一下这篇文章的内容,主要是通过 Adobe Acrobat DC(或者其他的PDF模板制作app),制作好PDF模板,然后通过itextpdf框架把数据写入到模板对应的文本域中,可以实现PDF文件打开,写入的内容可以正常显示代表pdf文件制作没用问题了。
  • 然后在需要连接打印机打印的上部署一个调用本地打印机的jar包,jar主要通过pdfbox框架实现调用本地打印机,成功把需要打印的pdf文件传递到打印机的打印队列,实现打印。
  • 在打印机主机的jar写好接受服务器打印的pdf的http接口,用于接收服务器传递过来的需要打印的pdf文件以及一些打印参数(包括指定打印机、自定义纸张大小、设置打印参数、以及显示打印对话框等)

二、代码实现

主要包括服务器部分(这里只包括写入pdf模板到调用打印机客户端接口,具体业务根据自己实际业务修改就行)和打印机客户端。

0、打印模板

通过 Adobe Acrobat DC配置对应的文本域,具体配置可以百度查看,这里就不赘述了

1、服务器部分 (端口:8090)

yml只设置了端口,就不展示出来了

1.1、maven依赖

xml 复制代码
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
                <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>4.5.15</version>
        </dependency>
<!--pdf生成-->
        <dependency>
            <groupId>com.itextpdf</groupId>
            <artifactId>itextpdf</artifactId>
            <version>5.5.1</version>
        </dependency>
        <dependency>
            <groupId>com.itextpdf</groupId>
            <artifactId>itext-asian</artifactId>
            <version>5.2.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.pdfbox</groupId>
            <artifactId>pdfbox</artifactId>
            <version>2.0.23</version>
        </dependency>
        <!--fastjson-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.48</version>
        </dependency>

1.2、实体

1.2.1、接口返回类

java 复制代码
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result {
    /**
     * 是否成功
     **/
    private Boolean success;
    /**
     * 错误信息
     **/
    private String message;
    /**
     * 请求状态 200-成功 400-失败
     **/
    private Integer code;
    /**
     * 当前时间戳
     **/
    private Long timestamp;
    /**
     * 返回结果
     **/
    private Object result;

    public static Result ok() {
        return new Result(true, null, 200, System.currentTimeMillis(),null);
    }

    public static Result ok(Object data) {
        return new Result(true, null, 200,System.currentTimeMillis(),data);
    }


    public static Result ok(List<?> data) {
        return new Result(true, null, 200,System.currentTimeMillis(),data);
    }

    public static Result error(String errorMsg) {
        return new Result(false, errorMsg, 400,System.currentTimeMillis(),null);
    }
}

1.2.2、标签纸页面参数类

打印的标签纸页面参数类,添加默认值

java 复制代码
/**
 * @author zhengfuping
 * @version 1.0
 * 110*65 的标签纸页面
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PaperVo {
    /**宽*/
    private double width = 100*2.83;
    /**高*/
    private double height = 60*2.83;
    /** X 坐标*/
    private double x = 15;
    /** Y 坐标*/
    private double y = 10;
    /**
     * 打印页面方向:
     *      0:横向 从右向左,1:横向 从左向右,2:纵向。
     * */
    private Integer orientation = PageFormat.PORTRAIT;
    private String name;
}

1.2.3、PDF模板参数类

对应pdf模板的文本域名称

java 复制代码
/**
 * @author zhengfuping
 * @version 1.0
 * pdf模板参数
 */
@Data
@Builder
public class Template {
    private String time;

    private String code;

    private String qrCode;

    private String barCode;

    private String image;

    private String age;

    private String name;
}

1.3、Controller层接口

java 复制代码
@RequestMapping("/pdf")
@RestController
public class PDFController {
    @Autowired
    private Netty netty;
    @Autowired
    private HttpPdf httpPdf;


    /**
     * @author yingfeng
     * @Param * @param params 包括两个参数 copies:打印张数、duplex:是否打印反面
     * @param request
     * @return * @return Result
     */
    @PostMapping("/print")
    public Result print(@RequestBody Map<String ,Object> params, HttpServletRequest request){
        //因为测试原因,便于理解,所以参数直接添加
        String Code = "43504277201002308221C0100C010145006";
        String barCode = "43504277201002308221C0100C010145006-bar";
        String time = DateUtil.format(new Date(), "yyyyMMdd");
        String qrCode = "https://blog.csdn.net/weixin_52315708";
        String name = "张三";
        String image = "D:/1zheng/dai/excel/exportexcel/a1.jpg";
        Template template = Template.builder()
                .qrCode(qrCode)
                .code(Code)
                .time(time)
                .barCode(barCode)
                .image(image)
                .name(name)
                .age("18").build();
        //转为map是因为需要循环把值写入对应的文本域
        Map<String, Object> map = BeanUtil.beanToMap(template);
		//调用写入文本域工具类,返回对应的byte[]数据
        byte[] pdf = PDFUtil.test(data);
        params.put("pdf",pdf);
        //用于调用客户端的接口
        Result result = httpPdf.doPostWith(params);
        return result;
    }

1.4、写入pdf工具类

java 复制代码
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DateUtil;
import com.itextpdf.text.*;
import com.itextpdf.text.pdf.*;
import com.itextpdf.text.pdf.qrcode.EncodeHintType;
import com.itextpdf.text.pdf.qrcode.ErrorCorrectionLevel;
import com.zheng.exceltest.pdf.entity.Template;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import sun.misc.BASE64Encoder;
import java.io.*;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;

/**
 * @author zhengfuping
 * @version 1.0
 * 实现往打印机打印
 */
@Slf4j
public class PDFUtil {

 public static byte[] test(Map<String, Object> data)  {
        BASE64Encoder encoder = new BASE64Encoder();
        BufferedInputStream bin = null;
        ByteArrayOutputStream bos = null;
        PdfStamper ps = null;
        OutputStream fos = null;
        try {
            // pdf模板
            String fileName = "exportexcel/PDF打印测试模板.pdf";
            //读取pdf
            PdfReader reader  = new PdfReader(fileName);
            bos = new ByteArrayOutputStream();
            //将要生成的目标PDF文件名称
            ps = new PdfStamper(reader, bos);
//        PdfContentByte under = ps.getUnderContent(1);
//        取出报表模板中的所有字段
            AcroFields fields = ps.getAcroFields();

//        对表单数据进行赋值
            fillData(fields,ps,data);
            
            ps.setFormFlattening(true);
            ps.close();
            fos = new FileOutputStream("D:/模板打印测试/a1.pdf");
            fos.write(bos.toByteArray());
            fos.flush();
            fos.close();   //实际应该finally在关闭一次
            bos.close();  //注意,需要在得到 byte[]之前关闭流

//            执行打印
            byte[] bytes = bos.toByteArray();
            return bytes;
        }catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 具体往模板的对应文本域写入数据
     * @author zhengfuping
     * @date 2023/8/9 15:55
     * @param fields AcroFields对象
     * @param ps PdfStamper对象
     * @param data 数据
     */
	public static void fillData(AcroFields fields, PdfStamper ps, Map<String, Object> data) throws IOException, DocumentException {
//        设置中文字体
        BaseFont bf = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED);
        ArrayList<BaseFont> fonts = new ArrayList<>();
        Font font = FontFactory.getFont(getFontPath("SimHei.ttf"), BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED,(short)14);
        fonts.add(font.getBaseFont());
        fonts.add(bf);
        fields.setSubstitutionFonts(fonts);
	//循环遍历集合中的文本域字段,根据名称进行不同处理
        for (String key : data.keySet()) {
            System.out.println(key+":"+data.get(key));
            if (data.get(key)==null)continue;
            if (key.equals("image")) {      //      生成图片
                String value = data.get(key).toString();
                String imgpath = value;
                int pageNo = fields.getFieldPositions(key).get(0).page;
                Rectangle signRect = fields.getFieldPositions(key).get(0).position;
                float x = signRect.getLeft();
                float y = signRect.getBottom();
                // 根据路径读取图片
                Image image = Image.getInstance(imgpath);
                // 获取图片页面
                PdfContentByte under = ps.getOverContent(pageNo);
                // 图片大小自适应
                image.scaleToFit(signRect.getWidth(), signRect.getHeight());
                // 添加图片
                image.setAbsolutePosition(x, y);
                under.addImage(image);
            } else if (key.equals("barCode")) {     //生成条形码
                //遍历条码字段
                String value = data.get(key).toString();
//              获取位置(左上右下)
                AcroFields.FieldPosition fieldPosition = fields.getFieldPositions(key).get(0);
//                  ?null
                PdfNumber rNum = fields.getFieldItem(key).getWidget(0).getAsDict(PdfName.AP).getAsNumber(PdfName.R);
                if (rNum == null) {
                    fieldPosition.position.setRotation(0);
                } else {
                    fieldPosition.position.setRotation(rNum.intValue());
                }
                //绘制条码
                Barcode128 barcode128 = new Barcode128();
                barcode128.setSize(8);
                if (fieldPosition.position.getRotation() == 90 || fieldPosition.position.getRotation() == 270) {
                    barcode128.setBarHeight(25);
                    barcode128.setX(0.82f);
                } else {
                    //条码宽高
//                    barcode128.setBarHeight(fieldPosition.position.getHeight() - 40);
//                    barcode128.setX(fieldPosition.position.getWidth() / 150);
                    barcode128.setBarHeight(25);
                    barcode128.setX(0.5f);
                }
                //条码与数字间距
                barcode128.setBaseline(8);
                //条码值
                barcode128.setCode(value);
                barcode128.setStartStopText(false);
                barcode128.setExtended(true);
                //绘制在第一页
                PdfContentByte cb = ps.getOverContent(1);
                //生成条码图片
                Image image128 = barcode128.createImageWithBarcode(cb, null, null);
                //旋转度数
                image128.setRotationDegrees(fieldPosition.position.getRotation());

                //左边距(居中处理)
                float marginLeft = (fieldPosition.position.getRight() - fieldPosition.position.getLeft() - image128.getWidth()) / 2;
                //条码位置
                image128.setAbsolutePosition(fieldPosition.position.getLeft() + marginLeft, fieldPosition.position.getBottom());
                //加入条码
                cb.addImage(image128);
            }else if ("qrCode".equals(key)){        //生成二维码
                //           遍历二维码字段
                String value = data.get(key).toString();
                // 获取属性的类型
                if (value != null ) {
                    //获取位置(左上右下)
                    AcroFields.FieldPosition fieldPosition = fields.getFieldPositions(key).get(0);
                    //绘制二维码
                    float width = fieldPosition.position.getRight()/2 - fieldPosition.position.getLeft()/2;
                    //设定容错性二维码容错率用字母表示,容错能力等级分为:L、M、Q、H四级:L :7%;M:15%;Q:25%;H:30%
                    Map<EncodeHintType, Object> hints = new HashMap<>();
                    hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H);
                    BarcodeQRCode pdf417 = new BarcodeQRCode(value.toString(), (int) width, (int) width, hints);
                    //生成二维码图像
                    Image image128 = pdf417.getImage();
                    //绘制在第一页
                    PdfContentByte cb = ps.getOverContent(1);
                    //左边距(居中处理)
                    float marginLeft = (fieldPosition.position.getRight() - fieldPosition.position.getLeft() - image128.getWidth()) / 2;
                    //条码位置
                    image128.setAbsolutePosition(fieldPosition.position.getLeft() + marginLeft, fieldPosition.position.getBottom()-3f);
                    //加入条码
                    cb.addImage(image128);
                }
            }else{          //生成文字
                String value = data.get(key).toString();
//                String partCode = (String) data.get("name");
                设置文本大小
//                if (partCode.length()<8 && key.equals("age")){
//                    fields.setFieldProperty(key,"textsize",(float)36,null);
//
//                }else {
//                    fields.setFieldProperty(key,"textsize",(float)9,null);
//                }
//                设置文本字体
                if (Pattern.compile("[\u4E00-\u9FA5]").matcher(key).find()){
                    fields.setFieldProperty(key,"textfont",bf,null);    //中文
                    fields.setField(key, value);
                }else {
                    fields.setFieldProperty(key,"textfont",font.getBaseFont(),null);  //英文
                    fields.setField(key, value);
                }
            }
        }
    }

    /**
     * 获取本机的字体文件
     *
     * @param fontName
     */
    private static String getFontPath(String fontName) {
        String fontPath = "C:\\Windows\\Fonts\\" + fontName;
        // 判断系统类型,加载字体文件
        java.util.Properties prop = System.getProperties();
        String osName = prop.getProperty("os.name").toLowerCase();
        if (osName.indexOf("linux") > -1) {
            fontPath = "/usr/share/fonts/" + fontName;
        }
        log.info(osName + "-------------------" + fontPath);
        return fontPath;
    }

1.5、调用客户端jar接口

java 复制代码
/**
 * @author zhengfuping
 * @version 1.0
 * 调用客户端打印机的jar
 * @date 2023/4/14 14:35
 */
@Service
public class HttpPdf {
    @Autowired
    private RestTemplate restTemplate;

	//接口路径
    final String url = "http://127.0.0.1:9050/print/print";


    public Result doPostWith(Map<String ,Object> params){
        PaperVo paperVo = new PaperVo();
        System.out.println();
        params.put("paper",paperVo);
        Result result = restTemplate.postForObject(url, params, Result.class);
        return result;
    }

2、打印机客户端jar (端口:9050)

2.1、依赖

xml 复制代码
 <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.apache.pdfbox</groupId>
            <artifactId>pdfbox</artifactId>
            <version>2.0.23</version>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.4.0</version>
        </dependency>
    </dependencies>

2.2、实体类

同上

2.2.1、纸张对象

java 复制代码
@Data
public class PaperVo {
    /**长*/
    private double width = 216;
    /**宽*/
    private double height = 360;
    /** X 坐标*/
    private double x = 5;
    /** Y 坐标*/
    private double y = 100;
    /**
     * 打印页面方向:
     *      0:横向 从右向左,1:横向 从左向右,2:纵向。
     * */
    private Integer orientation = PageFormat.PORTRAIT;
}

2.2.2、统一返回对象

java 复制代码
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result {
    /**
     * 是否成功
     **/
    private Boolean success;
    /**
     * 错误信息
     **/
    private String message;
    /**
     * 请求状态 200-成功 400-失败
     **/
    private Integer code;
    /**
     * 当前时间戳
     **/
    private Long timestamp;
    /**
     * 返回结果
     **/
    private Object result;

    public static Result ok() {
        return new Result(true, null, 200, System.currentTimeMillis(),null);
    }

    public static Result ok(Object data) {
        return new Result(true, null, 200,System.currentTimeMillis(),data);
    }


    public static Result ok(List<?> data) {
        return new Result(true, null, 200,System.currentTimeMillis(),data);
    }

    public static Result error(String errorMsg) {
        return new Result(false, errorMsg, 400,System.currentTimeMillis(),null);
    }
}

2.3、Controller层,接收服务器调用打印请求

java 复制代码
/**
 * @author zhengfuping
 * @version 1.0
 * 接收调用打印请求
 */
@RequestMapping("/print")
@RestController
public class PrintController {
    /**
     * @Description: 默认使用打印机的名称。可以存在数据库里做持久化
     */
    private static String printName = "SATO CL4NX Plus 305dpi";


    /**
     * @Description: 获取打印机列表
     * @Param: []
     * @Return: com.wq.print.util.R
     */
    @RequestMapping("/list")
    public Result list() {
        ArrayList<String> list = new ArrayList<>();
        // 遍历所有打印机的名称
        for (PrintService ps : PrinterJob.lookupPrintServices()) {
            list.add(ps.getName());
        }
        if (list.size() != 0) {
            return Result.ok(list);
        }
        return Result.error("暂无可用打印机,请检查系统打印机设置");
    }
    /**
     * @Description: 设置使用的打印机
     * @Param:
     * @Return: com.wq.print.util.R
     */
    @PostMapping("/setPrint")
    public Result setPrint(@RequestParam("printName") String printName) {
        PrintController.printName = printName;
        return Result.ok("打印机设置成功");
    }

    @PostMapping("/print")
    public Result print(@RequestBody Map<String ,Object> params, HttpServletRequest request){
        try {
            String pdfBase64Str = String.valueOf(params.get("pdf"));
            if (StrUtil.isEmptyIfStr(pdfBase64Str)) {
                return Result.error("pdf的Base64字符串有误或为空,请检查");
            }
            //因为传输过程中会把 byte[]转为 pdfBase64Str,需要重新转回来
            byte[] pdfByte = PrintUtil.base64ToFileByte(pdfBase64Str);
				//设置参数,没设置也要给默认值
            int copies = params.get("copies") == null ?1 : Integer.parseInt(params.get("copies").toString());
            boolean duplex = params.get("duplex") != null && Boolean.parseBoolean(params.get("duplex").toString());
            Map<String,Object> pdf = (Map<String,Object>) params.get("paper");
            PaperVo paperVo = null;
            //如果服务器没有传对应参数,就用这边的
            if (pdf==null){
                paperVo = new PaperVo();
            }else {
                paperVo = BeanUtil.mapToBean(pdf, PaperVo.class, false, new CopyOptions());
            }
            Boolean print = PrintUtil.print(pdfByte, PrintController.printName, copies, duplex,paperVo);

            if (print){
                return Result.ok("打印完成");
            }else {
                return Result.ok("打印失败");
            }
        } catch (NumberFormatException e) {
            e.printStackTrace();
            return Result.ok("打印失败"+e.getMessage());
        }
    }
}

2.4、配置打印参数调用打印机

配置配置参数调用客户端打印机

java 复制代码
public class PrintUtil {

    /** 
     * 调用配置打印机
     * @author zhengfuping
     * @param pdfByte 数据
     * @param printName  打印机名称
     * @param copies 打印张数
     * @param duplex 是否打印反面
     * @param paperVo 纸张参数
     * @return Boolean 
     */
    public static Boolean print(byte[] pdfByte, String printName, int copies, boolean duplex , PaperVo paperVo) {
        //加载pdf文件对象
        try(PDDocument document = PDDocument.load(pdfByte)){
            //            创建打印任务
            PrinterJob job = PrinterJob.getPrinterJob();
            //            遍历所有打印机的名称
            for (PrintService ps : PrinterJob.lookupPrintServices()){
                String psName = ps.getName();
                if (psName.equals(printName)){
                    job.setPrintService(ps);
                    break;
                }
            }

            job.setPageable(new PDFPageable(document));
//            纸张对象
            Paper paper = new Paper();
            // 设置打印纸张大小
            paper.setSize(paperVo.getWidth(),paperVo.getHeight());
            // 设置打印位置 坐标
            paper.setImageableArea(paperVo.getX(),paperVo.getY(),paper.getWidth(),paper.getHeight());

//            打印的页面参数
            PageFormat pageFormat = new PageFormat();
            pageFormat.setPaper(paper);
            pageFormat.setOrientation(paperVo.getOrientation()); //横向 从右向左

            Book book = new Book();
            //            打印页面对象--配置
            PDFPrintable pdfPrintable = new PDFPrintable(document, Scaling.SHRINK_TO_FIT, true, 0, true);
            book.append(pdfPrintable,pageFormat,1);
            job.setPageable(book);
//            打印份额
            job.setCopies(copies);

            if (duplex){
                HashPrintRequestAttributeSet printSet = new HashPrintRequestAttributeSet();
                printSet.add(Sides.DUPLEX);
                job.print(printSet);
            }else {
                job.print();
            }
            document.close();
            return true;

        }catch (Exception e){
            e.printStackTrace();
            return false;
        }
    }

    /** 
     * Base64转换编码
     * @Param * @param strBase64
     */
    
    public static byte[] base64ToFileByte(String strBase64) {
        return java.util.Base64.getDecoder().decode(strBase64);
    }

三、测试

1、调用测试

使用postman调用服务器的打印接口

最终实现的打印文件因为测试我是直接写入到本地的,实际项目中可能是通过浏览器调用,需要上载到浏览器,直接配置request就行,如果文件可以正常打开,并且客户端有接收到参数,pdf中有数据,说明服务器端代码没问题了。

  • 具体打印的pdf文件

2、实现结果

具体直接最后一句document.close(); 调用打印机后,对应的打印队列中有任务,则说明实现成功

3、查看客户端打印机

总结

  • 具体实现上需要注意文本域的名称要对应上;

  • 局限是需要再客户端服务器需要在同一个局域网,如果不是的话,则客户端的接口也需要映射出去用于给服务器调用。

  • 客户端的代码实际上可以转为jar后通过 exe4j 转为.exe文件,再通过Inno Setup 把jdk绑定进去。

相关推荐
巫山老妖4 分钟前
多 Agent 协作实战:我用 3 只龙虾组了个「AI小分队」,效率直接翻倍
java·前端
还不秃顶的计科生5 分钟前
latex代码裁剪pdf图片
pdf·latex
xienda9 分钟前
Spring Boot 核心定义与用处
java·spring boot·后端
直有两条腿18 分钟前
【Spring Boot】原理
java·spring boot·后端
一只叫煤球的猫20 分钟前
用这个框架彻底摆脱Controller,从此专注业务——ArcRoute
java·spring·开源
SunnyDays101122 分钟前
Java 如何根据模板高效生成Word文档
java·根据模板生成word文档·生成word文档
攀岩巨峰的程序猿24 分钟前
代码开发过程中涉及到bean的copy方法梳理
java
golang学习记27 分钟前
IDEA 2026.1 EAP 5 发布:K2模式更强了!
java·ide·intellij-idea
xuansec29 分钟前
【JavaEE安全】Java反序列化深度剖析:核心原理、利用链构造与安全风险管控
java·安全·java-ee
艾莉丝努力练剑31 分钟前
静态地址重定位与动态地址重定位:Linux操作系统的视角
java·linux·运维·服务器·c语言·开发语言·c++