You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.14 07:14:52