最近需要利用安卓端直连打印机,然后静默打印(所谓的云打印是一个解决方案,但是本身需要联网,而且惠普的云打印。。。服务个人尚且嫌慢,生产就更不用说了)
一般的打印中,我们首先会通过 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);
}
}
}