使用javax.imageio读取JPG元数据报错及图片加水印问题
解决JPG图片加水印时的
JFIF APP0 must be first marker after SOI异常 我来帮你搞定这个JPG加水印时遇到的异常问题。
问题根源
你碰到的这个异常,本质是Java原生的JPEG图片读取器对格式校验太严格了——标准的JFIF格式要求APP0标记(存储JFIF元数据的段)必须紧跟在图像起始标记SOI(也就是0xFFD8)后面,但有些非标准的JPG(比如被第三方编辑工具修改过、带有自定义APP标记的图片)会打乱这个顺序,导致读取元数据时直接抛出错误。
解决方案
下面给你两个靠谱的修复方案,你可以根据自己的场景选择:
方案1:修改JPEGReader参数,关闭严格校验
针对JPG格式,我们可以强制将ImageReader转为JPEGImageReader,并调整解码参数跳过APP0顺序检查。同时优化代码的资源管理(用try-with-resources自动关闭资源,避免内存泄漏):
private InputStream createWatermark(byte[] bytes, Long position) { String format = getExtensionFromImage(bytes); try (ByteArrayOutputStream os = new ByteArrayOutputStream(255); ImageOutputStream ios = ImageIO.createImageOutputStream(os); ByteArrayInputStream bais = new ByteArrayInputStream(bytes); ImageInputStream iis = ImageIO.createImageInputStream(bais)) { // 获取对应格式的图片读取器 Iterator<ImageReader> readerIter = ImageIO.getImageReadersBySuffix(format); if (!readerIter.hasNext()) { throw new IOException("找不到适配格式的图片读取器: " + format); } ImageReader reader = readerIter.next(); // 针对JPG格式,关闭严格的标记顺序校验 if (reader instanceof JPEGImageReader) { JPEGImageReader jpegReader = (JPEGImageReader) reader; JPEGDecodeParam decodeParam = jpegReader.getDefaultJPEGDecodeParam(iis); // 允许处理非标准顺序的JPG标记 decodeParam.setAllowNonStandardJPEG(true); jpegReader.setJPEGDecodeParam(decodeParam); } reader.setInput(iis); // 读取元数据和图片内容 IIOMetadata metadata = reader.getImageMetadata(0); BufferedImage bi = reader.read(0); // 添加水印逻辑 try (Graphics2D g2d = (Graphics2D) bi.getGraphics()) { DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd-MM-yyyy HH:mm:ss"); String watermark = LocalDateTime.now().format(formatter); g2d.setColor(Color.WHITE); g2d.setFont(new Font("Arial", Font.BOLD, bi.getWidth() / 45)); FontMetrics fontMetrics = g2d.getFontMetrics(); Rectangle2D rect = fontMetrics.getStringBounds(watermark, g2d); int x = getX(position, bi, rect); int y = getY(position, bi); g2d.drawString(watermark, x, y); } // 写入带水印的图片(保留原始元数据) Iterator<ImageWriter> writerIter = ImageIO.getImageWritersByFormatName(format); if (!writerIter.hasNext()) { throw new IOException("找不到适配格式的图片写入器: " + format); } ImageWriter writer = writerIter.next(); writer.setOutput(ios); ImageWriteParam iwParam = writer.getDefaultWriteParam(); iwParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); IIOImage image = new IIOImage(bi, null, metadata); writer.write(null, image, iwParam); writer.dispose(); return new ByteArrayInputStream(os.toByteArray()); } catch (IOException e) { e.printStackTrace(); // 异常场景下返回原始图片流(可根据业务需求调整) return new ByteArrayInputStream(bytes); } }
关键优化点:
- 用
try-with-resources自动关闭所有流和资源,避免内存泄漏 - 针对JPEGImageReader设置
setAllowNonStandardJPEG(true),允许处理非标准顺序的JPG标记 - 增加了读取器/写入器的存在性检查,避免
NoSuchElementException
方案2:预处理JPG字节数组,调整APP0标记顺序
如果方案1不生效(比如某些旧Java版本不支持setAllowNonStandardJPEG),可以手动修改JPG字节流,把APP0标记移到SOI之后的第一个位置:
// 辅助方法:调整JPG字节数组,确保APP0是SOI后的第一个标记 private byte[] adjustJpgMarkerOrder(byte[] bytes) throws IOException { // 先判断是不是JPG图片 if (bytes.length < 2 || bytes[0] != (byte) 0xFF || bytes[1] != (byte) 0xD8) { return bytes; } ByteArrayOutputStream baos = new ByteArrayOutputStream(); baos.write(bytes, 0, 2); // 先写入SOI标记 int app0Start = -1; int app0End = -1; int currentPos = 2; List<byte[]> otherMarkers = new ArrayList<>(); // 遍历找到APP0标记和其他前置标记 while (currentPos + 1 < bytes.length) { if (bytes[currentPos] == (byte) 0xFF) { byte marker = bytes[currentPos + 1]; if (marker == (byte) 0xE0) { // 找到APP0标记 int length = ((bytes[currentPos + 2] & 0xFF) << 8) | (bytes[currentPos + 3] & 0xFF); app0Start = currentPos; app0End = currentPos + length; break; } else { // 记录其他标记段 int length = ((bytes[currentPos + 2] & 0xFF) << 8) | (bytes[currentPos + 3] & 0xFF); byte[] markerSegment = Arrays.copyOfRange(bytes, currentPos, currentPos + length); otherMarkers.add(markerSegment); currentPos += length; } } else { currentPos++; } } // 先写入APP0标记(如果找到) if (app0Start != -1 && app0End != -1) { baos.write(bytes, app0Start, app0End - app0Start); } // 再写入其他标记段 for (byte[] marker : otherMarkers) { baos.write(marker); } // 写入剩余的图片数据 if (app0End != -1) { baos.write(bytes, app0End, bytes.length - app0End); } else { baos.write(bytes, currentPos, bytes.length - currentPos); } return baos.toByteArray(); }
然后在createWatermark方法开头调用这个辅助方法:
byte[] adjustedBytes = adjustJpgMarkerOrder(bytes); // 后续流程用adjustedBytes代替原bytes即可
这个方法通过重新排列JPG的标记顺序,让APP0回到SOI之后的第一个位置,符合标准JFIF格式要求,从而绕过ImageReader的校验异常。
额外建议
- 确保
getExtensionFromImage方法能正确识别jpg和jpeg两种后缀 - 水印的字体大小可以加个兜底逻辑,避免小图片上的水印过大超出范围
- 异常处理部分可以替换为业务日志记录,方便排查问题
内容的提问来源于stack exchange,提问作者Filip Fico Janackovic
相关产品推荐
相关产品推荐

