opendataloader-project/opendataloader-pdf · error · IOException

pdf2img PAGE_PNG_DATA is not a readable image

Error message

pdf2img PAGE_PNG_DATA is not a readable image

What it means

The Hancom AI backend returned a PAGE_PNG_DATA field that is valid Base64 but whose decoded bytes are not an image format that javax.imageio.ImageIO can decode. ImageIO.read() returns null when it finds no registered ImageReader for the byte stream's magic bytes. This is a page-level failure inside fetchPageImage, which is declared to throw only IOException so the per-page pipeline can skip the failed page rather than aborting the whole document.

Source

Thrown at java/opendataloader-pdf-core/src/main/java/org/opendataloader/pdf/hybrid/HancomAIClient.java:726

            String pngBase64 = innerResult.has("PAGE_PNG_DATA")
                ? innerResult.get("PAGE_PNG_DATA").asText() : null;
            if (pngBase64 == null || pngBase64.isEmpty()) {
                throw new IOException("pdf2img PAGE_PNG_DATA is empty");
            }

            byte[] pngBytes;
            try {
                pngBytes = Base64.getDecoder().decode(pngBase64);
            } catch (IllegalArgumentException e) {
                // fetchPageImage is declared to throw IOException and callers catch
                // only IOException. Escaping IAE would abort the whole conversion
                // instead of skipping the failed page.
                throw new IOException("pdf2img PAGE_PNG_DATA is not valid Base64", e);
            }
            BufferedImage image = ImageIO.read(new ByteArrayInputStream(pngBytes));
            if (image == null) {
                throw new IOException("pdf2img PAGE_PNG_DATA is not a readable image");
            }
            if (cropOutput.active()) {
                savePageImageFile(cropOutput.directory(), pageIndex, pngBytes);
            }
            return image;
        }
    }

    /**
     * Sends a cropped image to IMAGE_CAPTIONING and returns the caption text.
     */
    /** Image-captioning result: caption text + the model's self-reported confidence. */
    static final class CaptionResult {
        final String caption;
        final Double confidence;
        CaptionResult(String caption, Double confidence) {
            this.caption = caption;
            this.confidence = confidence;

View on GitHub (pinned to a7789b8e77)

Solutions

  1. Retry the page: this is often transient (truncated response), so calling fetchPageImage again for the same pageIndex may succeed on a clean response.
  2. Verify the Hancom AI server's pdf2img endpoint is healthy by calling it directly with a known-good PDF and inspecting the PAGE_PNG_DATA output.
  3. If using a reverse proxy or load balancer in front of the Hancom API, check for response size limits or body truncation settings.
  4. Register additional ImageIO readers (e.g., add a WebP or TIFF plugin JAR to the classpath) if the backend may return formats beyond PNG/JPEG.
  5. Enable cropOutput (--save-crops) to dump the raw pngBytes to disk for offline inspection of what the server actually returned.

Example fix

// before: no retry, single fetch fails the page
BufferedImage img = cache.getOrFetch(pageIndex, HancomAIClient.this::fetchPageImage);

// after: retry once before giving up on the page
BufferedImage img = null;
for (int attempt = 0; attempt < 2 && img == null; attempt++) {
    try {
        img = cache.getOrFetch(pageIndex, HancomAIClient.this::fetchPageImage);
    } catch (IOException e) {
        if (attempt == 1) throw e;
        LOGGER.warning("Retrying page " + pageIndex + " after image decode failure");
    }
}
Defensive patterns

Strategy: retry

Validate before calling

// Before calling fetchPageImage, validate the server health
client.checkAvailability(); // throws IOException if server unreachable
// Cannot pre-validate image decodability — the bytes come from the server at runtime.

Try / catch

try {
    BufferedImage image = cache.getOrFetch(pageIndex, this::fetchPageImage);
} catch (IOException e) {
    if (e.getMessage().contains("not a readable image")) {
        // Retry once — often transient (truncated response)
        image = fetchPageImage(pageIndex);
    } else {
        throw e;
    }
}

Prevention

When it happens

Trigger: Called when HancomAIClient.fetchPageImage() processes a pdf2img response where RESULT[0].RESULT.PAGE_PNG_DATA is non-empty Base64 but decodes to corrupt/truncated/wrong-format bytes (e.g., a JPEG masquerading in a field named PNG_DATA, a truncated response from a network glitch, or a server-side rendering failure that wrote an error string instead of image data). The Base64.decode succeeds but ImageIO.read(new ByteArrayInputStream(pngBytes)) returns null.

Common situations: Hancom AI server under heavy load producing truncated image payloads; a proxy or CDN truncating the Base64 string at a chunk boundary; server-side pdf2img module crashing mid-render and returning a partial or zero-length binary; content-type mismatch where the backend embeds a format ImageIO has no reader for (e.g., AVIF, WebP without native plugins).

Related errors


AI-assisted analysis of opendataloader-project/opendataloader-pdf@a7789b8e77 (2026-08-14). Data as JSON: /api/errors/e5918226dc4f346d. Report an issue: GitHub.