KDP(電子出版)のメモ 急急如律令

Amazon Kindleダイレクト・パブリッシングでの電子出版や電子書籍の作成販売について、文章やイラストの作成や編集方法について書いています。

「電書連 EPUB 3 制作ガイド」が更新されたのでwebpに対応させてみた

「電書連 EPUB 3 制作ガイド」まえは電書協だった気がするが、更新されてwebp対応とfilea-asのカタカナを削除などがあった。

 webp対応をしてみたがKindleではwebpは対応してないし対応しているビュワーも少なそう。それでもブラウザ動作させる「FixedEpub3JS」ではmimetypeを追加するだけなので直ぐにできた。

kyukyunyorituryo.github.io

AozoraEpub3ではJavaなのでImageIOに標準では対応していなかったのでWebPプラグインを追加して対応させる。拡張子がjpeg,gif,pngのみだったのでwebpも追加する。

dpfj.or.jp

あなた:

webpの対応させて

 /** ファイル名から画像を取得
     * 拡張子変更等は外側で修正しておく
     * ファイルシステムまたはZipファイルから指定されたファイル名の画像を取得
     * @param srcImageFileName ファイル名 Zipならエントリ名
     * ※先頭からシークされるので遅い? 
     * @throws RarException */
    public BufferedImage getImage(String srcImageFileName) throws IOException, RarException
    {
        if (this.isFile) {
            File file = new File(this.srcParentPath+srcImageFileName);
            if (!file.exists()) {
                //拡張子修正
                srcImageFileName = this.correctExt(srcImageFileName);
                file = new File(this.srcParentPath+srcImageFileName);
                if (!file.exists()) return null;
            }
            try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file), 8192)) {
                return ImageUtils.readImage(srcImageFileName.substring(srcImageFileName.lastIndexOf('.') + 1).toLowerCase(), bis);
            }
        } else {
            if (this.srcFile.getName().endsWith(".rar")) {
                InputStream is = null;
                Archive archive = new Archive(srcFile);
                try {
                FileHeader fileHeader = archive.nextFileHeader();
                while (fileHeader != null) {
                    if (!fileHeader.isDirectory()) {
                        String entryName = fileHeader.getFileName();
                        entryName = entryName.replace('\\', '/');
                        if (srcImageFileName.equals(entryName)) {
                            is = archive.getInputStream(fileHeader);
                            return ImageUtils.readImage(srcImageFileName.substring(srcImageFileName.lastIndexOf('.')+1).toLowerCase(), is);
                        }
                    }
                    fileHeader = archive.nextFileHeader();
                }
                } finally {
                    if (is != null) is.close();
                    archive.close();
                }
                
            } else {
                try (ZipFile zf = ZipFile.builder().setFile(this.srcFile).setUseUnicodeExtraFields(true).get()) {
                    ZipArchiveEntry entry = zf.getEntry(srcImageFileName);
                    if (entry == null) {
                        srcImageFileName = this.correctExt(srcImageFileName);
                        entry = zf.getEntry(srcImageFileName);
                        if (entry == null) return null;
                    }
                    try (InputStream is = zf.getInputStream(entry)) {
                        return ImageUtils.readImage(srcImageFileName.substring(srcImageFileName.lastIndexOf('.')+1).toLowerCase(), is);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
        return null;
    }

ChatGPT:

了解です。 このメソッドに WebP 形式の画像対応 を追加したいのですね。

Java 標準の ImageIO は WebP を直接サポートしていないため、 以下の2つのアプローチのいずれかが必要です。

ImageUtils.readImage() が内部で ImageIO.read() を使っているなら、 WebP のデコーダをプラグインで追加すれば、このメソッドの修正は最小限にできます。

例:

import com.luciad.imageio.webp.WebPReadParam;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.InputStream;

public class ImageUtils {
    public static BufferedImage readImage(String ext, InputStream is) throws IOException {
        ext = ext.toLowerCase();
        if (ext.equals("webp")) {
            // WebP対応
            ImageReader reader = ImageIO.getImageReadersByFormatName("webp").next();
            ImageInputStream iis = ImageIO.createImageInputStream(is);
            reader.setInput(iis, true);
            WebPReadParam param = new WebPReadParam();
            return reader.read(0, param);
        } else {
            return ImageIO.read(is);
        }
    }
}

import com.luciad.imageio.webp.WebPReadParam;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.InputStream;

public class ImageUtils {
    public static BufferedImage readImage(String ext, InputStream is) throws IOException {
        ext = ext.toLowerCase();
        if (ext.equals("webp")) {
            // WebP対応
            ImageReader reader = ImageIO.getImageReadersByFormatName("webp").next();
            ImageInputStream iis = ImageIO.createImageInputStream(is);
            reader.setInput(iis, true);
            WebPReadParam param = new WebPReadParam();
            return reader.read(0, param);
        } else {
            return ImageIO.read(is);
        }
    }
}

📦 必要ライブラリ: webp-imageio

(Maven: com.twelvemonkeys.imageio:imageio-webp:3.10.1 でもOK)

ImageUtils.readImage() を触りたくない場合は、getImage() メソッド側で判定します。

public BufferedImage getImage(String srcImageFileName) throws IOException, RarException {
    String ext = srcImageFileName.substring(srcImageFileName.lastIndexOf('.') + 1).toLowerCase();

    if (this.isFile) {
        File file = new File(this.srcParentPath + srcImageFileName);
        if (!file.exists()) {
            srcImageFileName = this.correctExt(srcImageFileName);
            file = new File(this.srcParentPath + srcImageFileName);
            if (!file.exists()) return null;
        }

        try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file), 8192)) {
            if (ext.equals("webp")) {
                return readWebP(bis);
            }
            return ImageUtils.readImage(ext, bis);
        }
    } else if (this.srcFile.getName().endsWith(".rar")) {
        try (Archive archive = new Archive(srcFile)) {
            FileHeader fileHeader;
            while ((fileHeader = archive.nextFileHeader()) != null) {
                if (!fileHeader.isDirectory()) {
                    String entryName = fileHeader.getFileName().replace('\\', '/');
                    if (srcImageFileName.equals(entryName)) {
                        try (InputStream is = archive.getInputStream(fileHeader)) {
                            if (ext.equals("webp")) return readWebP(is);
                            return ImageUtils.readImage(ext, is);
                        }
                    }
                }
            }
        }
    } else {
        try (ZipFile zf = ZipFile.builder().setFile(this.srcFile).setUseUnicodeExtraFields(true).get()) {
            ZipArchiveEntry entry = zf.getEntry(srcImageFileName);
            if (entry == null) {
                srcImageFileName = this.correctExt(srcImageFileName);
                entry = zf.getEntry(srcImageFileName);
                if (entry == null) return null;
            }
            try (InputStream is = zf.getInputStream(entry)) {
                if (ext.equals("webp")) return readWebP(is);
                return ImageUtils.readImage(ext, is);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    return null;
}

/** WebP専用読み込み */
private BufferedImage readWebP(InputStream is) throws IOException {
    ImageReader reader = ImageIO.getImageReadersByFormatName("webp").next();
    ImageInputStream iis = ImageIO.createImageInputStream(is);
    reader.setInput(iis, true);
    return reader.read(0, new com.luciad.imageio.webp.WebPReadParam());
}

public BufferedImage getImage(String srcImageFileName) throws IOException, RarException {
    String ext = srcImageFileName.substring(srcImageFileName.lastIndexOf('.') + 1).toLowerCase();

    if (this.isFile) {
        File file = new File(this.srcParentPath + srcImageFileName);
        if (!file.exists()) {
            srcImageFileName = this.correctExt(srcImageFileName);
            file = new File(this.srcParentPath + srcImageFileName);
            if (!file.exists()) return null;
        }

        try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file), 8192)) {
            if (ext.equals("webp")) {
                return readWebP(bis);
            }
            return ImageUtils.readImage(ext, bis);
        }
    } else if (this.srcFile.getName().endsWith(".rar")) {
        try (Archive archive = new Archive(srcFile)) {
            FileHeader fileHeader;
            while ((fileHeader = archive.nextFileHeader()) != null) {
                if (!fileHeader.isDirectory()) {
                    String entryName = fileHeader.getFileName().replace('\\', '/');
                    if (srcImageFileName.equals(entryName)) {
                        try (InputStream is = archive.getInputStream(fileHeader)) {
                            if (ext.equals("webp")) return readWebP(is);
                            return ImageUtils.readImage(ext, is);
                        }
                    }
                }
            }
        }
    } else {
        try (ZipFile zf = ZipFile.builder().setFile(this.srcFile).setUseUnicodeExtraFields(true).get()) {
            ZipArchiveEntry entry = zf.getEntry(srcImageFileName);
            if (entry == null) {
                srcImageFileName = this.correctExt(srcImageFileName);
                entry = zf.getEntry(srcImageFileName);
                if (entry == null) return null;
            }
            try (InputStream is = zf.getInputStream(entry)) {
                if (ext.equals("webp")) return readWebP(is);
                return ImageUtils.readImage(ext, is);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    return null;
}

/** WebP専用読み込み */
private BufferedImage readWebP(InputStream is) throws IOException {
    ImageReader reader = ImageIO.getImageReadersByFormatName("webp").next();
    ImageInputStream iis = ImageIO.createImageInputStream(is);
    reader.setInput(iis, true);
    return reader.read(0, new com.luciad.imageio.webp.WebPReadParam());
}

WebPを読み込むには、次のいずれかの依存関係を追加します:

<dependency>
  <groupId>com.twelvemonkeys.imageio</groupId>
  <artifactId>imageio-webp</artifactId>
  <version>3.10.1</version>
</dependency>

<dependency>
  <groupId>com.twelvemonkeys.imageio</groupId>
  <artifactId>imageio-webp</artifactId>
  <version>3.10.1</version>
</dependency>
implementation 'com.twelvemonkeys.imageio:imageio-webp:3.10.1'

implementation 'com.twelvemonkeys.imageio:imageio-webp:3.10.1'

ImageIO.scanForPlugins() を一度呼んでおくと、自動登録されます。

どちらの方針にしたいですか?

  • ImageUtils を編集できるなら「方針1」がシンプル。
  • 編集できない場合は「方針2」で getImage() 内にWebP対応を追加します。

ImageUtils を編集できるなら「方針1」がシンプル。

編集できない場合は「方針2」で getImage() 内にWebP対応を追加します。

あなた:

 /** ストリームから画像を読み込み */
    static public BufferedImage readImage(String ext, InputStream is) throws IOException
    {
        BufferedImage image;
            try {
                image = ImageIO.read(is);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        is.close();
        return image;
    }

ChatGPT:

ありがとうございます。 この ImageUtils.readImage() の中身を見る限り、ImageIO.read() しか呼んでいませんね。 つまり WebP対応をここに追加するのが最も簡潔で安全 です(=先ほどの「方針1」)。

com.twelvemonkeys.imageio:imageio-webp などの WebP プラグインを使う想定です。 この修正版は、他の呼び出し側(getImage() など)を一切変更せずに動作します。

/** ストリームから画像を読み込み */
static public BufferedImage readImage(String ext, InputStream is) throws IOException {
    BufferedImage image = null;
    try {
        ext = ext.toLowerCase();

        if (ext.equals("webp")) {
            // WebP専用処理
            ImageReader reader = ImageIO.getImageReadersByFormatName("webp").next();
            try (ImageInputStream iis = ImageIO.createImageInputStream(is)) {
                reader.setInput(iis, true);
                return reader.read(0, reader.getDefaultReadParam());
            }
        } else {
            // 通常画像(jpg, png, gif, bmpなど)
            image = ImageIO.read(is);
        }

    } catch (Exception e) {
        throw new RuntimeException(e);
    } finally {
        is.close();
    }
    return image;
}

/** ストリームから画像を読み込み */
static public BufferedImage readImage(String ext, InputStream is) throws IOException {
    BufferedImage image = null;
    try {
        ext = ext.toLowerCase();

        if (ext.equals("webp")) {
            // WebP専用処理
            ImageReader reader = ImageIO.getImageReadersByFormatName("webp").next();
            try (ImageInputStream iis = ImageIO.createImageInputStream(is)) {
                reader.setInput(iis, true);
                return reader.read(0, reader.getDefaultReadParam());
            }
        } else {
            // 通常画像(jpg, png, gif, bmpなど)
            image = ImageIO.read(is);
        }

    } catch (Exception e) {
        throw new RuntimeException(e);
    } finally {
        is.close();
    }
    return image;
}

このコードが動作するには、WebPプラグインを追加してください。

<dependency>
  <groupId>com.twelvemonkeys.imageio</groupId>
  <artifactId>imageio-webp</artifactId>
  <version>3.10.1</version>
</dependency>

<dependency>
  <groupId>com.twelvemonkeys.imageio</groupId>
  <artifactId>imageio-webp</artifactId>
  <version>3.10.1</version>
</dependency>
implementation 'com.twelvemonkeys.imageio:imageio-webp:3.10.1'

implementation 'com.twelvemonkeys.imageio:imageio-webp:3.10.1'

初回読み込み前に1度だけ以下を実行しておくと、 WebPプラグインが自動登録されます(例:アプリ起動時)。

ImageIO.scanForPlugins();

ImageIO.scanForPlugins();
  • 既存の getImage() や他の呼び出しコードを変更不要
  • .webp 拡張子だけ自動で専用リーダーに切り替え
  • 例外処理・ストリームクローズも従来通り安全

既存の getImage() や他の呼び出しコードを変更不要

.webp 拡張子だけ自動で専用リーダーに切り替え

例外処理・ストリームクローズも従来通り安全

希望があれば、

  • .webp 以外にも .avif や .heif に対応
  • ImageIO.read() が null を返したときの再試行 も加えた拡張版を出せます。

.webp 以外にも .avif や .heif に対応

ImageIO.read() が null を返したときの再試行 も加えた拡張版を出せます。

どうしますか?(WebPだけで十分?)

あなた:

webp-imageioを使うときの注意点は

ChatGPT:

良い質問です。 webp-imageio(たとえば sejda/webp-imageio )や twelvemonkeys の imageio-webp を使うときには、 いくつか 実務上の注意点・落とし穴 があります。

以下、実際にAozoraEpub3のような画像処理アプリに組み込む場合を想定して、 代表的な注意点を整理します。

まず、Java向け WebP デコーダには主に2系統あります:

ライブラリ Maven座標 特徴
webp-imageio com.luciad.imageio:webp-imageio Google製libwebpに準拠。高品質・安定。WebPReadParamなど専用APIあり。
TwelveMonkeys ImageIO WebP com.twelvemonkeys.imageio:imageio-webp ImageIO互換で他形式とも統一的に扱える。Android非対応。

🔸推奨: デスクトップ/Javaアプリでは TwelveMonkeys 系。 高機能でメンテが続いており、ImageIO.read()にも自動登録可能。

ImageIOは、起動時に自動でプラグインを認識しないことがあります。 そのため、初回使用前に1度だけ明示的に呼ぶと安全です。

ImageIO.scanForPlugins();

ImageIO.scanForPlugins();

これを呼んでおけば、 ImageIO.read() で webp も自動的に処理できるようになります。

🔸呼び出しタイミングの例: main() の最初や、static {} ブロックなど。

ImageIO.read(InputStream) は、 サポートされない形式のとき null を返す点に注意です。

WebPが未登録だと、

BufferedImage img = ImageIO.read(is);

BufferedImage img = ImageIO.read(is);

で例外ではなく null が返るため、 その後 img.getWidth() などで NullPointerException が発生します。

対策:

BufferedImage img = ImageIO.read(is);
if (img == null) throw new IOException("Unsupported image format");

BufferedImage img = ImageIO.read(is);
if (img == null) throw new IOException("Unsupported image format");

WebPは透過(alpha)付きの画像を扱えますが、 BufferedImage へ変換するときに TYPE_INT_ARGB でない場合、背景が黒くなることがあります。

対策(必要な場合):

BufferedImage image = ImageIO.read(is);
if (image.getType() != BufferedImage.TYPE_INT_ARGB) {
    BufferedImage argb = new BufferedImage(
        image.getWidth(),
        image.getHeight(),
        BufferedImage.TYPE_INT_ARGB
    );
    Graphics2D g = argb.createGraphics();
    g.drawImage(image, 0, 0, null);
    g.dispose();
    image = argb;
}

BufferedImage image = ImageIO.read(is);
if (image.getType() != BufferedImage.TYPE_INT_ARGB) {
    BufferedImage argb = new BufferedImage(
        image.getWidth(),
        image.getHeight(),
        BufferedImage.TYPE_INT_ARGB
    );
    Graphics2D g = argb.createGraphics();
    g.drawImage(image, 0, 0, null);
    g.dispose();
    image = argb;
}

環境によってはライブラリが存在しない・読み込み失敗する場合もあります。 その場合は例外をキャッチして、代替処理(PNGなど)にフォールバックさせます。

try {
    ImageIO.scanForPlugins();
    image = ImageIO.read(is);
    if (image == null) throw new IOException("Unsupported format");
} catch (Exception e) {
    // fallback
}

try {
    ImageIO.scanForPlugins();
    image = ImageIO.read(is);
    if (image == null) throw new IOException("Unsupported format");
} catch (Exception e) {
    // fallback
}
Javaバージョン 注意点
Java 8 問題なし(最も安定)
Java 11+ OK。だが古い webp-imageio は module-info 非対応なので --add-opens が必要な場合あり
Java 17+ twelvemonkeys 最新版を使用推奨(3.10.1以降)
  • imageio-webp は 静止画のみ対応(アニメーションWebPは非対応)
  • webp-imageio(Luciad版)は アニメーション対応 だが、ページ数分の read(index) が必要

imageio-webp は 静止画のみ対応(アニメーションWebPは非対応)

webp-imageio(Luciad版)は アニメーション対応 だが、ページ数分の read(index) が必要

もしアニメーションを扱うなら:

ImageReader reader = ImageIO.getImageReadersByFormatName("webp").next();
reader.setInput(iis);
int numImages = reader.getNumImages(true);
for (int i = 0; i < numImages; i++) {
    BufferedImage frame = reader.read(i);
    ...
}

ImageReader reader = ImageIO.getImageReadersByFormatName("webp").next();
reader.setInput(iis);
int numImages = reader.getNumImages(true);
for (int i = 0; i < numImages; i++) {
    BufferedImage frame = reader.read(i);
    ...
}

javax.imageio 系ライブラリは Android では動作しません。 (Androidは BitmapFactory.decodeStream() を使う別系統のAPI)

なので、WebP対応は Java/Swing/デスクトップアプリ限定 です。

項目 内容
登録 ImageIO.scanForPlugins() を初回に呼ぶ
例外処理 ImageIO.read() の null チェックを忘れない
アニメWebP 対応しているのは Luciad版のみ
Javaバージョン 17以上なら twelvemonkeys 3.10+ 推奨
Android 非対応(代替:BitmapFactory)

ご希望があれば、 AozoraEpub3用の ImageUtils.readImage() を twelvemonkeys で安全に WebP対応した 完全版 にしてお渡しできます。 作成しましょうか?

あなた:

ImageIO.scanForPlugins()はどこに入れるべきか

ChatGPT:

とても良い質問です。 ImageIO.scanForPlugins() は 1回だけ実行すれば十分 で、 アプリ起動直後(最初に画像を読み込む前) に呼ぶのがベストです。

以下に、AozoraEpub3 などの構成を想定して 「どこに入れるのが最適か」を段階的に解説します👇

public static void main(String[] args) {
    // WebPなどImageIOプラグインを読み込む
    ImageIO.scanForPlugins();

    // アプリ初期化
    new AozoraEpub3App().start(args);
}

public static void main(String[] args) {
    // WebPなどImageIOプラグインを読み込む
    ImageIO.scanForPlugins();

    // アプリ初期化
    new AozoraEpub3App().start(args);
}

💡 これが一番確実。 どのクラスよりも先に呼ばれるため、 その後の ImageIO.read() ですぐWebPが使えます。

もし main() にアクセスできない(ライブラリ内で動いているなど)場合は、 ImageUtils クラスの static初期化ブロック に入れておく方法があります。

public class ImageUtils {
    static {
        // クラスロード時に1回だけ実行される
        ImageIO.scanForPlugins();
    }

    /** ストリームから画像を読み込み */
    public static BufferedImage readImage(String ext, InputStream is) throws IOException {
        BufferedImage image;
        try {
            ext = ext.toLowerCase();
            if (ext.equals("webp")) {
                ImageReader reader = ImageIO.getImageReadersByFormatName("webp").next();
                try (ImageInputStream iis = ImageIO.createImageInputStream(is)) {
                    reader.setInput(iis, true);
                    return reader.read(0, reader.getDefaultReadParam());
                }
            } else {
                image = ImageIO.read(is);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            is.close();
        }
        return image;
    }
}

public class ImageUtils {
    static {
        // クラスロード時に1回だけ実行される
        ImageIO.scanForPlugins();
    }

    /** ストリームから画像を読み込み */
    public static BufferedImage readImage(String ext, InputStream is) throws IOException {
        BufferedImage image;
        try {
            ext = ext.toLowerCase();
            if (ext.equals("webp")) {
                ImageReader reader = ImageIO.getImageReadersByFormatName("webp").next();
                try (ImageInputStream iis = ImageIO.createImageInputStream(is)) {
                    reader.setInput(iis, true);
                    return reader.read(0, reader.getDefaultReadParam());
                }
            } else {
                image = ImageIO.read(is);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            is.close();
        }
        return image;
    }
}

✅ この方法だと、アプリ全体で ImageUtils.readImage() を最初に呼んだときに自動登録されます。 (以降のすべてのImageIO.read()にもWebPプラグインが反映されます)

もしアプリが AppConfig や Settings のような初期化用クラスを持っている場合、 その中に入れてもOKです。

public class AppConfig {
    public static void initialize() {
        ImageIO.scanForPlugins();
        // 他の初期化(設定読込、キャッシュ準備など)
    }
}

public class AppConfig {
    public static void initialize() {
        ImageIO.scanForPlugins();
        // 他の初期化(設定読込、キャッシュ準備など)
    }
}

そして、アプリ起動時に

AppConfig.initialize();

AppConfig.initialize();

を呼ぶだけでWebP対応が有効になります。

  • 呼ぶのは1回で十分。
  • 2回以上呼んでも問題はありません(再スキャンするだけ)。
  • コストは非常に小さい(数ms程度)。

呼ぶのは1回で十分。

2回以上呼んでも問題はありません(再スキャンするだけ)。

コストは非常に小さい(数ms程度)。

ImageIO.read() は WebPファイルを読めずに nullを返します。 例外ではないため、気づかずに NullPointerException が出ることがあります。

目的 ベストな場所
アプリ全体で確実に有効化 main() の最初
ImageUtils 単体で完結させたい static { ImageIO.scanForPlugins(); } に記述
初期化専用クラスがある その中の initialize() メソッド

もしよければ、 あなたのプロジェクトの main クラス名や構造に合わせて 「実際に入れる完全版コード」を示すこともできます。

どこで呼び出しているか(例:Main.java や AozoraEpub3App など)を教えてもらえますか?

あなた:

main関数でImageIOを呼び出していない場合は

ChatGPT:

とても良い質問です。 AozoraEpub3 のように「明示的な main() 関数がない構成(例:GUIアプリ/CLIランチャー/JavaFX/サーブレットなど)」では、 ImageIO.scanForPlugins() を どこで呼ぶか は確かに迷うポイントです。

以下では、そうした「main() がない(もしくは編集しにくい)」場合の安全な代替方法を整理します👇

この方法なら、どこから呼び出されても 最初に ImageUtils クラスがロードされた時点で 自動的に WebP プラグインが有効になります。

public class ImageUtils {
    static {
        // クラス読み込み時に一度だけ実行される
        try {
            ImageIO.scanForPlugins();
        } catch (Throwable t) {
            System.err.println("ImageIO plugin scan failed: " + t);
        }
    }

    /** ストリームから画像を読み込み */
    public static BufferedImage readImage(String ext, InputStream is) throws IOException {
        BufferedImage image;
        try {
            ext = ext.toLowerCase();
            if (ext.equals("webp")) {
                ImageReader reader = ImageIO.getImageReadersByFormatName("webp").next();
                try (ImageInputStream iis = ImageIO.createImageInputStream(is)) {
                    reader.setInput(iis, true);
                    return reader.read(0, reader.getDefaultReadParam());
                }
            } else {
                image = ImageIO.read(is);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            is.close();
        }
        return image;
    }
}

public class ImageUtils {
    static {
        // クラス読み込み時に一度だけ実行される
        try {
            ImageIO.scanForPlugins();
        } catch (Throwable t) {
            System.err.println("ImageIO plugin scan failed: " + t);
        }
    }

    /** ストリームから画像を読み込み */
    public static BufferedImage readImage(String ext, InputStream is) throws IOException {
        BufferedImage image;
        try {
            ext = ext.toLowerCase();
            if (ext.equals("webp")) {
                ImageReader reader = ImageIO.getImageReadersByFormatName("webp").next();
                try (ImageInputStream iis = ImageIO.createImageInputStream(is)) {
                    reader.setInput(iis, true);
                    return reader.read(0, reader.getDefaultReadParam());
                }
            } else {
                image = ImageIO.read(is);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            is.close();
        }
        return image;
    }
}

✅ この方法のメリット

  • main() がなくても確実に実行される
  • 画像処理専用の ImageUtils に閉じているため依存が分かりやすい
  • アプリ全体で1回しか呼ばれない
  • 他のコードを一切変更しなくていい

main() がなくても確実に実行される

画像処理専用の ImageUtils に閉じているため依存が分かりやすい

アプリ全体で1回しか呼ばれない

他のコードを一切変更しなくていい

アプリに「設定ロード」「パス初期化」「UI初期化」などの処理がある場合、 そこに一行追加するのも安全です。

public class AppInitializer {
    public static void init() {
        ImageIO.scanForPlugins();  // WebP対応を有効化
        // 設定読み込みやUI準備など...
    }
}

public class AppInitializer {
    public static void init() {
        ImageIO.scanForPlugins();  // WebP対応を有効化
        // 設定読み込みやUI準備など...
    }
}

そして、アプリ起動時に

AppInitializer.init();

AppInitializer.init();

を呼ぶだけ。

Swingなら、フレームや画面を作る前に呼ぶのが良いです。

public class AozoraEpub3App {
    public void start() {
        ImageIO.scanForPlugins(); // WebP登録

        // UI初期化など
        SwingUtilities.invokeLater(() -> new MainFrame().setVisible(true));
    }
}

public class AozoraEpub3App {
    public void start() {
        ImageIO.scanForPlugins(); // WebP登録

        // UI初期化など
        SwingUtilities.invokeLater(() -> new MainFrame().setVisible(true));
    }
}

JavaFXの start(Stage primaryStage) の先頭に入れるのが安全です。

@Override
public void start(Stage stage) {
    ImageIO.scanForPlugins();
    // 画面構築処理
}

@Override
public void start(Stage stage) {
    ImageIO.scanForPlugins();
    // 画面構築処理
}

もしこのコードが別のアプリに組み込まれる「ライブラリ」なら、 呼び出し側アプリで1回だけ ImageIO.scanForPlugins() を呼んでもらう設計にしてもOKです。 (ドキュメントなどに「WebPを使う場合は初期化が必要」と記載)

本当に登録できているか確認したい場合は:

for (String format : ImageIO.getReaderFormatNames()) {
    if (format.equalsIgnoreCase("webp")) {
        System.out.println("WebP reader is registered");
    }
}

for (String format : ImageIO.getReaderFormatNames()) {
    if (format.equalsIgnoreCase("webp")) {
        System.out.println("WebP reader is registered");
    }
}

これでコンソールに WebP reader is registered が出ればOKです。

環境 最適な場所 理由
汎用ライブラリ ImageUtils の staticブロック 自動実行・呼び出し側を変更不要
GUIアプリ(Swing / JavaFX) UI生成前 (start()やinit() の最初) プラグイン有効化を確実に行える
CLIツール / サーバーアプリ 起動初期化メソッド (init() など) 他の初期化とまとめて管理できる

もしあなたのAozoraEpub3版アプリの起動構成(例:MainFrame → AppConfig → MainPanel)が分かれば、 実際に挿入すべき場所を具体的に 指定できます。

どのクラスが最初に起動されるか教えてもらえますか?

あなた:

画像ファイルをできるだけ編集しないように扱うには

ChatGPT:

とても良い方針です。 「画像ファイルをできるだけ編集しない(=破壊せず、安全に扱う)」というのは、 EPUBや変換ツールのようなアプリでは非常に大切です。

以下では、Javaで画像を読み取って利用する際に“元ファイルを一切変更しない”ように扱う具体的な指針をまとめます。 AozoraEpub3や同系統の変換処理にそのまま適用できる考え方です。

最も基本的ですが重要です。

try (InputStream is = new BufferedInputStream(new FileInputStream(file))) {
    BufferedImage img = ImageIO.read(is);
}

try (InputStream is = new BufferedInputStream(new FileInputStream(file))) {
    BufferedImage img = ImageIO.read(is);
}
  • FileInputStream は読み込み専用。
  • BufferedInputStream に包むと OS のキャッシュ効率も良く、速度も安定。
  • ImageIO.read() はストリームを読み取るだけで、ファイル自体には一切書き込まない。

FileInputStream は読み込み専用。

BufferedInputStream に包むと OS のキャッシュ効率も良く、速度も安定。

ImageIO.read() はストリームを読み取るだけで、ファイル自体には一切書き込まない。

🔸ポイント: ファイル操作系(例:RandomAccessFile, FileOutputStream)を使わないこと。 読み込み時に 書き込み可能モードを開かない のが安全の第一歩です。

Javaの BufferedImage はメモリ上のデータを直接操作できてしまうため、 後で誤って上書き保存してしまう危険があります。

加工が必要な場合は、常に別インスタンスに描画して処理 します。

BufferedImage original = ImageIO.read(is);
BufferedImage copy = new BufferedImage(
    original.getWidth(), original.getHeight(), BufferedImage.TYPE_INT_ARGB);
Graphics2D g = copy.createGraphics();
g.drawImage(original, 0, 0, null);
g.dispose();

BufferedImage original = ImageIO.read(is);
BufferedImage copy = new BufferedImage(
    original.getWidth(), original.getHeight(), BufferedImage.TYPE_INT_ARGB);
Graphics2D g = copy.createGraphics();
g.drawImage(original, 0, 0, null);
g.dispose();

こうすると original は完全に読み取り専用として扱えます。 コピーした画像を変形・リサイズ・トリミングに使用します。

加工結果を保存する場合、 元ファイルと同じパス・ファイル名で上書き保存しないようにします。

File output = new File(destDir, srcFile.getName());
if (output.equals(srcFile)) {
    // 安全のため別名に変更
    output = new File(destDir, "converted_" + srcFile.getName());
}
ImageIO.write(copy, "png", output);

File output = new File(destDir, srcFile.getName());
if (output.equals(srcFile)) {
    // 安全のため別名に変更
    output = new File(destDir, "converted_" + srcFile.getName());
}
ImageIO.write(copy, "png", output);

上書きを防ぐことで、元画像の再利用・再処理が容易になります。

変換やサムネイル生成の中間データは、 ファイルシステムに直接書かずに メモリ上で済ませる か、 一時ファイルを java.nio.file.Files.createTempFile() で生成します。

Path temp = Files.createTempFile("img_", ".png");
ImageIO.write(image, "png", temp.toFile());

Path temp = Files.createTempFile("img_", ".png");
ImageIO.write(image, "png", temp.toFile());

OSが自動的に一時ディレクトリを管理するため、 永続的な破壊は起きません。

ImageIO プラグインは基本的に読み込み専用でも使えます。 ただしライブラリによっては「デコード中に書き込みを試みる」実装があることがあります。 以下のように、入力ストリームから直接読み込む方式なら安全です。

try (InputStream is = new BufferedInputStream(new FileInputStream(file))) {
    ImageReader reader = ImageIO.getImageReadersByFormatName("webp").next();
    try (ImageInputStream iis = ImageIO.createImageInputStream(is)) {
        reader.setInput(iis, true); // true = 読み込み専用
        BufferedImage img = reader.read(0);
    }
}

try (InputStream is = new BufferedInputStream(new FileInputStream(file))) {
    ImageReader reader = ImageIO.getImageReadersByFormatName("webp").next();
    try (ImageInputStream iis = ImageIO.createImageInputStream(is)) {
        reader.setInput(iis, true); // true = 読み込み専用
        BufferedImage img = reader.read(0);
    }
}

✅ setInput(iis, true) の第2引数を true にすることで 「読み込み専用モード」で安全にデコードされます。

例外処理は常に try-with-resources で閉じるようにします。

try (InputStream is = new FileInputStream(file)) {
    BufferedImage image = ImageIO.read(is);
} catch (IOException e) {
    System.err.println("Failed to read image: " + file);
}

try (InputStream is = new FileInputStream(file)) {
    BufferedImage image = ImageIO.read(is);
} catch (IOException e) {
    System.err.println("Failed to read image: " + file);
}

try-with-resources を使えば、例外発生時にもファイルハンドルが自動で閉じられ、 OSがファイルをロックしたままになるのを防げます。

JPEG や WebP のように「再圧縮を伴う形式」に変換すると 画質が劣化したり、EXIF情報が失われます。

対策:

  • 出力形式はなるべく 非圧縮(PNG)や可逆圧縮(WebP Lossless) にする
  • 元画像を コピーしてから 変換する
  • EXIFなどのメタデータを必要に応じて保持する

出力形式はなるべく 非圧縮(PNG)や可逆圧縮(WebP Lossless) にする

元画像を コピーしてから 変換する

EXIFなどのメタデータを必要に応じて保持する

配布環境でさらに安全を確保したい場合は:

  • 処理対象のフォルダを読み取り専用にする
  • ファイルを File.setReadOnly() でロックする
  • 一時作業フォルダを AppData や Temp に分離する

処理対象のフォルダを読み取り専用にする

ファイルを File.setReadOnly() でロックする

一時作業フォルダを AppData や Temp に分離する

目的 方法
ファイルを壊さない 読み込み専用ストリーム (FileInputStream) を使う
加工は別バッファで BufferedImage copy = new BufferedImage(...)
上書きを防ぐ 出力ファイル名を変える
一時変換 Files.createTempFile() を使う
安全な読込 setInput(iis, true)
ライブラリ管理 ImageIO.scanForPlugins() を先に呼ぶ

もしよければ、 あなたの getImage() と ImageUtils.readImage() をベースに、 「元画像を絶対に変更しない安全版」 として 完全な統合コードを生成できます。

作成しましょうか?

あなた:

webpの出力品質の変化させるには

ChatGPT:

とても良い質問です。 webp-imageio(またはそれと互換性のある com.luciad.imageio.webp 系プラグイン)を使う場合、 WebP出力の品質(圧縮率)を制御するには、ImageWriteParam を適切に設定する必要があります。

以下で、

  • 品質を変更する方法(lossy/lossless両対応)
  • 注意点 を順に説明します。

品質を変更する方法(lossy/lossless両対応)

注意点 を順に説明します。

import javax.imageio.*;
import javax.imageio.stream.*;
import java.awt.image.*;
import java.io.*;
import java.util.Iterator;

public class WebPWriter {
    public static void writeWebP(BufferedImage image, File outputFile, float quality) throws IOException {
        Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("webp");
        if (!writers.hasNext()) {
            throw new IllegalStateException("No WebP ImageWriter found. Make sure webp-imageio is loaded.");
        }

        ImageWriter writer = writers.next();
        ImageWriteParam param = writer.getDefaultWriteParam();

        // lossy モード(品質を指定)
        if (param.canWriteCompressed()) {
            param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
            param.setCompressionQuality(quality); // 0.0f~1.0f(高いほど高品質・低圧縮)
        }

        try (ImageOutputStream ios = ImageIO.createImageOutputStream(outputFile)) {
            writer.setOutput(ios);
            writer.write(null, new IIOImage(image, null, null), param);
        } finally {
            writer.dispose();
        }
    }

    public static void main(String[] args) throws IOException {
        BufferedImage img = ImageIO.read(new File("test.png"));
        writeWebP(img, new File("test_q80.webp"), 0.8f);
    }
}

import javax.imageio.*;
import javax.imageio.stream.*;
import java.awt.image.*;
import java.io.*;
import java.util.Iterator;

public class WebPWriter {
    public static void writeWebP(BufferedImage image, File outputFile, float quality) throws IOException {
        Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("webp");
        if (!writers.hasNext()) {
            throw new IllegalStateException("No WebP ImageWriter found. Make sure webp-imageio is loaded.");
        }

        ImageWriter writer = writers.next();
        ImageWriteParam param = writer.getDefaultWriteParam();

        // lossy モード(品質を指定)
        if (param.canWriteCompressed()) {
            param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
            param.setCompressionQuality(quality); // 0.0f~1.0f(高いほど高品質・低圧縮)
        }

        try (ImageOutputStream ios = ImageIO.createImageOutputStream(outputFile)) {
            writer.setOutput(ios);
            writer.write(null, new IIOImage(image, null, null), param);
        } finally {
            writer.dispose();
        }
    }

    public static void main(String[] args) throws IOException {
        BufferedImage img = ImageIO.read(new File("test.png"));
        writeWebP(img, new File("test_q80.webp"), 0.8f);
    }
}
意味 備考
0.0f 最低品質(最高圧縮率) 画質は悪いがサイズが小さい
0.75f 通常のバランス Web用途に多い
0.9f 高品質 ほぼ視覚劣化なし
1.0f 非常に高品質 サイズ大きめ、ほぼロスレスに近い

WebPの「品質」は JPEGの setCompressionQuality() と似ていますが、 エンコーダが内部的にPSNR最適化を行うため、 同じ値でも実際の画質・サイズは環境によって若干異なります。

webp-imageio の場合、ImageWriteParam に追加パラメータを指定することで 完全ロスレスモード に切り替えることも可能です。

param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
param.setCompressionType("Lossless"); // または "lossless"

param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
param.setCompressionType("Lossless"); // または "lossless"

これにより品質指定 (setCompressionQuality) は無視され、 可逆圧縮(PNGに近い)で保存されます。

どの compressionType が利用可能かを確認するには:

String[] types = param.getCompressionTypes();
if (types != null) {
    for (String t : types) {
        System.out.println("Available compression type: " + t);
    }
}

String[] types = param.getCompressionTypes();
if (types != null) {
    for (String t : types) {
        System.out.println("Available compression type: " + t);
    }
}

多くの WebP 実装では

  • "Lossy"
  • "Lossless" の2種類が返されます。

"Lossy"

"Lossless" の2種類が返されます。

注意点 内容
🔸 ImageIO.scanForPlugins() 先に呼び出さないと WebP Writer が検出されない場合あり。
🔸 quality は 0.0~1.0 範囲外だと IllegalArgumentException。
🔸 ロスレス時 setCompressionQuality() の指定は無視される。
🔸 メモリ消費 高品質設定はメモリ使用量が増える。
🔸 透過画像 RGBA画像を扱う場合は BufferedImage.TYPE_INT_ARGB を使うこと。
float[] qualities = {0.3f, 0.6f, 0.9f};
for (float q : qualities) {
    File out = new File(String.format("output_q%.0f.webp", q * 100));
    writeWebP(image, out, q);
}

float[] qualities = {0.3f, 0.6f, 0.9f};
for (float q : qualities) {
    File out = new File(String.format("output_q%.0f.webp", q * 100));
    writeWebP(image, out, q);
}

これで 30%, 60%, 90% 品質の WebP が生成され、 サイズと画質を比較できます。

目的 方法
圧縮率を下げて高画質に param.setCompressionQuality(1.0f)
高圧縮で軽量化 param.setCompressionQuality(0.3f)
可逆圧縮にする param.setCompressionType("Lossless")
プラグインが検出されない ImageIO.scanForPlugins() を実行

ご希望があれば、 🔹 「PNG → WebP 変換(品質指定付き)」を行う 🔹 「複数ファイルを一括処理するCLIツール」 🔹 「JavaFXやSwingでGUIプレビュー付き変換」

のいずれかの完全コードを出力することもできます。 どのパターンを希望しますか?

あなた:

webp対応させて

/** 画像を出力 マージン指定があればカット
 * //@param margin カットするピクセル数(left, top, right, bottom) */
static private void _writeImage(ZipArchiveOutputStream zos, BufferedImage srcImage, String ext, float jpegQuality) throws IOException
{
    if ("png".equals(ext)) {
        /*//PNGEncoder kindlegenでエラーになるのと色が反映されない
        PngEncoder pngEncoder = new PngEncoder();
        int pngColorType = PngEncoder.COLOR_TRUECOLOR;
        switch (srcImage.getType()) {
        case BufferedImage.TYPE_BYTE_BINARY:
            pngColorType = PngEncoder.COLOR_INDEXED; break;
        case BufferedImage.TYPE_BYTE_INDEXED:
            pngColorType = PngEncoder.COLOR_INDEXED; break;
        case BufferedImage.TYPE_BYTE_GRAY:
            pngColorType = PngEncoder.COLOR_GRAYSCALE; break;
        }
        pngEncoder.setColorType(pngColorType);
        pngEncoder.setCompression(PngEncoder.BEST_COMPRESSION);
        pngEncoder.setIndexedColorMode(PngEncoder.INDEXED_COLORS_AUTO);
        pngEncoder.encode(srcImage, zos);
        */
        //ImageIO.write(srcImage, "PNG", zos);
        ImageWriter imageWriter = getPngImageWriter();
        imageWriter.setOutput(ImageIO.createImageOutputStream(zos));
        imageWriter.write(srcImage);
    } else if ("jpeg".equals(ext) || "jpg".equals(ext)) {
        ImageWriter imageWriter = getJpegImageWriter();
        imageWriter.setOutput(ImageIO.createImageOutputStream(zos));
        ImageWriteParam iwp = imageWriter.getDefaultWriteParam();
        if (iwp.canWriteCompressed()) {
            try {
                iwp.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
                iwp.setCompressionQuality(jpegQuality);
                imageWriter.write(null, new IIOImage(srcImage, null, null), iwp);
            } catch (Exception e) { e.printStackTrace(); }
        } else {
            imageWriter.write(srcImage);
        }
    } else {
        ImageIO.write(srcImage, ext, zos);
    }
    zos.flush();
}

ChatGPT:

良いですね👍 この _writeImage() に WebP対応 を加えるのは比較的簡単です。 既存のPNG/JPEG処理をほぼ維持したまま、WebP用の分岐を1つ追加すればOKです。

/** 画像を出力 マージン指定があればカット
 * //@param margin カットするピクセル数(left, top, right, bottom) */
static private void _writeImage(ZipArchiveOutputStream zos, BufferedImage srcImage, String ext, float jpegQuality) throws IOException
{
    ext = ext.toLowerCase();

    if ("png".equals(ext)) {
        ImageWriter imageWriter = getPngImageWriter();
        imageWriter.setOutput(ImageIO.createImageOutputStream(zos));
        imageWriter.write(srcImage);

    } else if ("jpeg".equals(ext) || "jpg".equals(ext)) {
        ImageWriter imageWriter = getJpegImageWriter();
        imageWriter.setOutput(ImageIO.createImageOutputStream(zos));
        ImageWriteParam iwp = imageWriter.getDefaultWriteParam();
        if (iwp.canWriteCompressed()) {
            try {
                iwp.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
                iwp.setCompressionQuality(jpegQuality);
                imageWriter.write(null, new IIOImage(srcImage, null, null), iwp);
            } catch (Exception e) {
                e.printStackTrace();
            }
        } else {
            imageWriter.write(srcImage);
        }

    } else if ("webp".equals(ext)) {
        // --- WebP出力処理 ---
        ImageWriter imageWriter = null;
        Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("webp");
        if (writers.hasNext()) {
            imageWriter = writers.next();
        } else {
            throw new IOException("WebP writer not found. Make sure webp-imageio is in classpath.");
        }

        imageWriter.setOutput(ImageIO.createImageOutputStream(zos));
        ImageWriteParam param = imageWriter.getDefaultWriteParam();

        if (param.canWriteCompressed()) {
            param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
            param.setCompressionType("Lossy"); // "Lossless" も可
            param.setCompressionQuality(jpegQuality); // 0.0f~1.0f
        }

        imageWriter.write(null, new IIOImage(srcImage, null, null), param);
        imageWriter.dispose();

    } else {
        ImageIO.write(srcImage, ext, zos);
    }

    zos.flush();
}

/** 画像を出力 マージン指定があればカット
 * //@param margin カットするピクセル数(left, top, right, bottom) */
static private void _writeImage(ZipArchiveOutputStream zos, BufferedImage srcImage, String ext, float jpegQuality) throws IOException
{
    ext = ext.toLowerCase();

    if ("png".equals(ext)) {
        ImageWriter imageWriter = getPngImageWriter();
        imageWriter.setOutput(ImageIO.createImageOutputStream(zos));
        imageWriter.write(srcImage);

    } else if ("jpeg".equals(ext) || "jpg".equals(ext)) {
        ImageWriter imageWriter = getJpegImageWriter();
        imageWriter.setOutput(ImageIO.createImageOutputStream(zos));
        ImageWriteParam iwp = imageWriter.getDefaultWriteParam();
        if (iwp.canWriteCompressed()) {
            try {
                iwp.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
                iwp.setCompressionQuality(jpegQuality);
                imageWriter.write(null, new IIOImage(srcImage, null, null), iwp);
            } catch (Exception e) {
                e.printStackTrace();
            }
        } else {
            imageWriter.write(srcImage);
        }

    } else if ("webp".equals(ext)) {
        // --- WebP出力処理 ---
        ImageWriter imageWriter = null;
        Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("webp");
        if (writers.hasNext()) {
            imageWriter = writers.next();
        } else {
            throw new IOException("WebP writer not found. Make sure webp-imageio is in classpath.");
        }

        imageWriter.setOutput(ImageIO.createImageOutputStream(zos));
        ImageWriteParam param = imageWriter.getDefaultWriteParam();

        if (param.canWriteCompressed()) {
            param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
            param.setCompressionType("Lossy"); // "Lossless" も可
            param.setCompressionQuality(jpegQuality); // 0.0f~1.0f
        }

        imageWriter.write(null, new IIOImage(srcImage, null, null), param);
        imageWriter.dispose();

    } else {
        ImageIO.write(srcImage, ext, zos);
    }

    zos.flush();
}

webp-imageio(または luciad-webp-imageio)がクラスパスに必要です。

<dependency>
  <groupId>com.twelvemonkeys.imageio</groupId>
  <artifactId>imageio-webp</artifactId>
  <version>3.10.1</version>
</dependency>

<dependency>
  <groupId>com.twelvemonkeys.imageio</groupId>
  <artifactId>imageio-webp</artifactId>
  <version>3.10.1</version>
</dependency>

または:

<dependency>
  <groupId>com.luciad</groupId>
  <artifactId>luciad-webp-imageio</artifactId>
  <version>1.4.0</version>
</dependency>

<dependency>
  <groupId>com.luciad</groupId>
  <artifactId>luciad-webp-imageio</artifactId>
  <version>1.4.0</version>
</dependency>
項目 内容
🔸 ImageIO.scanForPlugins() アプリ起動時に1度呼んでおくと確実に検出される
🔸 jpegQuality WebPでも 0.0f~1.0f の範囲で圧縮率指定可能
🔸 Lossless 出力 param.setCompressionType("Lossless"); に切り替え
🔸 ストリーム zos は既に呼び出し元が管理しているので、close() せずに flush() のみ
🔸 RGBA画像 WebPは透過対応済み(ただしLossy時はやや非可逆)

もし main() がなくても、最初のどこか(例えば static {} ブロックなど)で呼んでおくと安心です:

static {
    try {
        ImageIO.scanForPlugins();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

static {
    try {
        ImageIO.scanForPlugins();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

この修正版は、

  • .webp を出力形式として指定したときに自動でWebP出力
  • JPEGと同じように jpegQuality が品質に反映される
  • 既存の呼び出しコードに変更不要

.webp を出力形式として指定したときに自動でWebP出力

JPEGと同じように jpegQuality が品質に反映される

既存の呼び出しコードに変更不要

です。

必要であれば「PNG/JPEG/WebPを自動選択してサイズ比較」や「ロスレスWebPに切り替える」バージョンも提示できます。 どちらを希望しますか?

あなた:

同じようにして

static private ImageWriter getPngImageWriter()
{
    if (pngImageWriter != null) return pngImageWriter;
    Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("png");
    pngImageWriter = writers.next();
    //jai-imageioのpngの挙動がおかしいのでインストールされていても使わない
    if (writers.hasNext() && pngImageWriter.getClass().getName().endsWith("CLibPNGImageWriter")) pngImageWriter = writers.next();
    return pngImageWriter;
}

static private ImageWriter getJpegImageWriter()
{
    if (jpegImageWriter!= null) return jpegImageWriter;
    Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("jpg");
    jpegImageWriter = writers.next();
    return jpegImageWriter;
}

ChatGPT:

完璧です。 この構成なら、getWebpImageWriter() を同じパターンで安全に追加できます。 WebPは追加プラグイン(webp-imageioやluciad-webp-imageio)で提供されるため、 存在確認とエラーハンドリングを入れておくのがポイントです。

static private ImageWriter webpImageWriter;

/** WebP ImageWriter を取得 */
static private ImageWriter getWebpImageWriter() {
    if (webpImageWriter != null) return webpImageWriter;

    Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("webp");

    if (!writers.hasNext()) {
        // プラグイン未登録エラー
        throw new IllegalStateException(
            "WebP ImageWriter not found. Make sure webp-imageio (or luciad-webp-imageio) is in the classpath, and call ImageIO.scanForPlugins()."
        );
    }

    webpImageWriter = writers.next();

    // 念のためクラス名チェック(複数プラグイン共存時)
    String className = webpImageWriter.getClass().getName();
    System.out.println("Using WebP writer: " + className);

    return webpImageWriter;
}

static private ImageWriter webpImageWriter;

/** WebP ImageWriter を取得 */
static private ImageWriter getWebpImageWriter() {
    if (webpImageWriter != null) return webpImageWriter;

    Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("webp");

    if (!writers.hasNext()) {
        // プラグイン未登録エラー
        throw new IllegalStateException(
            "WebP ImageWriter not found. Make sure webp-imageio (or luciad-webp-imageio) is in the classpath, and call ImageIO.scanForPlugins()."
        );
    }

    webpImageWriter = writers.next();

    // 念のためクラス名チェック(複数プラグイン共存時)
    String className = webpImageWriter.getClass().getName();
    System.out.println("Using WebP writer: " + className);

    return webpImageWriter;
}

前回の _writeImage() 内のこの部分を:

ImageWriter imageWriter = null;
Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("webp");
if (writers.hasNext()) {
    imageWriter = writers.next();
} else {
    throw new IOException("WebP writer not found. Make sure webp-imageio is in classpath.");
}

ImageWriter imageWriter = null;
Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("webp");
if (writers.hasNext()) {
    imageWriter = writers.next();
} else {
    throw new IOException("WebP writer not found. Make sure webp-imageio is in classpath.");
}

👇 次のように簡潔に置き換えられます。

ImageWriter imageWriter = getWebpImageWriter();

ImageWriter imageWriter = getWebpImageWriter();

このようにしておくと:

  • ImageWriter の再取得コストを回避(高速化)
  • スレッドセーフではないが、基本的に単一スレッドI/Oなら安全
  • メモリリーク防止(dispose() は呼び出し側で実行)

ImageWriter の再取得コストを回避(高速化)

スレッドセーフではないが、基本的に単一スレッドI/Oなら安全

メモリリーク防止(dispose() は呼び出し側で実行)

もし並列書き込みを行うなら、getWebpImageWriter() 内で都度 writers.next() を返すようにする方が安全です。

最終的に以下のように3種類揃えておくと美しいです。

static private ImageWriter pngImageWriter;
static private ImageWriter jpegImageWriter;
static private ImageWriter webpImageWriter;

static private ImageWriter getPngImageWriter() {
    if (pngImageWriter != null) return pngImageWriter;
    Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("png");
    pngImageWriter = writers.next();
    if (writers.hasNext() && pngImageWriter.getClass().getName().endsWith("CLibPNGImageWriter"))
        pngImageWriter = writers.next();
    return pngImageWriter;
}

static private ImageWriter getJpegImageWriter() {
    if (jpegImageWriter != null) return jpegImageWriter;
    Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("jpg");
    jpegImageWriter = writers.next();
    return jpegImageWriter;
}

static private ImageWriter getWebpImageWriter() {
    if (webpImageWriter != null) return webpImageWriter;
    Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("webp");
    if (!writers.hasNext()) {
        throw new IllegalStateException("WebP writer not found. Make sure webp-imageio or luciad-webp-imageio is on the classpath.");
    }
    webpImageWriter = writers.next();
    return webpImageWriter;
}

static private ImageWriter pngImageWriter;
static private ImageWriter jpegImageWriter;
static private ImageWriter webpImageWriter;

static private ImageWriter getPngImageWriter() {
    if (pngImageWriter != null) return pngImageWriter;
    Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("png");
    pngImageWriter = writers.next();
    if (writers.hasNext() && pngImageWriter.getClass().getName().endsWith("CLibPNGImageWriter"))
        pngImageWriter = writers.next();
    return pngImageWriter;
}

static private ImageWriter getJpegImageWriter() {
    if (jpegImageWriter != null) return jpegImageWriter;
    Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("jpg");
    jpegImageWriter = writers.next();
    return jpegImageWriter;
}

static private ImageWriter getWebpImageWriter() {
    if (webpImageWriter != null) return webpImageWriter;
    Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("webp");
    if (!writers.hasNext()) {
        throw new IllegalStateException("WebP writer not found. Make sure webp-imageio or luciad-webp-imageio is on the classpath.");
    }
    webpImageWriter = writers.next();
    return webpImageWriter;
}

必要であればこの getWebpImageWriter() をスレッドセーフにした(複数スレッド対応)版も出せます。 マルチスレッドで出力処理していますか?

あなた:

webp対応させて

/** 拡張子修正 大文字小文字は3パターンのみ */
public String correctExt(String srcImageFileName) {
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    //拡張子修正
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".png");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".jpg");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".jpeg");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".gif");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;

    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".PNG");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".JPG");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".JPEG");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".GIF");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;

    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".Png");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".Jpg");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".Jpeg");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".Gif");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;

    return null;
}

ChatGPT:

良いですね。 この correctExt() は既存の画像拡張子(PNG/JPEG/GIF)の大文字小文字ゆらぎを吸収する処理なので、 WebPを追加するには非常にシンプルに1行(または3パターン)追加すればOKです。

以下は既存構造をほぼ変えず、変更最小限で .webp, .WEBP, .Webp を追加したバージョンです。

/** 拡張子修正 大文字小文字は3パターンのみ */
public String correctExt(String srcImageFileName) {
    if (this.hasImage(srcImageFileName)) return srcImageFileName;

    // 拡張子修正(小文字)
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".png");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".jpg");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".jpeg");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".gif");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".webp");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;

    // 拡張子修正(大文字)
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".PNG");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".JPG");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".JPEG");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".GIF");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".WEBP");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;

    // 拡張子修正(先頭のみ大文字)
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".Png");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".Jpg");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".Jpeg");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".Gif");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".Webp");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;

    return null;
}

/** 拡張子修正 大文字小文字は3パターンのみ */
public String correctExt(String srcImageFileName) {
    if (this.hasImage(srcImageFileName)) return srcImageFileName;

    // 拡張子修正(小文字)
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".png");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".jpg");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".jpeg");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".gif");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".webp");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;

    // 拡張子修正(大文字)
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".PNG");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".JPG");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".JPEG");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".GIF");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".WEBP");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;

    // 拡張子修正(先頭のみ大文字)
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".Png");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".Jpg");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".Jpeg");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".Gif");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".Webp");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;

    return null;
}
追加行 内容
小文字 .webp PNG/JPEG/GIFと同列に追加
大文字 .WEBP 大文字ブロックに追加
先頭のみ大文字 .Webp CamelCaseブロックに追加

このメソッドは、

  • 実ファイル (File)
  • Zip/Rar内のエントリ を共通的に確認しているはずなので、拡張子修正後に呼ぶだけでOKです。 WebPも既に ImageIO で読み込める状態になっている(ImageIO.scanForPlugins() 済み)なら問題ありません。

実ファイル (File)

Zip/Rar内のエントリ を共通的に確認しているはずなので、拡張子修正後に呼ぶだけでOKです。 WebPも既に ImageIO で読み込める状態になっている(ImageIO.scanForPlugins() 済み)なら問題ありません。

もしコード量を減らしたい場合は、 拡張子候補を配列化してループ処理にまとめることもできます。

public String correctExt(String srcImageFileName) {
    String[] exts = {
        "png", "jpg", "jpeg", "gif", "webp",
        "PNG", "JPG", "JPEG", "GIF", "WEBP",
        "Png", "Jpg", "Jpeg", "Gif", "Webp"
    };
    for (String ext : exts) {
        String name = srcImageFileName.replaceFirst("\\.\\w+$", "." + ext);
        if (this.hasImage(name)) return name;
    }
    return null;
}

public String correctExt(String srcImageFileName) {
    String[] exts = {
        "png", "jpg", "jpeg", "gif", "webp",
        "PNG", "JPG", "JPEG", "GIF", "WEBP",
        "Png", "Jpg", "Jpeg", "Gif", "Webp"
    };
    for (String ext : exts) {
        String name = srcImageFileName.replaceFirst("\\.\\w+$", "." + ext);
        if (this.hasImage(name)) return name;
    }
    return null;
}

このほうが保守しやすく、将来 .avif などを追加するときも1行で済みます。

どちらの書式(元の形を保ったまま/配列ループ化)で統一したいですか?

あなた:

webp対応させて

/** zip内の画像情報をすべて読み込み
 * @throws IOException */
public void loadZipImageInfos(File srcFile, boolean addFileName) throws IOException
{
    try (ZipArchiveInputStream zis = new ZipArchiveInputStream(new BufferedInputStream(new FileInputStream(srcFile), 65536), "MS932", false)) {
        ArchiveEntry entry;
        int idx = 0;
        while ((entry = zis.getNextEntry()) != null) {
            if (idx++ % 10 == 0) LogAppender.append(".");
            String entryName = entry.getName();
            String lowerName = entryName.toLowerCase();
            if (lowerName.endsWith(".png") || lowerName.endsWith(".jpg") || lowerName.endsWith(".jpeg") || lowerName.endsWith(".gif")) {
                ImageInfo imageInfo = null;
                try {
                    imageInfo = ImageInfo.getImageInfo(zis);
                } catch (Exception e) {
                    LogAppender.error("画像が読み込めませんでした: " + srcFile.getPath());
                    e.printStackTrace();
                }
                if (imageInfo != null) {
                    this.imageFileInfos.put(entryName, imageInfo);
                    if (addFileName) this.addImageFileName(entryName);
                }
            }
        }
    } finally {
        LogAppender.println();
    }
}

ChatGPT:

webp形式の画像にも対応させる場合、既存コードの構造をできるだけ変えずに拡張子チェック部分を調整すればOKです。 以下はあなたのコードに最小限の変更を加えた WebP対応版 です。

/** zip内の画像情報をすべて読み込み
 * @throws IOException */
public void loadZipImageInfos(File srcFile, boolean addFileName) throws IOException
{
    try (ZipArchiveInputStream zis = new ZipArchiveInputStream(
            new BufferedInputStream(new FileInputStream(srcFile), 65536), "MS932", false)) {

        ArchiveEntry entry;
        int idx = 0;

        while ((entry = zis.getNextEntry()) != null) {
            if (idx++ % 10 == 0) LogAppender.append(".");

            String entryName = entry.getName();
            String lowerName = entryName.toLowerCase();

            // ★ webp を追加
            if (lowerName.endsWith(".png") ||
                lowerName.endsWith(".jpg") ||
                lowerName.endsWith(".jpeg") ||
                lowerName.endsWith(".gif") ||
                lowerName.endsWith(".webp")) { // ← ここを追加

                ImageInfo imageInfo = null;
                try {
                    imageInfo = ImageInfo.getImageInfo(zis);
                } catch (Exception e) {
                    LogAppender.error("画像が読み込めませんでした: " + srcFile.getPath());
                    e.printStackTrace();
                }

                if (imageInfo != null) {
                    this.imageFileInfos.put(entryName, imageInfo);
                    if (addFileName) this.addImageFileName(entryName);
                }
            }
        }

    } finally {
        LogAppender.println();
    }
}

/** zip内の画像情報をすべて読み込み
 * @throws IOException */
public void loadZipImageInfos(File srcFile, boolean addFileName) throws IOException
{
    try (ZipArchiveInputStream zis = new ZipArchiveInputStream(
            new BufferedInputStream(new FileInputStream(srcFile), 65536), "MS932", false)) {

        ArchiveEntry entry;
        int idx = 0;

        while ((entry = zis.getNextEntry()) != null) {
            if (idx++ % 10 == 0) LogAppender.append(".");

            String entryName = entry.getName();
            String lowerName = entryName.toLowerCase();

            // ★ webp を追加
            if (lowerName.endsWith(".png") ||
                lowerName.endsWith(".jpg") ||
                lowerName.endsWith(".jpeg") ||
                lowerName.endsWith(".gif") ||
                lowerName.endsWith(".webp")) { // ← ここを追加

                ImageInfo imageInfo = null;
                try {
                    imageInfo = ImageInfo.getImageInfo(zis);
                } catch (Exception e) {
                    LogAppender.error("画像が読み込めませんでした: " + srcFile.getPath());
                    e.printStackTrace();
                }

                if (imageInfo != null) {
                    this.imageFileInfos.put(entryName, imageInfo);
                    if (addFileName) this.addImageFileName(entryName);
                }
            }
        }

    } finally {
        LogAppender.println();
    }
}
  1. .webp拡張子チェックを追加 既存の endsWith() チェックに .webp を加えるだけで対応可能です。 WebPの大文字・小文字(例: .WEBP)も許可したい場合は .toLowerCase() を既に使用しているためそのままでOKです。
  2. ImageIOでのWebP対応 この関数は画像データを解析するだけなので、 実際にWebP画像を読み取るには前もって

java コードをコピーする ImageIO.scanForPlugins();

を呼び出し、 webp-imageio(例: com.luciad.imageio.webp.WebPImageReaderSpi)が登録されている必要があります。

通常はアプリ起動時(main関数や静的初期化ブロック)で以下のように一度呼び出しておくと確実です:

java コードをコピーする static { ImageIO.scanForPlugins(); }

.webp拡張子チェックを追加 既存の endsWith() チェックに .webp を加えるだけで対応可能です。 WebPの大文字・小文字(例: .WEBP)も許可したい場合は .toLowerCase() を既に使用しているためそのままでOKです。

ImageIOでのWebP対応 この関数は画像データを解析するだけなので、 実際にWebP画像を読み取るには前もって

ImageIO.scanForPlugins();

ImageIO.scanForPlugins();

を呼び出し、 webp-imageio(例: com.luciad.imageio.webp.WebPImageReaderSpi)が登録されている必要があります。

通常はアプリ起動時(main関数や静的初期化ブロック)で以下のように一度呼び出しておくと確実です:

static {
    ImageIO.scanForPlugins();
}

static {
    ImageIO.scanForPlugins();
}

希望があれば、ImageInfo.getImageInfo(zis) が WebP に未対応だった場合のフォールバック(例: BufferedImage image = ImageIO.read(...) で強制読み込み)を組み込んだ堅牢版も提示できます。 作りたいのは「最小変更版」と「安全フォールバック付き版」どちらにしますか?

あなた:

webp対応させて

/** rar内の画像情報をすべて読み込み */
public void loadRarImageInfos(File srcFile, boolean addFileName) throws IOException, RarException
{
    try (Archive archive = new Archive(srcFile)) {
        int idx = 0;
        for (FileHeader fileHeader : archive.getFileHeaders()) {
            if (idx++ % 10 == 0) LogAppender.append(".");
            if (!fileHeader.isDirectory()) {
                String entryName = fileHeader.getFileName();
                entryName = entryName.replace('\\', '/');
                String lowerName = entryName.toLowerCase();
                if (lowerName.endsWith(".png") || lowerName.endsWith(".jpg") || lowerName.endsWith(".jpeg") || lowerName.endsWith(".gif")) {
                    ImageInfo imageInfo;
                    InputStream is = null;
                    try {
                        //読めない場合があるので一旦バイト配列に読み込み
                        ByteArrayOutputStream baos = new ByteArrayOutputStream();
                        archive.extractFile(fileHeader, baos);
                        baos.close();
                        is = new ByteArrayInputStream(baos.toByteArray());
                        imageInfo = ImageInfo.getImageInfo(is);
                        if (imageInfo != null) {
                            this.imageFileInfos.put(entryName, imageInfo);
                            if (addFileName) this.addImageFileName(entryName);
                        } else {
                            LogAppender.println();
                            LogAppender.error("画像が読み込めませんでした: " + entryName);
                        }
                    } catch (Exception e) {
                        LogAppender.println();
                        LogAppender.error("画像が読み込めませんでした: " + entryName);
                        e.printStackTrace();
                    } finally {
                        if (is != null) is.close();
                    }
                }
            }
        }
    } finally {
        LogAppender.println();
    }
}

ChatGPT:

良いですね 👍 この RAR 内画像読み込み処理も、ZIP のときと同様に .webp を追加すれば対応できます。 既存の構造を崩さず、最小限の修正で WebP を扱えるようにした完全版を以下に示します。

/** rar内の画像情報をすべて読み込み */
public void loadRarImageInfos(File srcFile, boolean addFileName) throws IOException, RarException
{
    try (Archive archive = new Archive(srcFile)) {
        int idx = 0;
        for (FileHeader fileHeader : archive.getFileHeaders()) {
            if (idx++ % 10 == 0) LogAppender.append(".");
            if (!fileHeader.isDirectory()) {
                String entryName = fileHeader.getFileName();
                entryName = entryName.replace('\\', '/');
                String lowerName = entryName.toLowerCase();

                // ★ WebP対応(拡張子チェックを追加)
                if (lowerName.endsWith(".png") ||
                    lowerName.endsWith(".jpg") ||
                    lowerName.endsWith(".jpeg") ||
                    lowerName.endsWith(".gif") ||
                    lowerName.endsWith(".webp")) { // ←ここを追加

                    ImageInfo imageInfo;
                    InputStream is = null;
                    try {
                        // 読めない場合があるので一旦バイト配列に読み込み
                        ByteArrayOutputStream baos = new ByteArrayOutputStream();
                        archive.extractFile(fileHeader, baos);
                        baos.close();

                        is = new ByteArrayInputStream(baos.toByteArray());
                        imageInfo = ImageInfo.getImageInfo(is);

                        if (imageInfo != null) {
                            this.imageFileInfos.put(entryName, imageInfo);
                            if (addFileName) this.addImageFileName(entryName);
                        } else {
                            LogAppender.println();
                            LogAppender.error("画像が読み込めませんでした: " + entryName);
                        }

                    } catch (Exception e) {
                        LogAppender.println();
                        LogAppender.error("画像が読み込めませんでした: " + entryName);
                        e.printStackTrace();
                    } finally {
                        if (is != null) is.close();
                    }
                }
            }
        }
    } finally {
        LogAppender.println();
    }
}

/** rar内の画像情報をすべて読み込み */
public void loadRarImageInfos(File srcFile, boolean addFileName) throws IOException, RarException
{
    try (Archive archive = new Archive(srcFile)) {
        int idx = 0;
        for (FileHeader fileHeader : archive.getFileHeaders()) {
            if (idx++ % 10 == 0) LogAppender.append(".");
            if (!fileHeader.isDirectory()) {
                String entryName = fileHeader.getFileName();
                entryName = entryName.replace('\\', '/');
                String lowerName = entryName.toLowerCase();

                // ★ WebP対応(拡張子チェックを追加)
                if (lowerName.endsWith(".png") ||
                    lowerName.endsWith(".jpg") ||
                    lowerName.endsWith(".jpeg") ||
                    lowerName.endsWith(".gif") ||
                    lowerName.endsWith(".webp")) { // ←ここを追加

                    ImageInfo imageInfo;
                    InputStream is = null;
                    try {
                        // 読めない場合があるので一旦バイト配列に読み込み
                        ByteArrayOutputStream baos = new ByteArrayOutputStream();
                        archive.extractFile(fileHeader, baos);
                        baos.close();

                        is = new ByteArrayInputStream(baos.toByteArray());
                        imageInfo = ImageInfo.getImageInfo(is);

                        if (imageInfo != null) {
                            this.imageFileInfos.put(entryName, imageInfo);
                            if (addFileName) this.addImageFileName(entryName);
                        } else {
                            LogAppender.println();
                            LogAppender.error("画像が読み込めませんでした: " + entryName);
                        }

                    } catch (Exception e) {
                        LogAppender.println();
                        LogAppender.error("画像が読み込めませんでした: " + entryName);
                        e.printStackTrace();
                    } finally {
                        if (is != null) is.close();
                    }
                }
            }
        }
    } finally {
        LogAppender.println();
    }
}
  1. .webp拡張子の追加だけで十分

ImageIO.scanForPlugins() を呼び出して webp-imageio が有効になっていれば、 ImageInfo.getImageInfo() が内部で WebP を正しく解析できます。 (Luciad の WebPImageReaderSpi を利用) 2. ImageIO.scanForPlugins() を呼び出して webp-imageio が有効になっていれば、 ImageInfo.getImageInfo() が内部で WebP を正しく解析できます。 (Luciad の WebPImageReaderSpi を利用) 3. ImageIO.scanForPlugins() の呼び出し場所

RAR / ZIP 共通で一度だけ呼べばOK。 通常はアプリ起動時や静的初期化ブロックに入れます。

java コードをコピーする static { ImageIO.scanForPlugins(); }

  1. RAR / ZIP 共通で一度だけ呼べばOK。 通常はアプリ起動時や静的初期化ブロックに入れます。

java コードをコピーする static { ImageIO.scanForPlugins(); }

  1. WebP非対応環境への安全策(任意)

ImageInfo.getImageInfo() が WebP を認識できない場合、 代替として ImageIO.read() にフォールバックするコードを追加できます。 例:

java コードをコピーする if (imageInfo == null) { BufferedImage img = ImageIO.read(is); if (img != null) { imageInfo = new ImageInfo(img.getWidth(), img.getHeight()); } }

ただしメモリ負荷が高くなるため、WebPが確実にサポートされる環境なら不要です。 6. ImageInfo.getImageInfo() が WebP を認識できない場合、 代替として ImageIO.read() にフォールバックするコードを追加できます。 例:

java コードをコピーする if (imageInfo == null) { BufferedImage img = ImageIO.read(is); if (img != null) { imageInfo = new ImageInfo(img.getWidth(), img.getHeight()); } }

ただしメモリ負荷が高くなるため、WebPが確実にサポートされる環境なら不要です。

.webp拡張子の追加だけで十分

  • ImageIO.scanForPlugins() を呼び出して webp-imageio が有効になっていれば、 ImageInfo.getImageInfo() が内部で WebP を正しく解析できます。 (Luciad の WebPImageReaderSpi を利用)

ImageIO.scanForPlugins() を呼び出して webp-imageio が有効になっていれば、 ImageInfo.getImageInfo() が内部で WebP を正しく解析できます。 (Luciad の WebPImageReaderSpi を利用)

ImageIO.scanForPlugins() の呼び出し場所

  • RAR / ZIP 共通で一度だけ呼べばOK。 通常はアプリ起動時や静的初期化ブロックに入れます。

java コードをコピーする static { ImageIO.scanForPlugins(); }

RAR / ZIP 共通で一度だけ呼べばOK。 通常はアプリ起動時や静的初期化ブロックに入れます。

static {
    ImageIO.scanForPlugins();
}

static {
    ImageIO.scanForPlugins();
}

WebP非対応環境への安全策(任意)

  • ImageInfo.getImageInfo() が WebP を認識できない場合、 代替として ImageIO.read() にフォールバックするコードを追加できます。 例:

java コードをコピーする if (imageInfo == null) { BufferedImage img = ImageIO.read(is); if (img != null) { imageInfo = new ImageInfo(img.getWidth(), img.getHeight()); } }

ただしメモリ負荷が高くなるため、WebPが確実にサポートされる環境なら不要です。

ImageInfo.getImageInfo() が WebP を認識できない場合、 代替として ImageIO.read() にフォールバックするコードを追加できます。 例:

if (imageInfo == null) {
    BufferedImage img = ImageIO.read(is);
    if (img != null) {
        imageInfo = new ImageInfo(img.getWidth(), img.getHeight());
    }
}

if (imageInfo == null) {
    BufferedImage img = ImageIO.read(is);
    if (img != null) {
        imageInfo = new ImageInfo(img.getWidth(), img.getHeight());
    }
}

ただしメモリ負荷が高くなるため、WebPが確実にサポートされる環境なら不要です。

ご希望があれば、 🔹「フォールバック付き堅牢版」 🔹「読み込み失敗時に自動でPNG変換して格納する版」 のどちらかを作成することも可能です。 どちらにしましょうか?

あなた:

webp対応させて

@Override
public String getImageFilePath(String srcImageFileName, int lineNum)
{
    boolean isCover = false;
    this.imageIndex++; //xhtmlと画像ファイル名の番号を合わせるため先に++
    String ext = "";
    try { ext = srcImageFileName.substring(srcImageFileName.lastIndexOf('.')+1).toLowerCase(); } catch (Exception e) {}
    String imageId = decimalFormat.format(this.imageIndex);
    String imageFileName = IMAGES_PATH+imageId+"."+ext;
    ImageInfo imageInfo;
    try {
        imageInfo = this.imageInfoReader.getImageInfo(srcImageFileName);
        imageInfo.setId(imageId);
        imageInfo.setOutFileName(imageId+"."+ext);
        if (!imageInfo.getExt().matches("^(png|jpeg|gif|jpg)$")) {
            LogAppender.error(lineNum, "画像フォーマットエラー", srcImageFileName);
            return null;
        }
        if (this.imageIndex-1 == bookInfo.coverImageIndex) {
            imageInfo.setIsCover(true);
            isCover = true;
        }
        this.imageInfos.add(imageInfo);

    } catch (IOException e) {
        e.printStackTrace();
        return null;
    }
    //先頭に表紙ページ移動の場合でカバーページならnullを返して本文中から削除
    if (bookInfo.insertCoverPage && isCover) return null;
    return "../"+imageFileName;
}

ChatGPT:

いいですね 👍 このメソッドも既存の構造を崩さず、WebP形式を受け入れるようにするだけでOKです。 下記はあなたのコードに最小限の変更を加えて、WebP対応させた完全版です👇

@Override
public String getImageFilePath(String srcImageFileName, int lineNum)
{
    boolean isCover = false;
    this.imageIndex++; // xhtmlと画像ファイル名の番号を合わせるため先に++
    String ext = "";
    try {
        ext = srcImageFileName.substring(srcImageFileName.lastIndexOf('.') + 1).toLowerCase();
    } catch (Exception e) {}

    String imageId = decimalFormat.format(this.imageIndex);
    String imageFileName = IMAGES_PATH + imageId + "." + ext;
    ImageInfo imageInfo;

    try {
        imageInfo = this.imageInfoReader.getImageInfo(srcImageFileName);
        imageInfo.setId(imageId);
        imageInfo.setOutFileName(imageId + "." + ext);

        // ★ WebP対応: 拡張子チェックに webp を追加
        if (!imageInfo.getExt().matches("^(png|jpeg|gif|jpg|webp)$")) {
            LogAppender.error(lineNum, "画像フォーマットエラー", srcImageFileName);
            return null;
        }

        if (this.imageIndex - 1 == bookInfo.coverImageIndex) {
            imageInfo.setIsCover(true);
            isCover = true;
        }

        this.imageInfos.add(imageInfo);

    } catch (IOException e) {
        e.printStackTrace();
        return null;
    }

    // 先頭に表紙ページ移動の場合でカバーページならnullを返して本文中から削除
    if (bookInfo.insertCoverPage && isCover) return null;

    return "../" + imageFileName;
}

@Override
public String getImageFilePath(String srcImageFileName, int lineNum)
{
    boolean isCover = false;
    this.imageIndex++; // xhtmlと画像ファイル名の番号を合わせるため先に++
    String ext = "";
    try {
        ext = srcImageFileName.substring(srcImageFileName.lastIndexOf('.') + 1).toLowerCase();
    } catch (Exception e) {}

    String imageId = decimalFormat.format(this.imageIndex);
    String imageFileName = IMAGES_PATH + imageId + "." + ext;
    ImageInfo imageInfo;

    try {
        imageInfo = this.imageInfoReader.getImageInfo(srcImageFileName);
        imageInfo.setId(imageId);
        imageInfo.setOutFileName(imageId + "." + ext);

        // ★ WebP対応: 拡張子チェックに webp を追加
        if (!imageInfo.getExt().matches("^(png|jpeg|gif|jpg|webp)$")) {
            LogAppender.error(lineNum, "画像フォーマットエラー", srcImageFileName);
            return null;
        }

        if (this.imageIndex - 1 == bookInfo.coverImageIndex) {
            imageInfo.setIsCover(true);
            isCover = true;
        }

        this.imageInfos.add(imageInfo);

    } catch (IOException e) {
        e.printStackTrace();
        return null;
    }

    // 先頭に表紙ページ移動の場合でカバーページならnullを返して本文中から削除
    if (bookInfo.insertCoverPage && isCover) return null;

    return "../" + imageFileName;
}
  • 正規表現部分

java コードをコピーする if (!imageInfo.getExt().matches("^(png|jpeg|gif|jpg|webp)$"))

これで .webp が正式に許可されます。 (既存の .jpg / .jpeg とも両立) - ImageIO.scanForPlugins() 呼び出し webp-imageio を使うなら、 アプリ起動時またはクラス初期化時に以下を一度呼び出すのを忘れずに:

java コードをコピーする static { ImageIO.scanForPlugins(); }

  • 出力側の _writeImage も対応済みなら 入出力ともに .webp に完全対応できます。 読み込み、コピー、ZIP格納まで動作します。

正規表現部分

if (!imageInfo.getExt().matches("^(png|jpeg|gif|jpg|webp)$"))

if (!imageInfo.getExt().matches("^(png|jpeg|gif|jpg|webp)$"))

これで .webp が正式に許可されます。 (既存の .jpg / .jpeg とも両立)

ImageIO.scanForPlugins() 呼び出し webp-imageio を使うなら、 アプリ起動時またはクラス初期化時に以下を一度呼び出すのを忘れずに:

static {
    ImageIO.scanForPlugins();
}

static {
    ImageIO.scanForPlugins();
}

出力側の _writeImage も対応済みなら 入出力ともに .webp に完全対応できます。 読み込み、コピー、ZIP格納まで動作します。

もし .webp 画像を 一律で .jpg や .png に変換出力したい(Kindle互換など)場合は、 ext が "webp" のときに強制的に "jpg" に変える処理を入れることもできます。 希望しますか?## あなた: webpの対応させて

/** ファイル名から画像を取得
 * 拡張子変更等は外側で修正しておく
 * ファイルシステムまたはZipファイルから指定されたファイル名の画像を取得
 * @param srcImageFileName ファイル名 Zipならエントリ名
 * ※先頭からシークされるので遅い? 
 * @throws RarException */
public BufferedImage getImage(String srcImageFileName) throws IOException, RarException
{
    if (this.isFile) {
        File file = new File(this.srcParentPath+srcImageFileName);
        if (!file.exists()) {
            //拡張子修正
            srcImageFileName = this.correctExt(srcImageFileName);
            file = new File(this.srcParentPath+srcImageFileName);
            if (!file.exists()) return null;
        }
        try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file), 8192)) {
            return ImageUtils.readImage(srcImageFileName.substring(srcImageFileName.lastIndexOf('.') + 1).toLowerCase(), bis);
        }
    } else {
        if (this.srcFile.getName().endsWith(".rar")) {
            InputStream is = null;
            Archive archive = new Archive(srcFile);
            try {
            FileHeader fileHeader = archive.nextFileHeader();
            while (fileHeader != null) {
                if (!fileHeader.isDirectory()) {
                    String entryName = fileHeader.getFileName();
                    entryName = entryName.replace('\\', '/');
                    if (srcImageFileName.equals(entryName)) {
                        is = archive.getInputStream(fileHeader);
                        return ImageUtils.readImage(srcImageFileName.substring(srcImageFileName.lastIndexOf('.')+1).toLowerCase(), is);
                    }
                }
                fileHeader = archive.nextFileHeader();
            }
            } finally {
                if (is != null) is.close();
                archive.close();
            }

        } else {
            try (ZipFile zf = ZipFile.builder().setFile(this.srcFile).setUseUnicodeExtraFields(true).get()) {
                ZipArchiveEntry entry = zf.getEntry(srcImageFileName);
                if (entry == null) {
                    srcImageFileName = this.correctExt(srcImageFileName);
                    entry = zf.getEntry(srcImageFileName);
                    if (entry == null) return null;
                }
                try (InputStream is = zf.getInputStream(entry)) {
                    return ImageUtils.readImage(srcImageFileName.substring(srcImageFileName.lastIndexOf('.')+1).toLowerCase(), is);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    return null;
}

ChatGPT:

了解です。 このメソッドに WebP 形式の画像対応 を追加したいのですね。

Java 標準の ImageIO は WebP を直接サポートしていないため、 以下の2つのアプローチのいずれかが必要です。

ImageUtils.readImage() が内部で ImageIO.read() を使っているなら、 WebP のデコーダをプラグインで追加すれば、このメソッドの修正は最小限にできます。

例:

import com.luciad.imageio.webp.WebPReadParam;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.InputStream;

public class ImageUtils {
    public static BufferedImage readImage(String ext, InputStream is) throws IOException {
        ext = ext.toLowerCase();
        if (ext.equals("webp")) {
            // WebP対応
            ImageReader reader = ImageIO.getImageReadersByFormatName("webp").next();
            ImageInputStream iis = ImageIO.createImageInputStream(is);
            reader.setInput(iis, true);
            WebPReadParam param = new WebPReadParam();
            return reader.read(0, param);
        } else {
            return ImageIO.read(is);
        }
    }
}

import com.luciad.imageio.webp.WebPReadParam;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.InputStream;

public class ImageUtils {
    public static BufferedImage readImage(String ext, InputStream is) throws IOException {
        ext = ext.toLowerCase();
        if (ext.equals("webp")) {
            // WebP対応
            ImageReader reader = ImageIO.getImageReadersByFormatName("webp").next();
            ImageInputStream iis = ImageIO.createImageInputStream(is);
            reader.setInput(iis, true);
            WebPReadParam param = new WebPReadParam();
            return reader.read(0, param);
        } else {
            return ImageIO.read(is);
        }
    }
}

📦 必要ライブラリ: webp-imageio

(Maven: com.twelvemonkeys.imageio:imageio-webp:3.10.1 でもOK)

ImageUtils.readImage() を触りたくない場合は、getImage() メソッド側で判定します。

public BufferedImage getImage(String srcImageFileName) throws IOException, RarException {
    String ext = srcImageFileName.substring(srcImageFileName.lastIndexOf('.') + 1).toLowerCase();

    if (this.isFile) {
        File file = new File(this.srcParentPath + srcImageFileName);
        if (!file.exists()) {
            srcImageFileName = this.correctExt(srcImageFileName);
            file = new File(this.srcParentPath + srcImageFileName);
            if (!file.exists()) return null;
        }

        try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file), 8192)) {
            if (ext.equals("webp")) {
                return readWebP(bis);
            }
            return ImageUtils.readImage(ext, bis);
        }
    } else if (this.srcFile.getName().endsWith(".rar")) {
        try (Archive archive = new Archive(srcFile)) {
            FileHeader fileHeader;
            while ((fileHeader = archive.nextFileHeader()) != null) {
                if (!fileHeader.isDirectory()) {
                    String entryName = fileHeader.getFileName().replace('\\', '/');
                    if (srcImageFileName.equals(entryName)) {
                        try (InputStream is = archive.getInputStream(fileHeader)) {
                            if (ext.equals("webp")) return readWebP(is);
                            return ImageUtils.readImage(ext, is);
                        }
                    }
                }
            }
        }
    } else {
        try (ZipFile zf = ZipFile.builder().setFile(this.srcFile).setUseUnicodeExtraFields(true).get()) {
            ZipArchiveEntry entry = zf.getEntry(srcImageFileName);
            if (entry == null) {
                srcImageFileName = this.correctExt(srcImageFileName);
                entry = zf.getEntry(srcImageFileName);
                if (entry == null) return null;
            }
            try (InputStream is = zf.getInputStream(entry)) {
                if (ext.equals("webp")) return readWebP(is);
                return ImageUtils.readImage(ext, is);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    return null;
}

/** WebP専用読み込み */
private BufferedImage readWebP(InputStream is) throws IOException {
    ImageReader reader = ImageIO.getImageReadersByFormatName("webp").next();
    ImageInputStream iis = ImageIO.createImageInputStream(is);
    reader.setInput(iis, true);
    return reader.read(0, new com.luciad.imageio.webp.WebPReadParam());
}

public BufferedImage getImage(String srcImageFileName) throws IOException, RarException {
    String ext = srcImageFileName.substring(srcImageFileName.lastIndexOf('.') + 1).toLowerCase();

    if (this.isFile) {
        File file = new File(this.srcParentPath + srcImageFileName);
        if (!file.exists()) {
            srcImageFileName = this.correctExt(srcImageFileName);
            file = new File(this.srcParentPath + srcImageFileName);
            if (!file.exists()) return null;
        }

        try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file), 8192)) {
            if (ext.equals("webp")) {
                return readWebP(bis);
            }
            return ImageUtils.readImage(ext, bis);
        }
    } else if (this.srcFile.getName().endsWith(".rar")) {
        try (Archive archive = new Archive(srcFile)) {
            FileHeader fileHeader;
            while ((fileHeader = archive.nextFileHeader()) != null) {
                if (!fileHeader.isDirectory()) {
                    String entryName = fileHeader.getFileName().replace('\\', '/');
                    if (srcImageFileName.equals(entryName)) {
                        try (InputStream is = archive.getInputStream(fileHeader)) {
                            if (ext.equals("webp")) return readWebP(is);
                            return ImageUtils.readImage(ext, is);
                        }
                    }
                }
            }
        }
    } else {
        try (ZipFile zf = ZipFile.builder().setFile(this.srcFile).setUseUnicodeExtraFields(true).get()) {
            ZipArchiveEntry entry = zf.getEntry(srcImageFileName);
            if (entry == null) {
                srcImageFileName = this.correctExt(srcImageFileName);
                entry = zf.getEntry(srcImageFileName);
                if (entry == null) return null;
            }
            try (InputStream is = zf.getInputStream(entry)) {
                if (ext.equals("webp")) return readWebP(is);
                return ImageUtils.readImage(ext, is);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    return null;
}

/** WebP専用読み込み */
private BufferedImage readWebP(InputStream is) throws IOException {
    ImageReader reader = ImageIO.getImageReadersByFormatName("webp").next();
    ImageInputStream iis = ImageIO.createImageInputStream(is);
    reader.setInput(iis, true);
    return reader.read(0, new com.luciad.imageio.webp.WebPReadParam());
}

WebPを読み込むには、次のいずれかの依存関係を追加します:

<dependency>
  <groupId>com.twelvemonkeys.imageio</groupId>
  <artifactId>imageio-webp</artifactId>
  <version>3.10.1</version>
</dependency>

<dependency>
  <groupId>com.twelvemonkeys.imageio</groupId>
  <artifactId>imageio-webp</artifactId>
  <version>3.10.1</version>
</dependency>
implementation 'com.twelvemonkeys.imageio:imageio-webp:3.10.1'

implementation 'com.twelvemonkeys.imageio:imageio-webp:3.10.1'

ImageIO.scanForPlugins() を一度呼んでおくと、自動登録されます。

どちらの方針にしたいですか?

  • ImageUtils を編集できるなら「方針1」がシンプル。
  • 編集できない場合は「方針2」で getImage() 内にWebP対応を追加します。

ImageUtils を編集できるなら「方針1」がシンプル。

編集できない場合は「方針2」で getImage() 内にWebP対応を追加します。

あなた:

/** ストリームから画像を読み込み */
static public BufferedImage readImage(String ext, InputStream is) throws IOException
{
    BufferedImage image;
        try {
            image = ImageIO.read(is);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    is.close();
    return image;
}

ChatGPT:

ありがとうございます。 この ImageUtils.readImage() の中身を見る限り、ImageIO.read() しか呼んでいませんね。 つまり WebP対応をここに追加するのが最も簡潔で安全 です(=先ほどの「方針1」)。

com.twelvemonkeys.imageio:imageio-webp などの WebP プラグインを使う想定です。 この修正版は、他の呼び出し側(getImage() など)を一切変更せずに動作します。

/** ストリームから画像を読み込み */
static public BufferedImage readImage(String ext, InputStream is) throws IOException {
    BufferedImage image = null;
    try {
        ext = ext.toLowerCase();

        if (ext.equals("webp")) {
            // WebP専用処理
            ImageReader reader = ImageIO.getImageReadersByFormatName("webp").next();
            try (ImageInputStream iis = ImageIO.createImageInputStream(is)) {
                reader.setInput(iis, true);
                return reader.read(0, reader.getDefaultReadParam());
            }
        } else {
            // 通常画像(jpg, png, gif, bmpなど)
            image = ImageIO.read(is);
        }

    } catch (Exception e) {
        throw new RuntimeException(e);
    } finally {
        is.close();
    }
    return image;
}

/** ストリームから画像を読み込み */
static public BufferedImage readImage(String ext, InputStream is) throws IOException {
    BufferedImage image = null;
    try {
        ext = ext.toLowerCase();

        if (ext.equals("webp")) {
            // WebP専用処理
            ImageReader reader = ImageIO.getImageReadersByFormatName("webp").next();
            try (ImageInputStream iis = ImageIO.createImageInputStream(is)) {
                reader.setInput(iis, true);
                return reader.read(0, reader.getDefaultReadParam());
            }
        } else {
            // 通常画像(jpg, png, gif, bmpなど)
            image = ImageIO.read(is);
        }

    } catch (Exception e) {
        throw new RuntimeException(e);
    } finally {
        is.close();
    }
    return image;
}

このコードが動作するには、WebPプラグインを追加してください。

<dependency>
  <groupId>com.twelvemonkeys.imageio</groupId>
  <artifactId>imageio-webp</artifactId>
  <version>3.10.1</version>
</dependency>

<dependency>
  <groupId>com.twelvemonkeys.imageio</groupId>
  <artifactId>imageio-webp</artifactId>
  <version>3.10.1</version>
</dependency>
implementation 'com.twelvemonkeys.imageio:imageio-webp:3.10.1'

implementation 'com.twelvemonkeys.imageio:imageio-webp:3.10.1'

初回読み込み前に1度だけ以下を実行しておくと、 WebPプラグインが自動登録されます(例:アプリ起動時)。

ImageIO.scanForPlugins();

ImageIO.scanForPlugins();
  • 既存の getImage() や他の呼び出しコードを変更不要
  • .webp 拡張子だけ自動で専用リーダーに切り替え
  • 例外処理・ストリームクローズも従来通り安全

既存の getImage() や他の呼び出しコードを変更不要

.webp 拡張子だけ自動で専用リーダーに切り替え

例外処理・ストリームクローズも従来通り安全

希望があれば、

  • .webp 以外にも .avif や .heif に対応
  • ImageIO.read() が null を返したときの再試行 も加えた拡張版を出せます。

.webp 以外にも .avif や .heif に対応

ImageIO.read() が null を返したときの再試行 も加えた拡張版を出せます。

どうしますか?(WebPだけで十分?)

あなた:

webp-imageioを使うときの注意点は

ChatGPT:

良い質問です。 webp-imageio(たとえば sejda/webp-imageio )や twelvemonkeys の imageio-webp を使うときには、 いくつか 実務上の注意点・落とし穴 があります。

以下、実際にAozoraEpub3のような画像処理アプリに組み込む場合を想定して、 代表的な注意点を整理します。

まず、Java向け WebP デコーダには主に2系統あります:

ライブラリ Maven座標 特徴
webp-imageio com.luciad.imageio:webp-imageio Google製libwebpに準拠。高品質・安定。WebPReadParamなど専用APIあり。
TwelveMonkeys ImageIO WebP com.twelvemonkeys.imageio:imageio-webp ImageIO互換で他形式とも統一的に扱える。Android非対応。

🔸推奨: デスクトップ/Javaアプリでは TwelveMonkeys 系。 高機能でメンテが続いており、ImageIO.read()にも自動登録可能。

ImageIOは、起動時に自動でプラグインを認識しないことがあります。 そのため、初回使用前に1度だけ明示的に呼ぶと安全です。

ImageIO.scanForPlugins();

ImageIO.scanForPlugins();

これを呼んでおけば、 ImageIO.read() で webp も自動的に処理できるようになります。

🔸呼び出しタイミングの例: main() の最初や、static {} ブロックなど。

ImageIO.read(InputStream) は、 サポートされない形式のとき null を返す点に注意です。

WebPが未登録だと、

BufferedImage img = ImageIO.read(is);

BufferedImage img = ImageIO.read(is);

で例外ではなく null が返るため、 その後 img.getWidth() などで NullPointerException が発生します。

対策:

BufferedImage img = ImageIO.read(is);
if (img == null) throw new IOException("Unsupported image format");

BufferedImage img = ImageIO.read(is);
if (img == null) throw new IOException("Unsupported image format");

WebPは透過(alpha)付きの画像を扱えますが、 BufferedImage へ変換するときに TYPE_INT_ARGB でない場合、背景が黒くなることがあります。

対策(必要な場合):

BufferedImage image = ImageIO.read(is);
if (image.getType() != BufferedImage.TYPE_INT_ARGB) {
    BufferedImage argb = new BufferedImage(
        image.getWidth(),
        image.getHeight(),
        BufferedImage.TYPE_INT_ARGB
    );
    Graphics2D g = argb.createGraphics();
    g.drawImage(image, 0, 0, null);
    g.dispose();
    image = argb;
}

BufferedImage image = ImageIO.read(is);
if (image.getType() != BufferedImage.TYPE_INT_ARGB) {
    BufferedImage argb = new BufferedImage(
        image.getWidth(),
        image.getHeight(),
        BufferedImage.TYPE_INT_ARGB
    );
    Graphics2D g = argb.createGraphics();
    g.drawImage(image, 0, 0, null);
    g.dispose();
    image = argb;
}

環境によってはライブラリが存在しない・読み込み失敗する場合もあります。 その場合は例外をキャッチして、代替処理(PNGなど)にフォールバックさせます。

try {
    ImageIO.scanForPlugins();
    image = ImageIO.read(is);
    if (image == null) throw new IOException("Unsupported format");
} catch (Exception e) {
    // fallback
}

try {
    ImageIO.scanForPlugins();
    image = ImageIO.read(is);
    if (image == null) throw new IOException("Unsupported format");
} catch (Exception e) {
    // fallback
}
Javaバージョン 注意点
Java 8 問題なし(最も安定)
Java 11+ OK。だが古い webp-imageio は module-info 非対応なので --add-opens が必要な場合あり
Java 17+ twelvemonkeys 最新版を使用推奨(3.10.1以降)
  • imageio-webp は 静止画のみ対応(アニメーションWebPは非対応)
  • webp-imageio(Luciad版)は アニメーション対応 だが、ページ数分の read(index) が必要

imageio-webp は 静止画のみ対応(アニメーションWebPは非対応)

webp-imageio(Luciad版)は アニメーション対応 だが、ページ数分の read(index) が必要

もしアニメーションを扱うなら:

ImageReader reader = ImageIO.getImageReadersByFormatName("webp").next();
reader.setInput(iis);
int numImages = reader.getNumImages(true);
for (int i = 0; i < numImages; i++) {
    BufferedImage frame = reader.read(i);
    ...
}

ImageReader reader = ImageIO.getImageReadersByFormatName("webp").next();
reader.setInput(iis);
int numImages = reader.getNumImages(true);
for (int i = 0; i < numImages; i++) {
    BufferedImage frame = reader.read(i);
    ...
}

javax.imageio 系ライブラリは Android では動作しません。 (Androidは BitmapFactory.decodeStream() を使う別系統のAPI)

なので、WebP対応は Java/Swing/デスクトップアプリ限定 です。

項目 内容
登録 ImageIO.scanForPlugins() を初回に呼ぶ
例外処理 ImageIO.read() の null チェックを忘れない
アニメWebP 対応しているのは Luciad版のみ
Javaバージョン 17以上なら twelvemonkeys 3.10+ 推奨
Android 非対応(代替:BitmapFactory)

ご希望があれば、 AozoraEpub3用の ImageUtils.readImage() を twelvemonkeys で安全に WebP対応した 完全版 にしてお渡しできます。 作成しましょうか?

あなた:

ImageIO.scanForPlugins()はどこに入れるべきか

ChatGPT:

とても良い質問です。 ImageIO.scanForPlugins() は 1回だけ実行すれば十分 で、 アプリ起動直後(最初に画像を読み込む前) に呼ぶのがベストです。

以下に、AozoraEpub3 などの構成を想定して 「どこに入れるのが最適か」を段階的に解説します👇

public static void main(String[] args) {
    // WebPなどImageIOプラグインを読み込む
    ImageIO.scanForPlugins();

    // アプリ初期化
    new AozoraEpub3App().start(args);
}

public static void main(String[] args) {
    // WebPなどImageIOプラグインを読み込む
    ImageIO.scanForPlugins();

    // アプリ初期化
    new AozoraEpub3App().start(args);
}

💡 これが一番確実。 どのクラスよりも先に呼ばれるため、 その後の ImageIO.read() ですぐWebPが使えます。

もし main() にアクセスできない(ライブラリ内で動いているなど)場合は、 ImageUtils クラスの static初期化ブロック に入れておく方法があります。

public class ImageUtils {
    static {
        // クラスロード時に1回だけ実行される
        ImageIO.scanForPlugins();
    }

    /** ストリームから画像を読み込み */
    public static BufferedImage readImage(String ext, InputStream is) throws IOException {
        BufferedImage image;
        try {
            ext = ext.toLowerCase();
            if (ext.equals("webp")) {
                ImageReader reader = ImageIO.getImageReadersByFormatName("webp").next();
                try (ImageInputStream iis = ImageIO.createImageInputStream(is)) {
                    reader.setInput(iis, true);
                    return reader.read(0, reader.getDefaultReadParam());
                }
            } else {
                image = ImageIO.read(is);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            is.close();
        }
        return image;
    }
}

public class ImageUtils {
    static {
        // クラスロード時に1回だけ実行される
        ImageIO.scanForPlugins();
    }

    /** ストリームから画像を読み込み */
    public static BufferedImage readImage(String ext, InputStream is) throws IOException {
        BufferedImage image;
        try {
            ext = ext.toLowerCase();
            if (ext.equals("webp")) {
                ImageReader reader = ImageIO.getImageReadersByFormatName("webp").next();
                try (ImageInputStream iis = ImageIO.createImageInputStream(is)) {
                    reader.setInput(iis, true);
                    return reader.read(0, reader.getDefaultReadParam());
                }
            } else {
                image = ImageIO.read(is);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            is.close();
        }
        return image;
    }
}

✅ この方法だと、アプリ全体で ImageUtils.readImage() を最初に呼んだときに自動登録されます。 (以降のすべてのImageIO.read()にもWebPプラグインが反映されます)

もしアプリが AppConfig や Settings のような初期化用クラスを持っている場合、 その中に入れてもOKです。

public class AppConfig {
    public static void initialize() {
        ImageIO.scanForPlugins();
        // 他の初期化(設定読込、キャッシュ準備など)
    }
}

public class AppConfig {
    public static void initialize() {
        ImageIO.scanForPlugins();
        // 他の初期化(設定読込、キャッシュ準備など)
    }
}

そして、アプリ起動時に

AppConfig.initialize();

AppConfig.initialize();

を呼ぶだけでWebP対応が有効になります。

  • 呼ぶのは1回で十分。
  • 2回以上呼んでも問題はありません(再スキャンするだけ)。
  • コストは非常に小さい(数ms程度)。

呼ぶのは1回で十分。

2回以上呼んでも問題はありません(再スキャンするだけ)。

コストは非常に小さい(数ms程度)。

ImageIO.read() は WebPファイルを読めずに nullを返します。 例外ではないため、気づかずに NullPointerException が出ることがあります。

目的 ベストな場所
アプリ全体で確実に有効化 main() の最初
ImageUtils 単体で完結させたい static { ImageIO.scanForPlugins(); } に記述
初期化専用クラスがある その中の initialize() メソッド

もしよければ、 あなたのプロジェクトの main クラス名や構造に合わせて 「実際に入れる完全版コード」を示すこともできます。

どこで呼び出しているか(例:Main.java や AozoraEpub3App など)を教えてもらえますか?

あなた:

main関数でImageIOを呼び出していない場合は

ChatGPT:

とても良い質問です。 AozoraEpub3 のように「明示的な main() 関数がない構成(例:GUIアプリ/CLIランチャー/JavaFX/サーブレットなど)」では、 ImageIO.scanForPlugins() を どこで呼ぶか は確かに迷うポイントです。

以下では、そうした「main() がない(もしくは編集しにくい)」場合の安全な代替方法を整理します👇

この方法なら、どこから呼び出されても 最初に ImageUtils クラスがロードされた時点で 自動的に WebP プラグインが有効になります。

public class ImageUtils {
    static {
        // クラス読み込み時に一度だけ実行される
        try {
            ImageIO.scanForPlugins();
        } catch (Throwable t) {
            System.err.println("ImageIO plugin scan failed: " + t);
        }
    }

    /** ストリームから画像を読み込み */
    public static BufferedImage readImage(String ext, InputStream is) throws IOException {
        BufferedImage image;
        try {
            ext = ext.toLowerCase();
            if (ext.equals("webp")) {
                ImageReader reader = ImageIO.getImageReadersByFormatName("webp").next();
                try (ImageInputStream iis = ImageIO.createImageInputStream(is)) {
                    reader.setInput(iis, true);
                    return reader.read(0, reader.getDefaultReadParam());
                }
            } else {
                image = ImageIO.read(is);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            is.close();
        }
        return image;
    }
}

public class ImageUtils {
    static {
        // クラス読み込み時に一度だけ実行される
        try {
            ImageIO.scanForPlugins();
        } catch (Throwable t) {
            System.err.println("ImageIO plugin scan failed: " + t);
        }
    }

    /** ストリームから画像を読み込み */
    public static BufferedImage readImage(String ext, InputStream is) throws IOException {
        BufferedImage image;
        try {
            ext = ext.toLowerCase();
            if (ext.equals("webp")) {
                ImageReader reader = ImageIO.getImageReadersByFormatName("webp").next();
                try (ImageInputStream iis = ImageIO.createImageInputStream(is)) {
                    reader.setInput(iis, true);
                    return reader.read(0, reader.getDefaultReadParam());
                }
            } else {
                image = ImageIO.read(is);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            is.close();
        }
        return image;
    }
}

✅ この方法のメリット

  • main() がなくても確実に実行される
  • 画像処理専用の ImageUtils に閉じているため依存が分かりやすい
  • アプリ全体で1回しか呼ばれない
  • 他のコードを一切変更しなくていい

main() がなくても確実に実行される

画像処理専用の ImageUtils に閉じているため依存が分かりやすい

アプリ全体で1回しか呼ばれない

他のコードを一切変更しなくていい

アプリに「設定ロード」「パス初期化」「UI初期化」などの処理がある場合、 そこに一行追加するのも安全です。

public class AppInitializer {
    public static void init() {
        ImageIO.scanForPlugins();  // WebP対応を有効化
        // 設定読み込みやUI準備など...
    }
}

public class AppInitializer {
    public static void init() {
        ImageIO.scanForPlugins();  // WebP対応を有効化
        // 設定読み込みやUI準備など...
    }
}

そして、アプリ起動時に

AppInitializer.init();

AppInitializer.init();

を呼ぶだけ。

Swingなら、フレームや画面を作る前に呼ぶのが良いです。

public class AozoraEpub3App {
    public void start() {
        ImageIO.scanForPlugins(); // WebP登録

        // UI初期化など
        SwingUtilities.invokeLater(() -> new MainFrame().setVisible(true));
    }
}

public class AozoraEpub3App {
    public void start() {
        ImageIO.scanForPlugins(); // WebP登録

        // UI初期化など
        SwingUtilities.invokeLater(() -> new MainFrame().setVisible(true));
    }
}

JavaFXの start(Stage primaryStage) の先頭に入れるのが安全です。

@Override
public void start(Stage stage) {
    ImageIO.scanForPlugins();
    // 画面構築処理
}

@Override
public void start(Stage stage) {
    ImageIO.scanForPlugins();
    // 画面構築処理
}

もしこのコードが別のアプリに組み込まれる「ライブラリ」なら、 呼び出し側アプリで1回だけ ImageIO.scanForPlugins() を呼んでもらう設計にしてもOKです。 (ドキュメントなどに「WebPを使う場合は初期化が必要」と記載)

本当に登録できているか確認したい場合は:

for (String format : ImageIO.getReaderFormatNames()) {
    if (format.equalsIgnoreCase("webp")) {
        System.out.println("WebP reader is registered");
    }
}

for (String format : ImageIO.getReaderFormatNames()) {
    if (format.equalsIgnoreCase("webp")) {
        System.out.println("WebP reader is registered");
    }
}

これでコンソールに WebP reader is registered が出ればOKです。

環境 最適な場所 理由
汎用ライブラリ ImageUtils の staticブロック 自動実行・呼び出し側を変更不要
GUIアプリ(Swing / JavaFX) UI生成前 (start()やinit() の最初) プラグイン有効化を確実に行える
CLIツール / サーバーアプリ 起動初期化メソッド (init() など) 他の初期化とまとめて管理できる

もしあなたのAozoraEpub3版アプリの起動構成(例:MainFrame → AppConfig → MainPanel)が分かれば、 実際に挿入すべき場所を具体的に 指定できます。

どのクラスが最初に起動されるか教えてもらえますか?

あなた:

画像ファイルをできるだけ編集しないように扱うには

ChatGPT:

とても良い方針です。 「画像ファイルをできるだけ編集しない(=破壊せず、安全に扱う)」というのは、 EPUBや変換ツールのようなアプリでは非常に大切です。

以下では、Javaで画像を読み取って利用する際に“元ファイルを一切変更しない”ように扱う具体的な指針をまとめます。 AozoraEpub3や同系統の変換処理にそのまま適用できる考え方です。

最も基本的ですが重要です。

try (InputStream is = new BufferedInputStream(new FileInputStream(file))) {
    BufferedImage img = ImageIO.read(is);
}

try (InputStream is = new BufferedInputStream(new FileInputStream(file))) {
    BufferedImage img = ImageIO.read(is);
}
  • FileInputStream は読み込み専用。
  • BufferedInputStream に包むと OS のキャッシュ効率も良く、速度も安定。
  • ImageIO.read() はストリームを読み取るだけで、ファイル自体には一切書き込まない。

FileInputStream は読み込み専用。

BufferedInputStream に包むと OS のキャッシュ効率も良く、速度も安定。

ImageIO.read() はストリームを読み取るだけで、ファイル自体には一切書き込まない。

🔸ポイント: ファイル操作系(例:RandomAccessFile, FileOutputStream)を使わないこと。 読み込み時に 書き込み可能モードを開かない のが安全の第一歩です。

Javaの BufferedImage はメモリ上のデータを直接操作できてしまうため、 後で誤って上書き保存してしまう危険があります。

加工が必要な場合は、常に別インスタンスに描画して処理 します。

BufferedImage original = ImageIO.read(is);
BufferedImage copy = new BufferedImage(
    original.getWidth(), original.getHeight(), BufferedImage.TYPE_INT_ARGB);
Graphics2D g = copy.createGraphics();
g.drawImage(original, 0, 0, null);
g.dispose();

BufferedImage original = ImageIO.read(is);
BufferedImage copy = new BufferedImage(
    original.getWidth(), original.getHeight(), BufferedImage.TYPE_INT_ARGB);
Graphics2D g = copy.createGraphics();
g.drawImage(original, 0, 0, null);
g.dispose();

こうすると original は完全に読み取り専用として扱えます。 コピーした画像を変形・リサイズ・トリミングに使用します。

加工結果を保存する場合、 元ファイルと同じパス・ファイル名で上書き保存しないようにします。

File output = new File(destDir, srcFile.getName());
if (output.equals(srcFile)) {
    // 安全のため別名に変更
    output = new File(destDir, "converted_" + srcFile.getName());
}
ImageIO.write(copy, "png", output);

File output = new File(destDir, srcFile.getName());
if (output.equals(srcFile)) {
    // 安全のため別名に変更
    output = new File(destDir, "converted_" + srcFile.getName());
}
ImageIO.write(copy, "png", output);

上書きを防ぐことで、元画像の再利用・再処理が容易になります。

変換やサムネイル生成の中間データは、 ファイルシステムに直接書かずに メモリ上で済ませる か、 一時ファイルを java.nio.file.Files.createTempFile() で生成します。

Path temp = Files.createTempFile("img_", ".png");
ImageIO.write(image, "png", temp.toFile());

Path temp = Files.createTempFile("img_", ".png");
ImageIO.write(image, "png", temp.toFile());

OSが自動的に一時ディレクトリを管理するため、 永続的な破壊は起きません。

ImageIO プラグインは基本的に読み込み専用でも使えます。 ただしライブラリによっては「デコード中に書き込みを試みる」実装があることがあります。 以下のように、入力ストリームから直接読み込む方式なら安全です。

try (InputStream is = new BufferedInputStream(new FileInputStream(file))) {
    ImageReader reader = ImageIO.getImageReadersByFormatName("webp").next();
    try (ImageInputStream iis = ImageIO.createImageInputStream(is)) {
        reader.setInput(iis, true); // true = 読み込み専用
        BufferedImage img = reader.read(0);
    }
}

try (InputStream is = new BufferedInputStream(new FileInputStream(file))) {
    ImageReader reader = ImageIO.getImageReadersByFormatName("webp").next();
    try (ImageInputStream iis = ImageIO.createImageInputStream(is)) {
        reader.setInput(iis, true); // true = 読み込み専用
        BufferedImage img = reader.read(0);
    }
}

✅ setInput(iis, true) の第2引数を true にすることで 「読み込み専用モード」で安全にデコードされます。

例外処理は常に try-with-resources で閉じるようにします。

try (InputStream is = new FileInputStream(file)) {
    BufferedImage image = ImageIO.read(is);
} catch (IOException e) {
    System.err.println("Failed to read image: " + file);
}

try (InputStream is = new FileInputStream(file)) {
    BufferedImage image = ImageIO.read(is);
} catch (IOException e) {
    System.err.println("Failed to read image: " + file);
}

try-with-resources を使えば、例外発生時にもファイルハンドルが自動で閉じられ、 OSがファイルをロックしたままになるのを防げます。

JPEG や WebP のように「再圧縮を伴う形式」に変換すると 画質が劣化したり、EXIF情報が失われます。

対策:

  • 出力形式はなるべく 非圧縮(PNG)や可逆圧縮(WebP Lossless) にする
  • 元画像を コピーしてから 変換する
  • EXIFなどのメタデータを必要に応じて保持する

出力形式はなるべく 非圧縮(PNG)や可逆圧縮(WebP Lossless) にする

元画像を コピーしてから 変換する

EXIFなどのメタデータを必要に応じて保持する

配布環境でさらに安全を確保したい場合は:

  • 処理対象のフォルダを読み取り専用にする
  • ファイルを File.setReadOnly() でロックする
  • 一時作業フォルダを AppData や Temp に分離する

処理対象のフォルダを読み取り専用にする

ファイルを File.setReadOnly() でロックする

一時作業フォルダを AppData や Temp に分離する

目的 方法
ファイルを壊さない 読み込み専用ストリーム (FileInputStream) を使う
加工は別バッファで BufferedImage copy = new BufferedImage(...)
上書きを防ぐ 出力ファイル名を変える
一時変換 Files.createTempFile() を使う
安全な読込 setInput(iis, true)
ライブラリ管理 ImageIO.scanForPlugins() を先に呼ぶ

もしよければ、 あなたの getImage() と ImageUtils.readImage() をベースに、 「元画像を絶対に変更しない安全版」 として 完全な統合コードを生成できます。

作成しましょうか?

あなた:

webpの出力品質の変化させるには

ChatGPT:

とても良い質問です。 webp-imageio(またはそれと互換性のある com.luciad.imageio.webp 系プラグイン)を使う場合、 WebP出力の品質(圧縮率)を制御するには、ImageWriteParam を適切に設定する必要があります。

以下で、

  • 品質を変更する方法(lossy/lossless両対応)
  • 注意点 を順に説明します。

品質を変更する方法(lossy/lossless両対応)

注意点 を順に説明します。

import javax.imageio.*;
import javax.imageio.stream.*;
import java.awt.image.*;
import java.io.*;
import java.util.Iterator;

public class WebPWriter {
    public static void writeWebP(BufferedImage image, File outputFile, float quality) throws IOException {
        Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("webp");
        if (!writers.hasNext()) {
            throw new IllegalStateException("No WebP ImageWriter found. Make sure webp-imageio is loaded.");
        }

        ImageWriter writer = writers.next();
        ImageWriteParam param = writer.getDefaultWriteParam();

        // lossy モード(品質を指定)
        if (param.canWriteCompressed()) {
            param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
            param.setCompressionQuality(quality); // 0.0f~1.0f(高いほど高品質・低圧縮)
        }

        try (ImageOutputStream ios = ImageIO.createImageOutputStream(outputFile)) {
            writer.setOutput(ios);
            writer.write(null, new IIOImage(image, null, null), param);
        } finally {
            writer.dispose();
        }
    }

    public static void main(String[] args) throws IOException {
        BufferedImage img = ImageIO.read(new File("test.png"));
        writeWebP(img, new File("test_q80.webp"), 0.8f);
    }
}

import javax.imageio.*;
import javax.imageio.stream.*;
import java.awt.image.*;
import java.io.*;
import java.util.Iterator;

public class WebPWriter {
    public static void writeWebP(BufferedImage image, File outputFile, float quality) throws IOException {
        Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("webp");
        if (!writers.hasNext()) {
            throw new IllegalStateException("No WebP ImageWriter found. Make sure webp-imageio is loaded.");
        }

        ImageWriter writer = writers.next();
        ImageWriteParam param = writer.getDefaultWriteParam();

        // lossy モード(品質を指定)
        if (param.canWriteCompressed()) {
            param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
            param.setCompressionQuality(quality); // 0.0f~1.0f(高いほど高品質・低圧縮)
        }

        try (ImageOutputStream ios = ImageIO.createImageOutputStream(outputFile)) {
            writer.setOutput(ios);
            writer.write(null, new IIOImage(image, null, null), param);
        } finally {
            writer.dispose();
        }
    }

    public static void main(String[] args) throws IOException {
        BufferedImage img = ImageIO.read(new File("test.png"));
        writeWebP(img, new File("test_q80.webp"), 0.8f);
    }
}
意味 備考
0.0f 最低品質(最高圧縮率) 画質は悪いがサイズが小さい
0.75f 通常のバランス Web用途に多い
0.9f 高品質 ほぼ視覚劣化なし
1.0f 非常に高品質 サイズ大きめ、ほぼロスレスに近い

WebPの「品質」は JPEGの setCompressionQuality() と似ていますが、 エンコーダが内部的にPSNR最適化を行うため、 同じ値でも実際の画質・サイズは環境によって若干異なります。

webp-imageio の場合、ImageWriteParam に追加パラメータを指定することで 完全ロスレスモード に切り替えることも可能です。

param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
param.setCompressionType("Lossless"); // または "lossless"

param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
param.setCompressionType("Lossless"); // または "lossless"

これにより品質指定 (setCompressionQuality) は無視され、 可逆圧縮(PNGに近い)で保存されます。

どの compressionType が利用可能かを確認するには:

String[] types = param.getCompressionTypes();
if (types != null) {
    for (String t : types) {
        System.out.println("Available compression type: " + t);
    }
}

String[] types = param.getCompressionTypes();
if (types != null) {
    for (String t : types) {
        System.out.println("Available compression type: " + t);
    }
}

多くの WebP 実装では

  • "Lossy"
  • "Lossless" の2種類が返されます。

"Lossy"

"Lossless" の2種類が返されます。

注意点 内容
🔸 ImageIO.scanForPlugins() 先に呼び出さないと WebP Writer が検出されない場合あり。
🔸 quality は 0.0~1.0 範囲外だと IllegalArgumentException。
🔸 ロスレス時 setCompressionQuality() の指定は無視される。
🔸 メモリ消費 高品質設定はメモリ使用量が増える。
🔸 透過画像 RGBA画像を扱う場合は BufferedImage.TYPE_INT_ARGB を使うこと。
float[] qualities = {0.3f, 0.6f, 0.9f};
for (float q : qualities) {
    File out = new File(String.format("output_q%.0f.webp", q * 100));
    writeWebP(image, out, q);
}

float[] qualities = {0.3f, 0.6f, 0.9f};
for (float q : qualities) {
    File out = new File(String.format("output_q%.0f.webp", q * 100));
    writeWebP(image, out, q);
}

これで 30%, 60%, 90% 品質の WebP が生成され、 サイズと画質を比較できます。

目的 方法
圧縮率を下げて高画質に param.setCompressionQuality(1.0f)
高圧縮で軽量化 param.setCompressionQuality(0.3f)
可逆圧縮にする param.setCompressionType("Lossless")
プラグインが検出されない ImageIO.scanForPlugins() を実行

ご希望があれば、 🔹 「PNG → WebP 変換(品質指定付き)」を行う 🔹 「複数ファイルを一括処理するCLIツール」 🔹 「JavaFXやSwingでGUIプレビュー付き変換」

のいずれかの完全コードを出力することもできます。 どのパターンを希望しますか?

あなた:

webp対応させて

/** 画像を出力 マージン指定があればカット
 * //@param margin カットするピクセル数(left, top, right, bottom) */
static private void _writeImage(ZipArchiveOutputStream zos, BufferedImage srcImage, String ext, float jpegQuality) throws IOException
{
    if ("png".equals(ext)) {
        /*//PNGEncoder kindlegenでエラーになるのと色が反映されない
        PngEncoder pngEncoder = new PngEncoder();
        int pngColorType = PngEncoder.COLOR_TRUECOLOR;
        switch (srcImage.getType()) {
        case BufferedImage.TYPE_BYTE_BINARY:
            pngColorType = PngEncoder.COLOR_INDEXED; break;
        case BufferedImage.TYPE_BYTE_INDEXED:
            pngColorType = PngEncoder.COLOR_INDEXED; break;
        case BufferedImage.TYPE_BYTE_GRAY:
            pngColorType = PngEncoder.COLOR_GRAYSCALE; break;
        }
        pngEncoder.setColorType(pngColorType);
        pngEncoder.setCompression(PngEncoder.BEST_COMPRESSION);
        pngEncoder.setIndexedColorMode(PngEncoder.INDEXED_COLORS_AUTO);
        pngEncoder.encode(srcImage, zos);
        */
        //ImageIO.write(srcImage, "PNG", zos);
        ImageWriter imageWriter = getPngImageWriter();
        imageWriter.setOutput(ImageIO.createImageOutputStream(zos));
        imageWriter.write(srcImage);
    } else if ("jpeg".equals(ext) || "jpg".equals(ext)) {
        ImageWriter imageWriter = getJpegImageWriter();
        imageWriter.setOutput(ImageIO.createImageOutputStream(zos));
        ImageWriteParam iwp = imageWriter.getDefaultWriteParam();
        if (iwp.canWriteCompressed()) {
            try {
                iwp.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
                iwp.setCompressionQuality(jpegQuality);
                imageWriter.write(null, new IIOImage(srcImage, null, null), iwp);
            } catch (Exception e) { e.printStackTrace(); }
        } else {
            imageWriter.write(srcImage);
        }
    } else {
        ImageIO.write(srcImage, ext, zos);
    }
    zos.flush();
}

ChatGPT:

良いですね👍 この _writeImage() に WebP対応 を加えるのは比較的簡単です。 既存のPNG/JPEG処理をほぼ維持したまま、WebP用の分岐を1つ追加すればOKです。

/** 画像を出力 マージン指定があればカット
 * //@param margin カットするピクセル数(left, top, right, bottom) */
static private void _writeImage(ZipArchiveOutputStream zos, BufferedImage srcImage, String ext, float jpegQuality) throws IOException
{
    ext = ext.toLowerCase();

    if ("png".equals(ext)) {
        ImageWriter imageWriter = getPngImageWriter();
        imageWriter.setOutput(ImageIO.createImageOutputStream(zos));
        imageWriter.write(srcImage);

    } else if ("jpeg".equals(ext) || "jpg".equals(ext)) {
        ImageWriter imageWriter = getJpegImageWriter();
        imageWriter.setOutput(ImageIO.createImageOutputStream(zos));
        ImageWriteParam iwp = imageWriter.getDefaultWriteParam();
        if (iwp.canWriteCompressed()) {
            try {
                iwp.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
                iwp.setCompressionQuality(jpegQuality);
                imageWriter.write(null, new IIOImage(srcImage, null, null), iwp);
            } catch (Exception e) {
                e.printStackTrace();
            }
        } else {
            imageWriter.write(srcImage);
        }

    } else if ("webp".equals(ext)) {
        // --- WebP出力処理 ---
        ImageWriter imageWriter = null;
        Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("webp");
        if (writers.hasNext()) {
            imageWriter = writers.next();
        } else {
            throw new IOException("WebP writer not found. Make sure webp-imageio is in classpath.");
        }

        imageWriter.setOutput(ImageIO.createImageOutputStream(zos));
        ImageWriteParam param = imageWriter.getDefaultWriteParam();

        if (param.canWriteCompressed()) {
            param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
            param.setCompressionType("Lossy"); // "Lossless" も可
            param.setCompressionQuality(jpegQuality); // 0.0f~1.0f
        }

        imageWriter.write(null, new IIOImage(srcImage, null, null), param);
        imageWriter.dispose();

    } else {
        ImageIO.write(srcImage, ext, zos);
    }

    zos.flush();
}

/** 画像を出力 マージン指定があればカット
 * //@param margin カットするピクセル数(left, top, right, bottom) */
static private void _writeImage(ZipArchiveOutputStream zos, BufferedImage srcImage, String ext, float jpegQuality) throws IOException
{
    ext = ext.toLowerCase();

    if ("png".equals(ext)) {
        ImageWriter imageWriter = getPngImageWriter();
        imageWriter.setOutput(ImageIO.createImageOutputStream(zos));
        imageWriter.write(srcImage);

    } else if ("jpeg".equals(ext) || "jpg".equals(ext)) {
        ImageWriter imageWriter = getJpegImageWriter();
        imageWriter.setOutput(ImageIO.createImageOutputStream(zos));
        ImageWriteParam iwp = imageWriter.getDefaultWriteParam();
        if (iwp.canWriteCompressed()) {
            try {
                iwp.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
                iwp.setCompressionQuality(jpegQuality);
                imageWriter.write(null, new IIOImage(srcImage, null, null), iwp);
            } catch (Exception e) {
                e.printStackTrace();
            }
        } else {
            imageWriter.write(srcImage);
        }

    } else if ("webp".equals(ext)) {
        // --- WebP出力処理 ---
        ImageWriter imageWriter = null;
        Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("webp");
        if (writers.hasNext()) {
            imageWriter = writers.next();
        } else {
            throw new IOException("WebP writer not found. Make sure webp-imageio is in classpath.");
        }

        imageWriter.setOutput(ImageIO.createImageOutputStream(zos));
        ImageWriteParam param = imageWriter.getDefaultWriteParam();

        if (param.canWriteCompressed()) {
            param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
            param.setCompressionType("Lossy"); // "Lossless" も可
            param.setCompressionQuality(jpegQuality); // 0.0f~1.0f
        }

        imageWriter.write(null, new IIOImage(srcImage, null, null), param);
        imageWriter.dispose();

    } else {
        ImageIO.write(srcImage, ext, zos);
    }

    zos.flush();
}

webp-imageio(または luciad-webp-imageio)がクラスパスに必要です。

<dependency>
  <groupId>com.twelvemonkeys.imageio</groupId>
  <artifactId>imageio-webp</artifactId>
  <version>3.10.1</version>
</dependency>

<dependency>
  <groupId>com.twelvemonkeys.imageio</groupId>
  <artifactId>imageio-webp</artifactId>
  <version>3.10.1</version>
</dependency>

または:

<dependency>
  <groupId>com.luciad</groupId>
  <artifactId>luciad-webp-imageio</artifactId>
  <version>1.4.0</version>
</dependency>

<dependency>
  <groupId>com.luciad</groupId>
  <artifactId>luciad-webp-imageio</artifactId>
  <version>1.4.0</version>
</dependency>
項目 内容
🔸 ImageIO.scanForPlugins() アプリ起動時に1度呼んでおくと確実に検出される
🔸 jpegQuality WebPでも 0.0f~1.0f の範囲で圧縮率指定可能
🔸 Lossless 出力 param.setCompressionType("Lossless"); に切り替え
🔸 ストリーム zos は既に呼び出し元が管理しているので、close() せずに flush() のみ
🔸 RGBA画像 WebPは透過対応済み(ただしLossy時はやや非可逆)

もし main() がなくても、最初のどこか(例えば static {} ブロックなど)で呼んでおくと安心です:

static {
    try {
        ImageIO.scanForPlugins();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

static {
    try {
        ImageIO.scanForPlugins();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

この修正版は、

  • .webp を出力形式として指定したときに自動でWebP出力
  • JPEGと同じように jpegQuality が品質に反映される
  • 既存の呼び出しコードに変更不要

.webp を出力形式として指定したときに自動でWebP出力

JPEGと同じように jpegQuality が品質に反映される

既存の呼び出しコードに変更不要

です。

必要であれば「PNG/JPEG/WebPを自動選択してサイズ比較」や「ロスレスWebPに切り替える」バージョンも提示できます。 どちらを希望しますか?

あなた:

同じようにして

static private ImageWriter getPngImageWriter()
{
    if (pngImageWriter != null) return pngImageWriter;
    Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("png");
    pngImageWriter = writers.next();
    //jai-imageioのpngの挙動がおかしいのでインストールされていても使わない
    if (writers.hasNext() && pngImageWriter.getClass().getName().endsWith("CLibPNGImageWriter")) pngImageWriter = writers.next();
    return pngImageWriter;
}

static private ImageWriter getJpegImageWriter()
{
    if (jpegImageWriter!= null) return jpegImageWriter;
    Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("jpg");
    jpegImageWriter = writers.next();
    return jpegImageWriter;
}

ChatGPT:

完璧です。 この構成なら、getWebpImageWriter() を同じパターンで安全に追加できます。 WebPは追加プラグイン(webp-imageioやluciad-webp-imageio)で提供されるため、 存在確認とエラーハンドリングを入れておくのがポイントです。

static private ImageWriter webpImageWriter;

/** WebP ImageWriter を取得 */
static private ImageWriter getWebpImageWriter() {
    if (webpImageWriter != null) return webpImageWriter;

    Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("webp");

    if (!writers.hasNext()) {
        // プラグイン未登録エラー
        throw new IllegalStateException(
            "WebP ImageWriter not found. Make sure webp-imageio (or luciad-webp-imageio) is in the classpath, and call ImageIO.scanForPlugins()."
        );
    }

    webpImageWriter = writers.next();

    // 念のためクラス名チェック(複数プラグイン共存時)
    String className = webpImageWriter.getClass().getName();
    System.out.println("Using WebP writer: " + className);

    return webpImageWriter;
}

static private ImageWriter webpImageWriter;

/** WebP ImageWriter を取得 */
static private ImageWriter getWebpImageWriter() {
    if (webpImageWriter != null) return webpImageWriter;

    Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("webp");

    if (!writers.hasNext()) {
        // プラグイン未登録エラー
        throw new IllegalStateException(
            "WebP ImageWriter not found. Make sure webp-imageio (or luciad-webp-imageio) is in the classpath, and call ImageIO.scanForPlugins()."
        );
    }

    webpImageWriter = writers.next();

    // 念のためクラス名チェック(複数プラグイン共存時)
    String className = webpImageWriter.getClass().getName();
    System.out.println("Using WebP writer: " + className);

    return webpImageWriter;
}

前回の _writeImage() 内のこの部分を:

ImageWriter imageWriter = null;
Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("webp");
if (writers.hasNext()) {
    imageWriter = writers.next();
} else {
    throw new IOException("WebP writer not found. Make sure webp-imageio is in classpath.");
}

ImageWriter imageWriter = null;
Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("webp");
if (writers.hasNext()) {
    imageWriter = writers.next();
} else {
    throw new IOException("WebP writer not found. Make sure webp-imageio is in classpath.");
}

👇 次のように簡潔に置き換えられます。

ImageWriter imageWriter = getWebpImageWriter();

ImageWriter imageWriter = getWebpImageWriter();

このようにしておくと:

  • ImageWriter の再取得コストを回避(高速化)
  • スレッドセーフではないが、基本的に単一スレッドI/Oなら安全
  • メモリリーク防止(dispose() は呼び出し側で実行)

ImageWriter の再取得コストを回避(高速化)

スレッドセーフではないが、基本的に単一スレッドI/Oなら安全

メモリリーク防止(dispose() は呼び出し側で実行)

もし並列書き込みを行うなら、getWebpImageWriter() 内で都度 writers.next() を返すようにする方が安全です。

最終的に以下のように3種類揃えておくと美しいです。

static private ImageWriter pngImageWriter;
static private ImageWriter jpegImageWriter;
static private ImageWriter webpImageWriter;

static private ImageWriter getPngImageWriter() {
    if (pngImageWriter != null) return pngImageWriter;
    Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("png");
    pngImageWriter = writers.next();
    if (writers.hasNext() && pngImageWriter.getClass().getName().endsWith("CLibPNGImageWriter"))
        pngImageWriter = writers.next();
    return pngImageWriter;
}

static private ImageWriter getJpegImageWriter() {
    if (jpegImageWriter != null) return jpegImageWriter;
    Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("jpg");
    jpegImageWriter = writers.next();
    return jpegImageWriter;
}

static private ImageWriter getWebpImageWriter() {
    if (webpImageWriter != null) return webpImageWriter;
    Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("webp");
    if (!writers.hasNext()) {
        throw new IllegalStateException("WebP writer not found. Make sure webp-imageio or luciad-webp-imageio is on the classpath.");
    }
    webpImageWriter = writers.next();
    return webpImageWriter;
}

static private ImageWriter pngImageWriter;
static private ImageWriter jpegImageWriter;
static private ImageWriter webpImageWriter;

static private ImageWriter getPngImageWriter() {
    if (pngImageWriter != null) return pngImageWriter;
    Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("png");
    pngImageWriter = writers.next();
    if (writers.hasNext() && pngImageWriter.getClass().getName().endsWith("CLibPNGImageWriter"))
        pngImageWriter = writers.next();
    return pngImageWriter;
}

static private ImageWriter getJpegImageWriter() {
    if (jpegImageWriter != null) return jpegImageWriter;
    Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("jpg");
    jpegImageWriter = writers.next();
    return jpegImageWriter;
}

static private ImageWriter getWebpImageWriter() {
    if (webpImageWriter != null) return webpImageWriter;
    Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("webp");
    if (!writers.hasNext()) {
        throw new IllegalStateException("WebP writer not found. Make sure webp-imageio or luciad-webp-imageio is on the classpath.");
    }
    webpImageWriter = writers.next();
    return webpImageWriter;
}

必要であればこの getWebpImageWriter() をスレッドセーフにした(複数スレッド対応)版も出せます。 マルチスレッドで出力処理していますか?

あなた:

webp対応させて

/** 拡張子修正 大文字小文字は3パターンのみ */
public String correctExt(String srcImageFileName) {
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    //拡張子修正
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".png");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".jpg");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".jpeg");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".gif");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;

    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".PNG");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".JPG");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".JPEG");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".GIF");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;

    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".Png");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".Jpg");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".Jpeg");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".Gif");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;

    return null;
}

ChatGPT:

良いですね。 この correctExt() は既存の画像拡張子(PNG/JPEG/GIF)の大文字小文字ゆらぎを吸収する処理なので、 WebPを追加するには非常にシンプルに1行(または3パターン)追加すればOKです。

以下は既存構造をほぼ変えず、変更最小限で .webp, .WEBP, .Webp を追加したバージョンです。

/** 拡張子修正 大文字小文字は3パターンのみ */
public String correctExt(String srcImageFileName) {
    if (this.hasImage(srcImageFileName)) return srcImageFileName;

    // 拡張子修正(小文字)
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".png");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".jpg");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".jpeg");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".gif");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".webp");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;

    // 拡張子修正(大文字)
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".PNG");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".JPG");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".JPEG");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".GIF");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".WEBP");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;

    // 拡張子修正(先頭のみ大文字)
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".Png");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".Jpg");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".Jpeg");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".Gif");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".Webp");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;

    return null;
}

/** 拡張子修正 大文字小文字は3パターンのみ */
public String correctExt(String srcImageFileName) {
    if (this.hasImage(srcImageFileName)) return srcImageFileName;

    // 拡張子修正(小文字)
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".png");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".jpg");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".jpeg");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".gif");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".webp");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;

    // 拡張子修正(大文字)
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".PNG");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".JPG");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".JPEG");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".GIF");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".WEBP");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;

    // 拡張子修正(先頭のみ大文字)
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".Png");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".Jpg");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".Jpeg");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".Gif");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;
    srcImageFileName = srcImageFileName.replaceFirst("\\.\\w+$", ".Webp");
    if (this.hasImage(srcImageFileName)) return srcImageFileName;

    return null;
}
追加行 内容
小文字 .webp PNG/JPEG/GIFと同列に追加
大文字 .WEBP 大文字ブロックに追加
先頭のみ大文字 .Webp CamelCaseブロックに追加

このメソッドは、

  • 実ファイル (File)
  • Zip/Rar内のエントリ を共通的に確認しているはずなので、拡張子修正後に呼ぶだけでOKです。 WebPも既に ImageIO で読み込める状態になっている(ImageIO.scanForPlugins() 済み)なら問題ありません。

実ファイル (File)

Zip/Rar内のエントリ を共通的に確認しているはずなので、拡張子修正後に呼ぶだけでOKです。 WebPも既に ImageIO で読み込める状態になっている(ImageIO.scanForPlugins() 済み)なら問題ありません。

もしコード量を減らしたい場合は、 拡張子候補を配列化してループ処理にまとめることもできます。

public String correctExt(String srcImageFileName) {
    String[] exts = {
        "png", "jpg", "jpeg", "gif", "webp",
        "PNG", "JPG", "JPEG", "GIF", "WEBP",
        "Png", "Jpg", "Jpeg", "Gif", "Webp"
    };
    for (String ext : exts) {
        String name = srcImageFileName.replaceFirst("\\.\\w+$", "." + ext);
        if (this.hasImage(name)) return name;
    }
    return null;
}

public String correctExt(String srcImageFileName) {
    String[] exts = {
        "png", "jpg", "jpeg", "gif", "webp",
        "PNG", "JPG", "JPEG", "GIF", "WEBP",
        "Png", "Jpg", "Jpeg", "Gif", "Webp"
    };
    for (String ext : exts) {
        String name = srcImageFileName.replaceFirst("\\.\\w+$", "." + ext);
        if (this.hasImage(name)) return name;
    }
    return null;
}

このほうが保守しやすく、将来 .avif などを追加するときも1行で済みます。

どちらの書式(元の形を保ったまま/配列ループ化)で統一したいですか?

あなた:

webp対応させて

/** zip内の画像情報をすべて読み込み
 * @throws IOException */
public void loadZipImageInfos(File srcFile, boolean addFileName) throws IOException
{
    try (ZipArchiveInputStream zis = new ZipArchiveInputStream(new BufferedInputStream(new FileInputStream(srcFile), 65536), "MS932", false)) {
        ArchiveEntry entry;
        int idx = 0;
        while ((entry = zis.getNextEntry()) != null) {
            if (idx++ % 10 == 0) LogAppender.append(".");
            String entryName = entry.getName();
            String lowerName = entryName.toLowerCase();
            if (lowerName.endsWith(".png") || lowerName.endsWith(".jpg") || lowerName.endsWith(".jpeg") || lowerName.endsWith(".gif")) {
                ImageInfo imageInfo = null;
                try {
                    imageInfo = ImageInfo.getImageInfo(zis);
                } catch (Exception e) {
                    LogAppender.error("画像が読み込めませんでした: " + srcFile.getPath());
                    e.printStackTrace();
                }
                if (imageInfo != null) {
                    this.imageFileInfos.put(entryName, imageInfo);
                    if (addFileName) this.addImageFileName(entryName);
                }
            }
        }
    } finally {
        LogAppender.println();
    }
}

ChatGPT:

webp形式の画像にも対応させる場合、既存コードの構造をできるだけ変えずに拡張子チェック部分を調整すればOKです。 以下はあなたのコードに最小限の変更を加えた WebP対応版 です。

/** zip内の画像情報をすべて読み込み
 * @throws IOException */
public void loadZipImageInfos(File srcFile, boolean addFileName) throws IOException
{
    try (ZipArchiveInputStream zis = new ZipArchiveInputStream(
            new BufferedInputStream(new FileInputStream(srcFile), 65536), "MS932", false)) {

        ArchiveEntry entry;
        int idx = 0;

        while ((entry = zis.getNextEntry()) != null) {
            if (idx++ % 10 == 0) LogAppender.append(".");

            String entryName = entry.getName();
            String lowerName = entryName.toLowerCase();

            // ★ webp を追加
            if (lowerName.endsWith(".png") ||
                lowerName.endsWith(".jpg") ||
                lowerName.endsWith(".jpeg") ||
                lowerName.endsWith(".gif") ||
                lowerName.endsWith(".webp")) { // ← ここを追加

                ImageInfo imageInfo = null;
                try {
                    imageInfo = ImageInfo.getImageInfo(zis);
                } catch (Exception e) {
                    LogAppender.error("画像が読み込めませんでした: " + srcFile.getPath());
                    e.printStackTrace();
                }

                if (imageInfo != null) {
                    this.imageFileInfos.put(entryName, imageInfo);
                    if (addFileName) this.addImageFileName(entryName);
                }
            }
        }

    } finally {
        LogAppender.println();
    }
}

/** zip内の画像情報をすべて読み込み
 * @throws IOException */
public void loadZipImageInfos(File srcFile, boolean addFileName) throws IOException
{
    try (ZipArchiveInputStream zis = new ZipArchiveInputStream(
            new BufferedInputStream(new FileInputStream(srcFile), 65536), "MS932", false)) {

        ArchiveEntry entry;
        int idx = 0;

        while ((entry = zis.getNextEntry()) != null) {
            if (idx++ % 10 == 0) LogAppender.append(".");

            String entryName = entry.getName();
            String lowerName = entryName.toLowerCase();

            // ★ webp を追加
            if (lowerName.endsWith(".png") ||
                lowerName.endsWith(".jpg") ||
                lowerName.endsWith(".jpeg") ||
                lowerName.endsWith(".gif") ||
                lowerName.endsWith(".webp")) { // ← ここを追加

                ImageInfo imageInfo = null;
                try {
                    imageInfo = ImageInfo.getImageInfo(zis);
                } catch (Exception e) {
                    LogAppender.error("画像が読み込めませんでした: " + srcFile.getPath());
                    e.printStackTrace();
                }

                if (imageInfo != null) {
                    this.imageFileInfos.put(entryName, imageInfo);
                    if (addFileName) this.addImageFileName(entryName);
                }
            }
        }

    } finally {
        LogAppender.println();
    }
}
  1. .webp拡張子チェックを追加 既存の endsWith() チェックに .webp を加えるだけで対応可能です。 WebPの大文字・小文字(例: .WEBP)も許可したい場合は .toLowerCase() を既に使用しているためそのままでOKです。
  2. ImageIOでのWebP対応 この関数は画像データを解析するだけなので、 実際にWebP画像を読み取るには前もって

java コードをコピーする ImageIO.scanForPlugins();

を呼び出し、 webp-imageio(例: com.luciad.imageio.webp.WebPImageReaderSpi)が登録されている必要があります。

通常はアプリ起動時(main関数や静的初期化ブロック)で以下のように一度呼び出しておくと確実です:

java コードをコピーする static { ImageIO.scanForPlugins(); }

.webp拡張子チェックを追加 既存の endsWith() チェックに .webp を加えるだけで対応可能です。 WebPの大文字・小文字(例: .WEBP)も許可したい場合は .toLowerCase() を既に使用しているためそのままでOKです。

ImageIOでのWebP対応 この関数は画像データを解析するだけなので、 実際にWebP画像を読み取るには前もって

ImageIO.scanForPlugins();

ImageIO.scanForPlugins();

を呼び出し、 webp-imageio(例: com.luciad.imageio.webp.WebPImageReaderSpi)が登録されている必要があります。

通常はアプリ起動時(main関数や静的初期化ブロック)で以下のように一度呼び出しておくと確実です:

static {
    ImageIO.scanForPlugins();
}

static {
    ImageIO.scanForPlugins();
}

希望があれば、ImageInfo.getImageInfo(zis) が WebP に未対応だった場合のフォールバック(例: BufferedImage image = ImageIO.read(...) で強制読み込み)を組み込んだ堅牢版も提示できます。 作りたいのは「最小変更版」と「安全フォールバック付き版」どちらにしますか?

あなた:

webp対応させて

/** rar内の画像情報をすべて読み込み */
public void loadRarImageInfos(File srcFile, boolean addFileName) throws IOException, RarException
{
    try (Archive archive = new Archive(srcFile)) {
        int idx = 0;
        for (FileHeader fileHeader : archive.getFileHeaders()) {
            if (idx++ % 10 == 0) LogAppender.append(".");
            if (!fileHeader.isDirectory()) {
                String entryName = fileHeader.getFileName();
                entryName = entryName.replace('\\', '/');
                String lowerName = entryName.toLowerCase();
                if (lowerName.endsWith(".png") || lowerName.endsWith(".jpg") || lowerName.endsWith(".jpeg") || lowerName.endsWith(".gif")) {
                    ImageInfo imageInfo;
                    InputStream is = null;
                    try {
                        //読めない場合があるので一旦バイト配列に読み込み
                        ByteArrayOutputStream baos = new ByteArrayOutputStream();
                        archive.extractFile(fileHeader, baos);
                        baos.close();
                        is = new ByteArrayInputStream(baos.toByteArray());
                        imageInfo = ImageInfo.getImageInfo(is);
                        if (imageInfo != null) {
                            this.imageFileInfos.put(entryName, imageInfo);
                            if (addFileName) this.addImageFileName(entryName);
                        } else {
                            LogAppender.println();
                            LogAppender.error("画像が読み込めませんでした: " + entryName);
                        }
                    } catch (Exception e) {
                        LogAppender.println();
                        LogAppender.error("画像が読み込めませんでした: " + entryName);
                        e.printStackTrace();
                    } finally {
                        if (is != null) is.close();
                    }
                }
            }
        }
    } finally {
        LogAppender.println();
    }
}

ChatGPT:

良いですね 👍 この RAR 内画像読み込み処理も、ZIP のときと同様に .webp を追加すれば対応できます。 既存の構造を崩さず、最小限の修正で WebP を扱えるようにした完全版を以下に示します。

/** rar内の画像情報をすべて読み込み */
public void loadRarImageInfos(File srcFile, boolean addFileName) throws IOException, RarException
{
    try (Archive archive = new Archive(srcFile)) {
        int idx = 0;
        for (FileHeader fileHeader : archive.getFileHeaders()) {
            if (idx++ % 10 == 0) LogAppender.append(".");
            if (!fileHeader.isDirectory()) {
                String entryName = fileHeader.getFileName();
                entryName = entryName.replace('\\', '/');
                String lowerName = entryName.toLowerCase();

                // ★ WebP対応(拡張子チェックを追加)
                if (lowerName.endsWith(".png") ||
                    lowerName.endsWith(".jpg") ||
                    lowerName.endsWith(".jpeg") ||
                    lowerName.endsWith(".gif") ||
                    lowerName.endsWith(".webp")) { // ←ここを追加

                    ImageInfo imageInfo;
                    InputStream is = null;
                    try {
                        // 読めない場合があるので一旦バイト配列に読み込み
                        ByteArrayOutputStream baos = new ByteArrayOutputStream();
                        archive.extractFile(fileHeader, baos);
                        baos.close();

                        is = new ByteArrayInputStream(baos.toByteArray());
                        imageInfo = ImageInfo.getImageInfo(is);

                        if (imageInfo != null) {
                            this.imageFileInfos.put(entryName, imageInfo);
                            if (addFileName) this.addImageFileName(entryName);
                        } else {
                            LogAppender.println();
                            LogAppender.error("画像が読み込めませんでした: " + entryName);
                        }

                    } catch (Exception e) {
                        LogAppender.println();
                        LogAppender.error("画像が読み込めませんでした: " + entryName);
                        e.printStackTrace();
                    } finally {
                        if (is != null) is.close();
                    }
                }
            }
        }
    } finally {
        LogAppender.println();
    }
}

/** rar内の画像情報をすべて読み込み */
public void loadRarImageInfos(File srcFile, boolean addFileName) throws IOException, RarException
{
    try (Archive archive = new Archive(srcFile)) {
        int idx = 0;
        for (FileHeader fileHeader : archive.getFileHeaders()) {
            if (idx++ % 10 == 0) LogAppender.append(".");
            if (!fileHeader.isDirectory()) {
                String entryName = fileHeader.getFileName();
                entryName = entryName.replace('\\', '/');
                String lowerName = entryName.toLowerCase();

                // ★ WebP対応(拡張子チェックを追加)
                if (lowerName.endsWith(".png") ||
                    lowerName.endsWith(".jpg") ||
                    lowerName.endsWith(".jpeg") ||
                    lowerName.endsWith(".gif") ||
                    lowerName.endsWith(".webp")) { // ←ここを追加

                    ImageInfo imageInfo;
                    InputStream is = null;
                    try {
                        // 読めない場合があるので一旦バイト配列に読み込み
                        ByteArrayOutputStream baos = new ByteArrayOutputStream();
                        archive.extractFile(fileHeader, baos);
                        baos.close();

                        is = new ByteArrayInputStream(baos.toByteArray());
                        imageInfo = ImageInfo.getImageInfo(is);

                        if (imageInfo != null) {
                            this.imageFileInfos.put(entryName, imageInfo);
                            if (addFileName) this.addImageFileName(entryName);
                        } else {
                            LogAppender.println();
                            LogAppender.error("画像が読み込めませんでした: " + entryName);
                        }

                    } catch (Exception e) {
                        LogAppender.println();
                        LogAppender.error("画像が読み込めませんでした: " + entryName);
                        e.printStackTrace();
                    } finally {
                        if (is != null) is.close();
                    }
                }
            }
        }
    } finally {
        LogAppender.println();
    }
}
  1. .webp拡張子の追加だけで十分

ImageIO.scanForPlugins() を呼び出して webp-imageio が有効になっていれば、 ImageInfo.getImageInfo() が内部で WebP を正しく解析できます。 (Luciad の WebPImageReaderSpi を利用) 2. ImageIO.scanForPlugins() を呼び出して webp-imageio が有効になっていれば、 ImageInfo.getImageInfo() が内部で WebP を正しく解析できます。 (Luciad の WebPImageReaderSpi を利用) 3. ImageIO.scanForPlugins() の呼び出し場所

RAR / ZIP 共通で一度だけ呼べばOK。 通常はアプリ起動時や静的初期化ブロックに入れます。

java コードをコピーする static { ImageIO.scanForPlugins(); }

  1. RAR / ZIP 共通で一度だけ呼べばOK。 通常はアプリ起動時や静的初期化ブロックに入れます。

java コードをコピーする static { ImageIO.scanForPlugins(); }

  1. WebP非対応環境への安全策(任意)

ImageInfo.getImageInfo() が WebP を認識できない場合、 代替として ImageIO.read() にフォールバックするコードを追加できます。 例:

java コードをコピーする if (imageInfo == null) { BufferedImage img = ImageIO.read(is); if (img != null) { imageInfo = new ImageInfo(img.getWidth(), img.getHeight()); } }

ただしメモリ負荷が高くなるため、WebPが確実にサポートされる環境なら不要です。 6. ImageInfo.getImageInfo() が WebP を認識できない場合、 代替として ImageIO.read() にフォールバックするコードを追加できます。 例:

java コードをコピーする if (imageInfo == null) { BufferedImage img = ImageIO.read(is); if (img != null) { imageInfo = new ImageInfo(img.getWidth(), img.getHeight()); } }

ただしメモリ負荷が高くなるため、WebPが確実にサポートされる環境なら不要です。

.webp拡張子の追加だけで十分

  • ImageIO.scanForPlugins() を呼び出して webp-imageio が有効になっていれば、 ImageInfo.getImageInfo() が内部で WebP を正しく解析できます。 (Luciad の WebPImageReaderSpi を利用)

ImageIO.scanForPlugins() を呼び出して webp-imageio が有効になっていれば、 ImageInfo.getImageInfo() が内部で WebP を正しく解析できます。 (Luciad の WebPImageReaderSpi を利用)

ImageIO.scanForPlugins() の呼び出し場所

  • RAR / ZIP 共通で一度だけ呼べばOK。 通常はアプリ起動時や静的初期化ブロックに入れます。

java コードをコピーする static { ImageIO.scanForPlugins(); }

RAR / ZIP 共通で一度だけ呼べばOK。 通常はアプリ起動時や静的初期化ブロックに入れます。

static {
    ImageIO.scanForPlugins();
}

static {
    ImageIO.scanForPlugins();
}

WebP非対応環境への安全策(任意)

  • ImageInfo.getImageInfo() が WebP を認識できない場合、 代替として ImageIO.read() にフォールバックするコードを追加できます。 例:

java コードをコピーする if (imageInfo == null) { BufferedImage img = ImageIO.read(is); if (img != null) { imageInfo = new ImageInfo(img.getWidth(), img.getHeight()); } }

ただしメモリ負荷が高くなるため、WebPが確実にサポートされる環境なら不要です。

ImageInfo.getImageInfo() が WebP を認識できない場合、 代替として ImageIO.read() にフォールバックするコードを追加できます。 例:

if (imageInfo == null) {
    BufferedImage img = ImageIO.read(is);
    if (img != null) {
        imageInfo = new ImageInfo(img.getWidth(), img.getHeight());
    }
}

if (imageInfo == null) {
    BufferedImage img = ImageIO.read(is);
    if (img != null) {
        imageInfo = new ImageInfo(img.getWidth(), img.getHeight());
    }
}

ただしメモリ負荷が高くなるため、WebPが確実にサポートされる環境なら不要です。

ご希望があれば、 🔹「フォールバック付き堅牢版」 🔹「読み込み失敗時に自動でPNG変換して格納する版」 のどちらかを作成することも可能です。 どちらにしましょうか?

あなた:

webp対応させて

@Override
public String getImageFilePath(String srcImageFileName, int lineNum)
{
    boolean isCover = false;
    this.imageIndex++; //xhtmlと画像ファイル名の番号を合わせるため先に++
    String ext = "";
    try { ext = srcImageFileName.substring(srcImageFileName.lastIndexOf('.')+1).toLowerCase(); } catch (Exception e) {}
    String imageId = decimalFormat.format(this.imageIndex);
    String imageFileName = IMAGES_PATH+imageId+"."+ext;
    ImageInfo imageInfo;
    try {
        imageInfo = this.imageInfoReader.getImageInfo(srcImageFileName);
        imageInfo.setId(imageId);
        imageInfo.setOutFileName(imageId+"."+ext);
        if (!imageInfo.getExt().matches("^(png|jpeg|gif|jpg)$")) {
            LogAppender.error(lineNum, "画像フォーマットエラー", srcImageFileName);
            return null;
        }
        if (this.imageIndex-1 == bookInfo.coverImageIndex) {
            imageInfo.setIsCover(true);
            isCover = true;
        }
        this.imageInfos.add(imageInfo);

    } catch (IOException e) {
        e.printStackTrace();
        return null;
    }
    //先頭に表紙ページ移動の場合でカバーページならnullを返して本文中から削除
    if (bookInfo.insertCoverPage && isCover) return null;
    return "../"+imageFileName;
}

ChatGPT:

いいですね 👍 このメソッドも既存の構造を崩さず、WebP形式を受け入れるようにするだけでOKです。 下記はあなたのコードに最小限の変更を加えて、WebP対応させた完全版です👇

@Override
public String getImageFilePath(String srcImageFileName, int lineNum)
{
    boolean isCover = false;
    this.imageIndex++; // xhtmlと画像ファイル名の番号を合わせるため先に++
    String ext = "";
    try {
        ext = srcImageFileName.substring(srcImageFileName.lastIndexOf('.') + 1).toLowerCase();
    } catch (Exception e) {}

    String imageId = decimalFormat.format(this.imageIndex);
    String imageFileName = IMAGES_PATH + imageId + "." + ext;
    ImageInfo imageInfo;

    try {
        imageInfo = this.imageInfoReader.getImageInfo(srcImageFileName);
        imageInfo.setId(imageId);
        imageInfo.setOutFileName(imageId + "." + ext);

        // ★ WebP対応: 拡張子チェックに webp を追加
        if (!imageInfo.getExt().matches("^(png|jpeg|gif|jpg|webp)$")) {
            LogAppender.error(lineNum, "画像フォーマットエラー", srcImageFileName);
            return null;
        }

        if (this.imageIndex - 1 == bookInfo.coverImageIndex) {
            imageInfo.setIsCover(true);
            isCover = true;
        }

        this.imageInfos.add(imageInfo);

    } catch (IOException e) {
        e.printStackTrace();
        return null;
    }

    // 先頭に表紙ページ移動の場合でカバーページならnullを返して本文中から削除
    if (bookInfo.insertCoverPage && isCover) return null;

    return "../" + imageFileName;
}

@Override
public String getImageFilePath(String srcImageFileName, int lineNum)
{
    boolean isCover = false;
    this.imageIndex++; // xhtmlと画像ファイル名の番号を合わせるため先に++
    String ext = "";
    try {
        ext = srcImageFileName.substring(srcImageFileName.lastIndexOf('.') + 1).toLowerCase();
    } catch (Exception e) {}

    String imageId = decimalFormat.format(this.imageIndex);
    String imageFileName = IMAGES_PATH + imageId + "." + ext;
    ImageInfo imageInfo;

    try {
        imageInfo = this.imageInfoReader.getImageInfo(srcImageFileName);
        imageInfo.setId(imageId);
        imageInfo.setOutFileName(imageId + "." + ext);

        // ★ WebP対応: 拡張子チェックに webp を追加
        if (!imageInfo.getExt().matches("^(png|jpeg|gif|jpg|webp)$")) {
            LogAppender.error(lineNum, "画像フォーマットエラー", srcImageFileName);
            return null;
        }

        if (this.imageIndex - 1 == bookInfo.coverImageIndex) {
            imageInfo.setIsCover(true);
            isCover = true;
        }

        this.imageInfos.add(imageInfo);

    } catch (IOException e) {
        e.printStackTrace();
        return null;
    }

    // 先頭に表紙ページ移動の場合でカバーページならnullを返して本文中から削除
    if (bookInfo.insertCoverPage && isCover) return null;

    return "../" + imageFileName;
}
  • 正規表現部分

java コードをコピーする if (!imageInfo.getExt().matches("^(png|jpeg|gif|jpg|webp)$"))

これで .webp が正式に許可されます。 (既存の .jpg / .jpeg とも両立) - ImageIO.scanForPlugins() 呼び出し webp-imageio を使うなら、 アプリ起動時またはクラス初期化時に以下を一度呼び出すのを忘れずに:

java コードをコピーする static { ImageIO.scanForPlugins(); }

  • 出力側の _writeImage も対応済みなら 入出力ともに .webp に完全対応できます。 読み込み、コピー、ZIP格納まで動作します。

正規表現部分

if (!imageInfo.getExt().matches("^(png|jpeg|gif|jpg|webp)$"))

if (!imageInfo.getExt().matches("^(png|jpeg|gif|jpg|webp)$"))

これで .webp が正式に許可されます。 (既存の .jpg / .jpeg とも両立)

ImageIO.scanForPlugins() 呼び出し webp-imageio を使うなら、 アプリ起動時またはクラス初期化時に以下を一度呼び出すのを忘れずに:

static {
    ImageIO.scanForPlugins();
}

static {
    ImageIO.scanForPlugins();
}

出力側の _writeImage も対応済みなら 入出力ともに .webp に完全対応できます。 読み込み、コピー、ZIP格納まで動作します。

もし .webp 画像を 一律で .jpg や .png に変換出力したい(Kindle互換など)場合は、 ext が "webp" のときに強制的に "jpg" に変える処理を入れることもできます。 希望しますか?