Android 利用IPP协议静默打印

最近需要利用安卓端直连打印机,然后静默打印(所谓的云打印是一个解决方案,但是本身需要联网,而且惠普的云打印。。。服务个人尚且嫌慢,生产就更不用说了)

一般的打印中,我们首先会通过 PrintManager 这个来打印,但是这种打印方式在打印之前会弹出预览框,而且这种预览框属于系统级的弹框,无法取消,反射也无能为力。

搜索了一番之后,发现可以通过 原始协议(9100)端口,发送PJL命令打印,具体请见(https://stackoverflow.com/questions/47937508/printing-without-print-service-preview),但是很可惜,公司的打印机不直接支持PDF,但后来发现惠普的打印机 支持PCL这个东西,于是又将PDF转换成了PCL,但是打印机只支PCL3 ,转换出来的是PCL6 还是不能正确打印

public class PrintService {

    private static PrintListener printListener;

    public enum PaperSize {
        A4,
        A5
    }

    public static void printPDFFile(final String printerIP, final int printerPort,
                                    final File file, final String filename, final PaperSize paperSize, final int copies) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                Socket socket = null;
                DataOutputStream out = null;
                FileInputStream inputStream = null;
                try {
                    socket = new Socket(printerIP, printerPort);
                    out = new DataOutputStream(socket.getOutputStream());
                    DataInputStream input = new DataInputStream(socket.getInputStream());
                    inputStream = new FileInputStream(file);
                    byte[] buffer = new byte[3000];

                    final char ESC = 0x1b;
                    final String UEL = ESC + "%-12345X";
                    final String ESC_SEQ = ESC + "%-12345\r\n";

                    out.writeBytes(UEL);
                    out.writeBytes("@PJL \r\n");
                    out.writeBytes("@PJL JOB NAME = '" + filename + "' \r\n");
                    out.writeBytes("@PJL SET PAPER=" + paperSize.name());
                    out.writeBytes("@PJL SET COPIES=" + copies);
                    out.writeBytes("@PJL ENTER LANGUAGE = PDF\r\n");
                    while (inputStream.read(buffer) != -1)
                        out.write(buffer);
                    out.writeBytes(ESC_SEQ);
                    out.writeBytes("@PJL \r\n");
                    out.writeBytes("@PJL RESET \r\n");
                    out.writeBytes("@PJL EOJ NAME = '" + filename + "'");
                    out.writeBytes(UEL);

                    out.flush();
                } catch (IOException e) {
                    e.printStackTrace();
                    if (printListener != null)
                        printListener.networkError();
                } finally {
                    try {
                        if (inputStream != null)
                            inputStream.close();
                        if (out != null)
                            out.close();
                        if (socket != null)
                            socket.close();
                        if (printListener != null)
                            printListener.printCompleted();
                    } catch (IOException e) {
                        e.printStackTrace();
                        if (printListener != null)
                            printListener.networkError();
                    }
                }
            }
        });
        t.start();
    }

    public static void setPrintListener(PrintListener list) {
        printListener = list;
    }

    public interface PrintListener {
        void printCompleted();

        void networkError();
    }

又经过一番折腾后,发现还有IPP协议这个东西(默认端口631,需要打印机本身支持),可以通过IPP协议,将需要打印的文件传送给打印机,打印机解析完毕后就会开始打印了

ipp 文档 https://www.pwg.org/ipp/ippguide.html
java实现: https://github.com/HPInc/jipp

支持这个协议的打印机也不是什么类型的文档都可以打印,在发送文件之前,还需要知道 打印机支持的文件类型,可以利用下面的方法:

  public List<String> getSupportFormats() throws IOException {
    URI uri = URI.create(url);
    IppPacket attributeRequest = IppPacket.getPrinterAttributes(uri)
            .putOperationAttributes(
                    requestingUserName.of("jprint"),
                    requestedAttributes.of(documentFormatSupported.getName()))
            .build();
    try (IppPacketData request = new IppPacketData(attributeRequest)) {
        try (IppPacketData response = transport.sendData(uri, request)) {
            return response.getPacket().getStrings(printerAttributes, documentFormatSupported);
        }
    }
}

url是打印机的ipp地址,以惠普 tank 519 为例,这个地址是 http://打印机ip:631,如果 里面包含了 application/pdf,那么恭喜你,不需要其他转换,就可以把pdf文档发送给打印机打印了,但是公司这台打印机只支持

application/vnd.hp-pcl
application/PCLm
image/**

这些类型,并不直接支持PDF,因此还需要将PDF转化为PCLm,转化方法如下:

 private static final int DPI = 600;
private static final ImageType IMAGE_TYPE = ImageType.RGB;
public static void convert(File f, File p) throws IOException {

    final long start = System.currentTimeMillis();

    try (InputStream pdfInputStream = new BufferedInputStream(new FileInputStream(f));
         OutputStream outputStream = new BufferedOutputStream(new FileOutputStream(p))) {

        ColorSpace colorSpace = convertImageTypeToColorSpace(IMAGE_TYPE);
        try (PDDocument document = PDDocument.load(pdfInputStream)) {
            PDFRenderer pdfRenderer = new PDFRenderer(document);
            PDPageTree pages = document.getPages();
            final List<RenderablePage> renderablePages = new ArrayList<>();

            for (int pageIndex = 0; pageIndex < pages.getCount(); pageIndex++) {
                final BufferedImage image = pdfRenderer.renderImageWithDPI(pageIndex, DPI, IMAGE_TYPE);
                final int width = image.getWidth();
                final int height = image.getHeight();

                RenderablePage renderablePage = new RenderablePage(width, height) {

                    @Override
                    public void render(int yOffset, int swathHeight, ColorSpace colorSpace, byte[] byteArray) {
                        int red, green, blue, rgb;

                        int byteIndex = 0;
                        for (int y = yOffset; y < (yOffset + swathHeight); y++) {
                            for (int x = 0; x < width; x++) {

                                rgb = image.getRGB(x, y);
                                red = (rgb >> 16) & 0xFF;
                                green = (rgb >> 8) & 0xFF;
                                blue = rgb & 0xFF;
                                byteArray[byteIndex++] = (byte) red;
                                byteArray[byteIndex++] = (byte) green;
                                byteArray[byteIndex++] = (byte) blue;
                            }
                        }
                    }
                };
                renderablePages.add(renderablePage);
            }

            RenderableDocument renderableDocument = new RenderableDocument() {
                @Override
                public Iterator<RenderablePage> iterator() {
                    return renderablePages.iterator();
                }

                @Override
                public int getDpi() {
                    return DPI;
                }
            };

            saveRenderableDocumentAsPCLm(renderableDocument, colorSpace, outputStream);
        }
    }
    System.out.println(System.currentTimeMillis() - start);
}

private static ColorSpace convertImageTypeToColorSpace(ImageType imageType) {
    switch (imageType) {
        case BINARY:
        case GRAY:
            return ColorSpace.Grayscale;
        default:
            return ColorSpace.Rgb;
    }
}

private static void saveRenderableDocumentAsPCLm(RenderableDocument renderableDocument,
                                                 ColorSpace colorSpace, OutputStream outputStream) throws IOException {
    OutputSettings outputSettings = new OutputSettings(colorSpace, Sides.oneSided, MediaSource.auto, PrintQuality.draft, false, 2);
    PclmSettings caps = new PclmSettings(outputSettings, 32);

    PclmWriter writer = new PclmWriter(outputStream, caps);
    writer.write(renderableDocument);
    writer.close();
}

这方法应该用于服务器端,安卓端300/600DPI虽然可以工作,但是速度太慢,1200DPI直接就罢工了(HuaWei Mate 30 Pro),况且安卓本身不支持 BufferedImage(可以通过第三方pdfbox的jar包,得到一个bitmap)

最后将得到的PCLm文件传送给打印机就可以打印了,但是打印速度会比正常的PDF慢很多,打印方法如下:

public URI print(File pclm, PrintListener listener) throws IOException {
    lock.lock();
    try {
        URI uri = URI.create(url);
        IppPacket printRequest = IppPacket.printJob(uri)
                .putOperationAttributes(
                        requestingUserName.of("jprint"),
                        documentFormat.of("application/PCLm"))
                .build();
        try (BufferedInputStream stream = new BufferedInputStream(new FileInputStream(pclm))) {
            try (IppPacketData request = new IppPacketData(printRequest, stream)) {
                try (IppPacketData response = transport.sendData(uri, request)) {
                    URI jobUri = response.getPacket().getValue(Tag.jobAttributes, new UriType("job-uri"));
                    if (jobUri == null) {
                        lock.unlock();
                        listener.onComplete(null);
                        return null;
                    }
                    waitJobComplete(jobUri, listener);
                    return jobUri;
                }
            }
        }
    } catch (Throwable ex) {
        lock.unlock();
        try {
            listener.onError(ex);
        } finally {
            listener.onComplete(null);
        }
    }
    return null;
}

返回的URI地址是一个打印作业的地址,可以通过这个地址得到作业状态(测试过程中发现,连续调用两次这个方法(没有加锁状态),最终只能打印一份),所以我这里的逻辑是打印好了一份才会接受另一个请求

等待作业打印完毕:

private void waitJobComplete(URI jobUri, PrintListener listener) {
    //wait job complete
    final EnumType<JobState> enumType = new EnumType<>("job-state", new Function1<Integer, JobState>() {
        @Override
        public JobState invoke(Integer integer) {
            return JobState.Companion.get(integer);
        }
    });
    IppPacket printRequest = IppPacket.getJobAttributes(jobUri, enumType).build();
    try (IppPacketData request = new IppPacketData(printRequest)) {
        try (IppPacketData response = transport.sendData(URI.create(url), request)) {
            JobState state = response.getPacket().getValue(Tag.jobAttributes, enumType);
            if(state == null) {
                lock.unlock();
                listener.onComplete(null);
                return;
            }
            if (state.getCode() > 5) {
                lock.unlock();
                listener.onComplete(state);
            } else {
                Thread.sleep(500L);
                waitJobComplete(jobUri, listener);
            }
        }
    } catch (Throwable ex) {
        lock.unlock();
        try {
            listener.onError(ex);
        } finally {
            listener.onComplete(null);
        }
    }
}
camunda bpmn-js-properties-panel简单使用