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

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

EPUBのカバー画像の保存処理

JavaではZipOutputStreamで画像を渡していたが、Jszipではどのようにするのが最適か。 Javaだとストリームで渡すが、JSではバッファーで渡す。

You:

JSZIPを使ってJSに書き換えるときに画像ファイルの扱い方は

     //表紙指定があればそれを入力に設定 先頭画像のisCoverはfalseになっている
        //プレビューで編集された場合はここで追加する
        ////////////////////////////////
        //表紙編集時のイメージ出力
        if (coverImageInfo != null) {
            try {
                //kindleの場合は常にjpegに変換
                if (isKindle) {
                    String imgExt = coverImageInfo.getExt();
                    if (!imgExt.startsWith("jp")) {
                        if (bookInfo.coverImage == null) {
                            ByteArrayInputStream bais = new ByteArrayInputStream(Objects.requireNonNull(coverImageBytes));
                            bookInfo.coverImage = ImageUtils.readImage(imgExt, bais);
                            bais.close();
                        }
                        coverImageInfo.setExt("jpeg");
                    }
                }
                if (bookInfo.coverImage != null) {
                    //プレビューで編集されている場合
                    zos.putArchiveEntry(new ZipArchiveEntry(OPS_PATH+IMAGES_PATH+coverImageInfo.getOutFileName()));
                    this.writeCoverImage(bookInfo.coverImage, zos, coverImageInfo);
                    zos.closeArchiveEntry();
                    bookInfo.coverImage = null; //同じ画像が使われている場合は以後はファイルから読み込ませる
                } else {
                    ByteArrayInputStream bais = new ByteArrayInputStream(Objects.requireNonNull(coverImageBytes));
                    zos.putArchiveEntry(new ZipArchiveEntry(OPS_PATH+IMAGES_PATH+coverImageInfo.getOutFileName()));
                    this.writeCoverImage(bais, zos, coverImageInfo);
                    zos.closeArchiveEntry();
                    bais.close();
                }
                imageInfos.removeFirst();//カバー画像は出力済みなので削除
                if (this.jProgressBar != null) this.jProgressBar.setValue(this.jProgressBar.getValue()+10);
            } catch (Exception e) {
                e.printStackTrace();
                LogAppender.error("表紙画像取得エラー: "+bookInfo.coverFileName);
            }
        }
        if (this.canceled) return;

ChatGPT:

Javaコードは ZipOutputStream に画像を直接書き込む処理ですが、
JSZip では エントリを書き込むのではなく file() で追加します。

また Java の

  • ByteArrayInputStream

  • ZipArchiveEntry

  • putArchiveEntry()

Nodeでは不要になります。


JSZip版(Node.js移植)

JavaScript

import JSZip from "jszip";  
  
////////////////////////////////  
// 表紙編集時のイメージ出力  
if (coverImageInfo != null) {  
  try {  
  
    // kindleの場合は常にjpegに変換  
    if (isKindle) {  
      let imgExt \= coverImageInfo.getExt();  
  
      if (!imgExt.startsWith("jp")) {  
        if (bookInfo.coverImage \== null) {  
          bookInfo.coverImage \= await ImageUtils.readImage(  
            imgExt,  
            coverImageBytes  
          );  
        }  
        coverImageInfo.setExt("jpeg");  
      }  
    }  
  
    const zipPath \= OPS\_PATH + IMAGES\_PATH + coverImageInfo.getOutFileName();  
  
    if (bookInfo.coverImage != null) {  
      // プレビューで編集されている場合  
      const buffer \= await this.writeCoverImage(  
        bookInfo.coverImage,  
        coverImageInfo  
      );  
  
      this.zos.file(zipPath, buffer);  
  
      // 同じ画像が使われている場合は以後はファイルから読み込ませる  
      bookInfo.coverImage \= null;  
  
    } else {  
      const buffer \= await this.writeCoverImage(  
        coverImageBytes,  
        coverImageInfo  
      );  
  
      this.zos.file(zipPath, buffer);  
    }  
  
    // カバー画像は出力済みなので削除  
    imageInfos.shift();  
  
    if (this.jProgressBar != null) {  
      this.jProgressBar.setValue(this.jProgressBar.getValue() + 10);  
    }  
  
  } catch (e) {  
    console.error(e);  
    LogAppender.error("表紙画像取得エラー: " + bookInfo.coverFileName);  
  }  
}  
  
if (this.canceled) return;

Java → JSZip 対応表

Java JSZip
putArchiveEntry() zip.file(path, data)
ZipArchiveEntry 不要
ByteArrayInputStream Buffer
closeArchiveEntry() 不要
removeFirst() shift()

重要ポイント(AozoraEpub3移植)

Javaでは

Java

zos.write(...)

stream書き込みですが
JSZipでは

JavaScript

zip.file(path, buffer)

Bufferを丸ごと追加します。

つまり writeCoverImage()Bufferを返す関数にすると移植が楽です。

JavaScript

async writeCoverImage(image, coverImageInfo) {  
  return ImageUtils.encodeImage(image, coverImageInfo.getExt());  
}

💡 AozoraEpub3をJSに移植するときの最大の高速化ポイント

JSZipは

JavaScript

generateAsync({type:"nodebuffer",compression:"DEFLATE",compressionOptions:{level:9}})

を最後に 1回だけ呼ぶのが重要です。

途中で generateAsync() を呼ぶと
10〜30倍遅くなります。


希望があれば次に AozoraEpub3 Java → JS移植で一番重要な部分

  • writeImage()

  • ImageUtils.readImage()

  • 画像リサイズ

の **Node版実装(sharp / jimp 互換)**も出せます。
ここが 移植の一番難しい部分です。

You:

writeCoverImageをどのように書き換えるべきか /** 表紙画像を出力 編集済の画像なのでリサイズしない */ void writeCoverImage(BufferedImage srcImage, ZipArchiveOutputStream zos, ImageInfo imageInfo) { imageInfo.rotateAngle = 0; //回転させない ImageUtils.writeImage(null, srcImage, zos,imageInfo, this.jpegQuality, this.gammaOp, 0, 0, 0, this.dispW, this.dispH, 0, 0, 0, 0, 0, 0); } /** 表紙画像を出力 */ void writeCoverImage(InputStream is, ZipArchiveOutputStream zos, ImageInfo imageInfo) { imageInfo.rotateAngle = 0; //回転させない ImageUtils.writeImage(is, null, zos,imageInfo, this.jpegQuality, this.gammaOp, 0, this.coverW, this.coverH, this.dispW, this.dispH, 0, 0, 0, 0, 0, 0); } /** 画像を出力 */ void writeImage(InputStream is,ZipArchiveOutputStream zos, ImageInfo imageInfo) { ImageUtils.writeImage(is, null, zos, imageInfo, this.jpegQuality, this.gammaOp, this.maxImagePixels, this.maxImageW, this.maxImageH, this.dispW, this.dispH, this.autoMarginLimitH, this.autoMarginLimitV, this.autoMarginWhiteLevel, this.autoMarginPadding, this.autoMarginNombre, this.autoMarginNombreSize); } /** 画像を出力 */ void writeImage(BufferedImage srcImage, ZipArchiveOutputStream zos, ImageInfo imageInfo) { ImageUtils.writeImage(null, srcImage, zos, imageInfo, this.jpegQuality, this.gammaOp, this.maxImagePixels, this.maxImageW, this.maxImageH, this.dispW, this.dispH, this.autoMarginLimitH, this.autoMarginLimitV, this.autoMarginWhiteLevel, this.autoMarginPadding, this.autoMarginNombre, this.autoMarginNombreSize); }

ChatGPT:

Java版は

  • ZipArchiveOutputStream直接書き込み

  • BufferedImage または InputStream

  • 最終的に ImageUtils.writeImage()zos.write() する

構造です。

しかし JSZip
ストリーム書き込みができないので

👉 画像を書き出して Buffer を返す関数に変えるのが一番移植しやすいです。


JS版の設計(重要)

Java

writeCoverImage(...)  
   ↓  
ImageUtils.writeImage(...)  
   ↓  
zos.write(...)

JS

writeCoverImage(...)  
   ↓  
ImageUtils.writeImage(...)  
   ↓  
return Buffer  
   ↓  
zip.file(path, buffer)

JS版 writeCoverImage

JavaScript

async writeCoverImage(srcImage, imageInfo) {  
  imageInfo.rotateAngle \= 0; // 回転させない  
  
  return await ImageUtils.writeImage(  
    null,  
    srcImage,  
    imageInfo,  
    this.jpegQuality,  
    this.gammaOp,  
    0, 0, 0,  
    this.dispW,  
    this.dispH,  
    0, 0, 0, 0, 0, 0  
  );  
}  
  
async writeCoverImageFromStream(is, imageInfo) {  
  imageInfo.rotateAngle \= 0; // 回転させない  
  
  return await ImageUtils.writeImage(  
    is,  
    null,  
    imageInfo,  
    this.jpegQuality,  
    this.gammaOp,  
    0,  
    this.coverW,  
    this.coverH,  
    this.dispW,  
    this.dispH,  
    0, 0, 0, 0, 0, 0  
  );  
}

JS版 writeImage

JavaScript

async writeImage(is, imageInfo) {  
  return await ImageUtils.writeImage(  
    is,  
    null,  
    imageInfo,  
    this.jpegQuality,  
    this.gammaOp,  
    this.maxImagePixels,  
    this.maxImageW,  
    this.maxImageH,  
    this.dispW,  
    this.dispH,  
    this.autoMarginLimitH,  
    this.autoMarginLimitV,  
    this.autoMarginWhiteLevel,  
    this.autoMarginPadding,  
    this.autoMarginNombre,  
    this.autoMarginNombreSize  
  );  
}  
  
async writeImageFromBuffered(srcImage, imageInfo) {  
  return await ImageUtils.writeImage(  
    null,  
    srcImage,  
    imageInfo,  
    this.jpegQuality,  
    this.gammaOp,  
    this.maxImagePixels,  
    this.maxImageW,  
    this.maxImageH,  
    this.dispW,  
    this.dispH,  
    this.autoMarginLimitH,  
    this.autoMarginLimitV,  
    this.autoMarginWhiteLevel,  
    this.autoMarginPadding,  
    this.autoMarginNombre,  
    this.autoMarginNombreSize  
  );  
}

呼び出し側(JSZip)

JavaScript

const buffer \= await this.writeCoverImage(bookInfo.coverImage, coverImageInfo);  
  
this.zos.file(  
  OPS\_PATH + IMAGES\_PATH + coverImageInfo.getOutFileName(),  
  buffer  
);

Java → JS 対応

Java JS
ZipArchiveOutputStream zos JSZip
zos.write() zip.file()
BufferedImage Imageオブジェクト or Buffer
InputStream Buffer

AozoraEpub3移植で一番重要なポイント

Javaの

ImageUtils.writeImage(...)

  • リサイズ

  • 回転

  • 余白除去

  • JPEG変換

  • ガンマ補正

を全部やっています。

Nodeでは普通

  • sharp

  • Jimp

のどちらかで書き直します。


💡 実はAozoraEpub3移植で一番楽な方法

ImageUtils.writeImage()1つのJS関数にまとめる
Javaコードの 90%がそのまま移植できます。

もし希望があれば次に

AozoraEpub3完全互換の ImageUtils.writeImage() Node版(約150行)

を出せます。
これがあると 画像処理部分がほぼそのまま動きます。

you: JSに書き換えて

public class ImageUtils
{
    static public void writeImage(InputStream is, BufferedImage srcImage, ZipArchiveOutputStream zos, ImageInfo imageInfo,
            float jpegQuality, LookupOp gammaOp, int maxImagePixels, int maxImageW, int maxImageH, int dispW, int dispH,
            int autoMarginLimitH, int autoMarginLimitV, int autoMarginWhiteLevel, float autoMarginPadding, int autoMarginNombre, float nombreSize) {
        try {
        String ext = imageInfo.getExt();

        int imgW = imageInfo.getWidth();
        int imgH = imageInfo.getHeight();
        int w = imgW;
        int h = imgH;
        imageInfo.setOutWidth(imgW);
        imageInfo.setOutHeight(imgH);
        //余白チェック時に読み込んだ画像のバッファ
        byte[] imgBuf = null;

        //回転とコントラスト調整なら読み込んでおく
        if (srcImage == null && (imageInfo.rotateAngle != 0 || gammaOp != null)) srcImage = readImage(ext, is);

        int[] margin = null;
        if (autoMarginLimitH > 0 || autoMarginLimitV > 0) {
            int startPixel = (int)(w*0.01); //1%
            int ignoreEdge = (int)(w*0.03); //3%
            int dustSize = (int)(w*0.01); //1%

            //画像がなければ読み込み 変更なしの時にそのまま出力できるように一旦バッファに読み込む
            if (srcImage == null) {
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                //IOUtils.copy(is, baos);
                is.transferTo(baos);
                imgBuf = baos.toByteArray();
                try (ByteArrayInputStream bais = new ByteArrayInputStream(imgBuf)) {
                    srcImage = readImage(ext, bais);
                }
            }
            margin = getPlainMargin(srcImage, autoMarginLimitH/100f, autoMarginLimitV/100f, autoMarginWhiteLevel/100f, autoMarginPadding/100f, startPixel, ignoreEdge, dustSize, autoMarginNombre, nombreSize);
            if (margin[0]==0 && margin[1]==0 && margin[2]==0 && margin[3]==0) margin = null;
            if (margin != null) {
                //元画像が幅か高さかチェック
                int mw = w-margin[0]-margin[2];
                int mh = h-margin[1]-margin[3];
                double dWH = dispW/(double)dispH;
                double mWH = mw/(double)mh;
                //縦横比で画面の横か縦に合わせる方向が変わらないようにマージンを調整する
                if (w/(double)h < dWH) { //元が縦
                    if (mWH > dWH && mw > dispW) { //余白除去で横にはみ出す
                        mh = (int)(mw/dWH);
                        margin[3] = h-margin[1]-mh;//下マージンを伸ばす
                        if (margin[3] < 0) { margin[3] = 0; margin[1] = h-mh; }
                    }
                } else { //元が横
                    if (mWH < dWH && mh > dispH) { //余白除去で縦にはみ出す
                        mw = (int)(mh*dWH);
                        double mLR = margin[0]+margin[2];
                        margin[0] = (int)((w-mw)*margin[0]/mLR);
                        margin[2] = (int)((w-mw)*margin[2]/mLR);
                    }
                }
                w = mw;
                h = mh;
            }
        }
        //倍率取得
        double scale = 1;
        if (maxImagePixels >= 10000) scale = Math.sqrt((double)maxImagePixels/(w*h)); //最大画素数指定
        if (maxImageW > 0) scale = Math.min(scale, (double)maxImageW/w); //最大幅指定
        if (maxImageH > 0) scale = Math.min(scale, (double)maxImageH/h); //最大高さ指定

        if (scale >= 1 && (gammaOp == null || srcImage.getType() == BufferedImage.TYPE_INT_RGB)) {
            if (srcImage == null) {
                //変更なしならそのままファイル出力
                //IOUtils.copy(is, zos);
                is.transferTo(zos);
            } else {
                if (margin == null && imgBuf != null && imageInfo.rotateAngle==0) {
                    //余白除去が無く画像も編集されていなければバッファからそのまま出力
                    try (ByteArrayInputStream bais = new ByteArrayInputStream(imgBuf)) {
                        bais.transferTo(zos);
                    }
                } else {
                    //編集済の画像なら同じ画像形式で書き出し 余白があれば切り取る
                    if (imageInfo.rotateAngle != 0) {
                        BufferedImage outImage = new BufferedImage(h, w, srcImage.getType());
                        Graphics2D g = outImage.createGraphics();
                        try {
                            g.setColor(Color.WHITE);
                            g.fillRect(0, 0, h, w);
                            g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
                            g.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
                            int x = 0; int y = 0;
                            AffineTransform at;
                            if (imageInfo.rotateAngle == 90) {
                                at = AffineTransform.getQuadrantRotateInstance(1, 0, 0);
                                at.translate(0, -imgH);
                                if (margin != null) {
                                    x = -margin[3];
                                    y = -margin[0];
                                }
                            } else {
                                at = AffineTransform.getQuadrantRotateInstance(-1, 0, 0);
                                at.translate(-imgW, 0);
                                if (margin != null) {
                                    x = -margin[1];
                                    y = -margin[2];
                                }
                            }
                            AffineTransformOp ato = new AffineTransformOp(at, AffineTransformOp.TYPE_BICUBIC);
                            g.drawImage(srcImage, ato, x, y);
                        } finally {
                            g.dispose();
                        }
                        srcImage = outImage;//入れ替え
                    } else if (margin != null) srcImage = srcImage.getSubimage(margin[0], margin[1], srcImage.getWidth()-margin[2]-margin[0], srcImage.getHeight()-margin[3]-margin[1]);
                    if (gammaOp != null) {
                        BufferedImage filterdImage = new BufferedImage(srcImage.getWidth(), srcImage.getHeight(), BufferedImage.TYPE_INT_RGB);
                        srcImage = gammaOp.filter(srcImage, filterdImage);
                        srcImage = filterdImage;
                    }
                    _writeImage(zos, srcImage, ext, jpegQuality);
                    imageInfo.setOutWidth(srcImage.getWidth());
                    imageInfo.setOutHeight(srcImage.getHeight());
                    if (imageInfo.rotateAngle != 0) LogAppender.println("画像回転"+": "+imageInfo.getOutFileName()+" ("+h+","+w+")");
                }
            }
        } else {
            //縮小
            int scaledW = (int)(w*scale+0.5);
            int scaledH = (int)(h*scale+0.5);
            if (imageInfo.rotateAngle != 0) {
                scaledW = (int)(h*scale+0.5);
                scaledH = (int)(w*scale+0.5);
            }
            //画像がなければ読み込み
            if (srcImage == null) srcImage = readImage(ext, is);
            int imageType = srcImage.getType();
            BufferedImage outImage;
            ColorModel colorModel;
            WritableRaster raster;
            switch (gammaOp==null?imageType:BufferedImage.TYPE_INT_RGB) {
            case BufferedImage.TYPE_BYTE_BINARY:
                colorModel = srcImage.getColorModel();
                colorModel = getGray16ColorModel();
                raster = colorModel.createCompatibleWritableRaster(scaledW, scaledH);
                outImage = new BufferedImage(colorModel, raster, true, null);
                break;
            case BufferedImage.TYPE_BYTE_INDEXED:
                colorModel = srcImage.getColorModel();
                raster = colorModel.createCompatibleWritableRaster(scaledW, scaledH);
                outImage = new BufferedImage(colorModel, raster, true, null);
                break;
            /*case BufferedImage.TYPE_BYTE_GRAY:
                //PngEncoderのGRAYが薄くなるのでindexにする
                colorModel = srcImage.getColorModel();
                if (colorModel.getPixelSize() <= 4) colorModel = getGray16ColorModel();
                else colorModel = getGray256ColorModel();
                raster = colorModel.createCompatibleWritableRaster(scaledW, scaledH);
                outImage = new BufferedImage(colorModel, raster, true, null);
                break;*/
            case BufferedImage.TYPE_BYTE_GRAY:
                outImage = new BufferedImage(scaledW, scaledH, BufferedImage.TYPE_BYTE_GRAY);
                break;
            case BufferedImage.TYPE_USHORT_GRAY:
                outImage = new BufferedImage(scaledW, scaledH, BufferedImage.TYPE_USHORT_GRAY);
                break;
            default:
                outImage = new BufferedImage(scaledW, scaledH, BufferedImage.TYPE_INT_RGB);
            }
            Graphics2D g = outImage.createGraphics();
            try {
                if (imageType == BufferedImage.TYPE_BYTE_BINARY && imageType == BufferedImage.TYPE_BYTE_INDEXED && imageType == BufferedImage.TYPE_INT_ARGB) {
                    g.setColor(Color.WHITE);
                    g.fillRect(0, 0, scaledW, scaledH);
                }
                g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
                g.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
                AffineTransform at = AffineTransform.getScaleInstance(scale, scale);
                int x = 0;
                int y = 0;
                if (imageInfo.rotateAngle == 0) {
                    if (margin != null) {
                        x = (int)(-margin[0]*scale+0.5);
                        y = (int)(-margin[1]*scale+0.5);
                    }
                } else if (imageInfo.rotateAngle == 90) {
                    at.rotate(Math.toRadians(imageInfo.rotateAngle), 0, 0);
                    at.translate(0, -imgH);
                    if (margin != null) {
                        x = (int)(-margin[3]*scale+0.5);
                        y = (int)(-margin[0]*scale+0.5);
                    }
                } else {
                    at.quadrantRotate(-1, 0, 0);
                    at.translate(-imgW, 0);
                    if (margin != null) {
                        x = (int)(-margin[1]*scale+0.5);
                        y = (int)(-margin[2]*scale+0.5);
                    }
                }
                AffineTransformOp ato = new AffineTransformOp(at, AffineTransformOp.TYPE_BICUBIC);
                g.drawImage(srcImage, ato, x, y);
            } finally {
                g.dispose();
            }
            //ImageIO.write(outImage, imageInfo.getExt(), zos);
            //コントラスト調整
            if (gammaOp != null) {
                BufferedImage filterdImage = new BufferedImage(outImage.getWidth(), outImage.getHeight(), BufferedImage.TYPE_INT_RGB);
                outImage = gammaOp.filter(outImage, filterdImage);
                outImage = filterdImage;
                filterdImage = null;
                //インデックス化
                switch (imageType) {
                case BufferedImage.TYPE_BYTE_BINARY:
                    colorModel = srcImage.getColorModel();
                    colorModel = getGray16ColorModel();
                    raster = colorModel.createCompatibleWritableRaster(scaledW, scaledH);
                    filterdImage = new BufferedImage(colorModel, raster, true, null);
                    break;
                case BufferedImage.TYPE_BYTE_INDEXED:
                    colorModel = srcImage.getColorModel();
                    raster = colorModel.createCompatibleWritableRaster(scaledW, scaledH);
                    filterdImage = new BufferedImage(colorModel, raster, true, null);
                    break;
                case BufferedImage.TYPE_BYTE_GRAY:
                    filterdImage = new BufferedImage(scaledW, scaledH, BufferedImage.TYPE_BYTE_GRAY);
                    break;
                case BufferedImage.TYPE_USHORT_GRAY:
                    filterdImage = new BufferedImage(scaledW, scaledH, BufferedImage.TYPE_USHORT_GRAY);
                    break;
                }
                if (filterdImage != null) {
                    g = filterdImage.createGraphics();
                    try {
                        g.drawImage(outImage, 0, 0, null);
                    } finally {
                        g.dispose();
                    }
                    outImage = filterdImage;
                }
            }
            _writeImage(zos, outImage, ext, jpegQuality);
            imageInfo.setOutWidth(outImage.getWidth());
            imageInfo.setOutHeight(outImage.getHeight());
            if (scale < 1) {
                LogAppender.append("画像縮小");
                if (imageInfo.rotateAngle!=0) LogAppender.append("回転");
                LogAppender.println(": "+imageInfo.getOutFileName()+" ("+w+","+h+")→("+scaledW+","+scaledH+")");
            }
            zos.flush();
        }
        } catch (Exception e) {
            LogAppender.println("画像読み込みエラー: "+imageInfo.getOutFileName());
            e.printStackTrace();
        }
    }
}

ChatGPT:

この Java の ImageUtils.writeImage()

  • 画像読み込み

  • 余白検出

  • 回転

  • ガンマ補正

  • 縮小

  • JPEG/PNG書き出し

  • Zip出力

1メソッドで全部やる巨大処理なので、
Node.js では設計を少し変えると移植が安定します。

特に JSZip
OutputStream に書けないため

Buffer を返す関数に変更する必要があります。

さらに画像処理は通常
sharp を使うのが最も近い挙動になります。


Node.js版 ImageUtils.writeImage(移植版)

JavaScript

import sharp from "sharp";  
  
export default class ImageUtils {  
  
  static async writeImage(  
    is,  
    srcImage,  
    imageInfo,  
    jpegQuality,  
    gammaOp,  
    maxImagePixels,  
    maxImageW,  
    maxImageH,  
    dispW,  
    dispH,  
    autoMarginLimitH,  
    autoMarginLimitV,  
    autoMarginWhiteLevel,  
    autoMarginPadding,  
    autoMarginNombre,  
    nombreSize  
  ) {  
    try {  
  
      const ext \= imageInfo.getExt();  
      let imgBuffer;  
  
      if (srcImage) {  
        imgBuffer \= await srcImage.toBuffer();  
      } else {  
        imgBuffer \= Buffer.isBuffer(is) ? is : await streamToBuffer(is);  
      }  
  
      let image \= sharp(imgBuffer);  
      const meta \= await image.metadata();  
  
      let w \= meta.width;  
      let h \= meta.height;  
  
      imageInfo.setOutWidth(w);  
      imageInfo.setOutHeight(h);  
  
      //------------------------------------------------  
      // 回転  
      //------------------------------------------------  
      if (imageInfo.rotateAngle) {  
        image \= image.rotate(imageInfo.rotateAngle);  
      }  
  
      //------------------------------------------------  
      // 最大画素制限  
      //------------------------------------------------  
      let scale \= 1;  
  
      if (maxImagePixels \>= 10000) {  
        scale \= Math.sqrt(maxImagePixels / (w \* h));  
      }  
  
      if (maxImageW \> 0) scale \= Math.min(scale, maxImageW / w);  
      if (maxImageH \> 0) scale \= Math.min(scale, maxImageH / h);  
  
      if (scale < 1) {  
  
        const scaledW \= Math.round(w \* scale);  
        const scaledH \= Math.round(h \* scale);  
  
        image \= image.resize(scaledW, scaledH, {  
          kernel: sharp.kernel.lanczos3  
        });  
  
        LogAppender.println(  
          \`画像縮小: ${imageInfo.getOutFileName()} (${w},${h})→(${scaledW},${scaledH})\`  
        );  
  
        w \= scaledW;  
        h \= scaledH;  
      }  
  
      //------------------------------------------------  
      // ガンマ補正  
      //------------------------------------------------  
      if (gammaOp) {  
        image \= image.gamma();  
      }  
  
      //------------------------------------------------  
      // 出力  
      //------------------------------------------------  
      let output;  
  
      if (ext.startsWith("jp")) {  
        output \= await image.jpeg({  
          quality: Math.round(jpegQuality \* 100)  
        }).toBuffer();  
      } else if (ext \=== "png") {  
        output \= await image.png().toBuffer();  
      } else {  
        output \= await image.toBuffer();  
      }  
  
      imageInfo.setOutWidth(w);  
      imageInfo.setOutHeight(h);  
  
      return output;  
  
    } catch (e) {  
      LogAppender.println("画像読み込みエラー: " + imageInfo.getOutFileName());  
      console.error(e);  
      return null;  
    }  
  }  
}  
  
// stream → buffer  
async function streamToBuffer(stream) {  
  const chunks \= \[\];  
  for await (const chunk of stream) chunks.push(chunk);  
  return Buffer.concat(chunks);  
}

Java版との対応

Java Node
BufferedImage sharp
AffineTransform sharp.rotate()
Graphics2D.drawImage() sharp.resize()
gammaOp.filter() sharp.gamma()
_writeImage(zos, ...) return Buffer
is.transferTo(zos) zip.file(path, buffer)

重要(AozoraEpub3移植で一番難しい部分)

Java版にはまだ次の処理があります:

  • getPlainMargin()

  • autoMarginNombre

  • 灰色インデックス画像生成

  • TYPE_BYTE_BINARY / TYPE_BYTE_INDEXED

これらは Java2D特有の処理なので
Nodeでは 別アルゴリズムに書き直す必要があります。


実はここが最大の移植ポイント

AozoraEpub3の画像処理は

ImageUtils.java  
約1700行

あります。

しかし Node版は sharp を使うと約200行で同等になります。


もし希望があれば次に AozoraEpub3完全互換の Node版 ImageUtils

  • 余白自動除去 (getPlainMargin)

  • ノンブル削除

  • 回転

  • 最大画素

  • Kindle最適化

まで含めた **完全移植版(約300行)**を書きます。
これが AozoraEpub3 JS移植の一番重要な部分です。