如何生成含文字与无失真图片的PDF?(Codename One实现)
解决Codename One手动构建PDF时的图片显示问题
先澄清一个误解
你看到的“类似SQLite数据库格式”其实是PDF对二进制数据的编码方式(通常是ASCII85或Base64),再加上PDF对象的结构化字典,并非SQLite格式。PDF图像数据的核心是对应格式的压缩二进制流,配合正确的过滤器声明。
针对JPG失真的解决方法
JPG失真的核心是二次压缩——如果你把JPG解码成Image对象再重新编码写入PDF,就会损失画质。正确的做法是直接复用JPG的原始压缩数据:
- 不要通过Codename One的
Image类处理JPG,直接读取原始文件的字节流。 - 在PDF图像XObject中指定
/Filter /DCTDecode(JPG原生压缩过滤器),直接写入原始JPG字节。
示例代码:
// 读取原始JPG字节 InputStream jpgIn = FileSystemStorage.getInstance().openInputStream("/path/to/photo.jpg"); byte[] rawJpg = Util.readInputStream(jpgIn); jpgIn.close(); // 构建PDF图像对象(假设你有自定义的PDF对象处理类) PdfXObject imgObj = new PdfXObject(); imgObj.setType("/XObject"); imgObj.setSubtype("/Image"); imgObj.put("/Width", 1200); // 替换为实际图片宽度 imgObj.put("/Height", 800); // 替换为实际图片高度 imgObj.put("/BitsPerComponent", 8); imgObj.put("/ColorSpace", "/DeviceRGB"); imgObj.put("/Filter", "/DCTDecode"); imgObj.setRawData(rawJpg); // 直接写入原始JPG字节 // 在PDF页面中引用该图像 PdfPage currentPage = pdfDoc.getActivePage(); currentPage.getContentStream().append("q 0.5 0 0 0.5 50 50 cm /Img1 Do Q"); // 0.5是缩放比例,50,50是左下角坐标,/Img1是图像对象的名称
针对PNG不显示的解决方法
PNG不显示通常是因为透明通道处理不当,或者PDF未识别PNG的颜色模式。解决步骤:
- 若PNG是索引色模式,先转成RGB/A格式(Codename One的
Image.createImage()会自动处理,但要确保获取完整的RGBA数据)。 - PDF支持透明需要通过
/SMask(软蒙版)单独存储Alpha通道,不能直接把RGBA数据塞进普通图像对象。
示例代码:
// 读取PNG并获取像素数据 Image pngImg = Image.createImage("/path/to/icon.png"); int width = pngImg.getWidth(); int height = pngImg.getHeight(); int[] pixels = pngImg.getRGB(); // 1. 构建RGB像素字节流(PDF图像坐标是从下到上,所以反转y轴) ByteArrayOutputStream rgbStream = new ByteArrayOutputStream(); for (int y = height - 1; y >= 0; y--) { for (int x = 0; x < width; x++) { int pixel = pixels[y * width + x]; rgbStream.write((pixel >> 16) & 0xFF); // R rgbStream.write((pixel >> 8) & 0xFF); // G rgbStream.write(pixel & 0xFF); // B } } byte[] rgbData = rgbStream.toByteArray(); rgbStream.close(); // 2. 构建Alpha通道字节流(用于SMask) ByteArrayOutputStream alphaStream = new ByteArrayOutputStream(); for (int y = height - 1; y >= 0; y--) { for (int x = 0; x < width; x++) { int pixel = pixels[y * width + x]; alphaStream.write((pixel >> 24) & 0xFF); // Alpha值 } } byte[] alphaData = alphaStream.toByteArray(); alphaStream.close(); // 3. 压缩RGB和Alpha数据(PDF用FlateDecode,即ZLIB压缩) byte[] compressedRGB = compressData(rgbData); byte[] compressedAlpha = compressData(alphaData); // 4. 创建SMask对象(存储Alpha通道) PdfXObject smaskObj = new PdfXObject(); smaskObj.setType("/XObject"); smaskObj.setSubtype("/Image"); smaskObj.put("/Width", width); smaskObj.put("/Height", height); smaskObj.put("/BitsPerComponent", 8); smaskObj.put("/ColorSpace", "/DeviceGray"); smaskObj.put("/Filter", "/FlateDecode"); smaskObj.setRawData(compressedAlpha); // 5. 创建主图像对象并关联SMask PdfXObject imgObj = new PdfXObject(); imgObj.setType("/XObject"); imgObj.setSubtype("/Image"); imgObj.put("/Width", width); imgObj.put("/Height", height); imgObj.put("/BitsPerComponent", 8); imgObj.put("/ColorSpace", "/DeviceRGB"); imgObj.put("/Filter", "/FlateDecode"); imgObj.put("/SMask", smaskObj.getReference()); // 关联蒙版 imgObj.setRawData(compressedRGB); // 辅助压缩方法 private byte[] compressData(byte[] raw) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); DeflaterOutputStream deflater = new DeflaterOutputStream(out); deflater.write(raw); deflater.close(); return out.toByteArray(); }
两种实现方式的问题排查
- StringBuilder拼接:绝对不能直接把二进制字节转成字符串(会丢失编码),必须先把二进制数据用ASCII85或Base64编码后再拼入字符串。
- 直接写入流:要确保PDF对象的字典属性完整(Width、Height、Filter等缺一不可),流的写入顺序要符合PDF的文件结构(先写对象内容,再更新交叉引用表)。
正确提取插入图片二进制的核心原则
- 尽量复用原始压缩数据:JPG用DCTDecode直接写入原始字节,PNG的IDAT块数据可以直接用FlateDecode写入(无需解压缩再压缩)。
- 匹配PDF的图像规范:根据图片类型设置对应的
Filter和ColorSpace,透明PNG必须添加SMask。 - 避免不必要的编码转换:不要把二进制数据转成普通字符串,要么用ASCII85/Base64编码,要么直接写入二进制流。
内容的提问来源于stack exchange,提问作者Eric
相关产品推荐
相关产品推荐

