【Ruby】 EPUB 内の画像ファイル名を連番に変更する方法

はじめに

マンガのEPUBのリーダーアプリとしてNeeViewを使っている。 これはEPUBがzipファイルであることを利用して、画像だけのEPUBであれば簡易的にEPUBリーダーになる。

ところが、これは画像ファイル名が連番であることを暗に前提としている。というのも、連番になっていないEPUBではページの順番が狂うのだ。

ということで、EPUBの画像ファイル名を連番に変換するrubyスクリプトを書いた。あまり綺麗でないが、まあ使えるだろう。

(2025-08-05)epub-parserを使って、書き直した。中身は大分すっきりしたと思う。 注意としては、EPUB 3に準拠できていないEPUBではエラーになる。まずはEPUB 3に準拠させてから実行する必要がある。 EPUBの検証にはEPUB-Checkerが使える。

使い方

  1. EPUBファイルを a.zipに名前を変更し、フォルダ名 a に展開する。
  2. このスクリプトを実行する。
    epub-img-files-renum.rb a
    
  3. a より上位のフォルダでMakefileを実行してEPUBを生成する。

EPUB のフォルダ構成

EPUB のフォルダ構成はたいて以下のような三つのパターンになっている:

パターンA

a/
├── META-INF/
├── OPS/
├── js/
├── mimetype
└── rights.xml

パターンB

a/
├── mimetype
├── META-INF/
└── OEBPS/

パターンC

a/
├── mimetype
├── META-INF/
└── item/
└── js/
└── rights.xml

Ruby スクリプト

例えば epub-img-files-renum.rbという名前で保存する。

#!/usr/bin/env ruby

require 'epub/parser'
require 'nokogiri'
require 'pathname'
require 'find'

dry_run = false

ARGV.each{|input|
  if FileTest.directory?(input)
    dirName = input
  end
  
  book = EPUB::Parser.parse(dirName)

  img_OldNew = Hash.new
  cnt = 0
  book.spine.itemrefs.each{|itemref|
    item       = itemref.item
    media_type = item.media_type

    if media_type.start_with?('image/')
      fileNameOld   = item.href
      filePathOld   = Pathname(File.basename(fileNameOld))
      ext           = File.extname(fileNameOld)
      fileNameNew   = sprintf("image_%06d#{ext}", cnt)
      filePathNew   = Pathname(fileNameNew)
      img_OldNew[filePathOld.to_s] = filePathNew.to_s
      cnt += 1
    elsif
      content = item.read
      doc = Nokogiri::XML(content)

      if doc.css('img').empty?
        ns = {
          'svg' => 'http://www.w3.org/2000/svg',
          'xlink' => 'http://www.w3.org/1999/xlink'
        }
        image_nodes = doc.xpath('//svg:image', ns)
        imgs = image_nodes.map { |node| node.attribute_with_ns('href', ns['xlink'])&.value }
      else
        imgs = []
        doc.css('img').each{|img|
          imgs.push(img['src'])
        }
      end
      imgs.each{|imgLink|
        # p imgLink
        fileNameOld = imgLink
        filePathOld   = Pathname(File.basename(fileNameOld))
        ext         = File.extname(fileNameOld)
        fileNameNew   = sprintf("image_%06d#{ext}", cnt)
        filePathNew   = Pathname(fileNameNew)
        img_OldNew[filePathOld.to_s] = filePathNew.to_s
        cnt += 1
      }
    end

  }
  p img_OldNew
  imgFileList = Array.new
  xmlFileList = Array.new
  pathDir = Pathname(dirName)
  Find.find(pathDir) {|f|
    if /\.(png|jpg|jpeg|gif)/ =~ f
      if !FileTest.directory?(f)
        imgFileList.push(f)
      end
    elsif /mimetype/ !~ f
      if !FileTest.directory?(f)
        xmlFileList.push(f)
      end
    end
  }
  p imgFileList
  p xmlFileList
  # xmlの中身の書き換え
  xmlFileList.each{|xml|
    xml_new = ""
    xml_old = ""
    File.open(xml){|f|
      xml_new = f.read
      xml_old = xml_new.dup
      img_OldNew.each_key{|key|
        if /(#{key})/ =~ xml_new
          #p $1
          #p img_OldNew[key]
          xml_new = xml_new.gsub(/#{key}/, img_OldNew[key])
        end
      }
    }
    if !dry_run
      if xml_new != xml_old
        File.open(xml, "w"){|f_new|
          f_new.puts(xml_new)
        }
      end
    end
  }
  # imgFileのリネーム
  p img_OldNew
  imgFileList.each{|img|
    # p img
    img_OldNew.each_key{|key|
      # p key
      if /#{key}/ =~ img
        p orig_name = img
        p new_name  = img.gsub(/#{key}/, img_OldNew[key])
        if !dry_run
          File.rename(orig_name, new_name)
        end
      end
    }

  }

}

Makefile

この Makefile は、a/ 内の構成を見て、自動的に対象ファイルを切り替える。

EPUB_NAME = book.EPUB
SOURCE_DIR = a

# 検出対象ファイル・ディレクトリ
PATTERN_A = META-INF OPS js rights.xml
PATTERN_B = META-INF OEBPS
PATTERN_C = META-INF item js mimetype rights.xml

# mimetype はどちらでも共通
MIMETYPE_FILE = $(SOURCE_DIR)/mimetype

# デフォルトターゲット
all: $(EPUB_NAME)

# EPUB生成ロジック
$(EPUB_NAME): $(MIMETYPE_FILE)
	@rm -f $(EPUB_NAME)
	@echo "Detecting EPUB structure..."
	@if [ -d "$(SOURCE_DIR)/item" ] && [ -d "$(SOURCE_DIR)/js" ] && [ -f "$(SOURCE_DIR)/rights.xml" ]; then \
		echo "Using pattern C (item/js/rights.xml)"; \
		cd $(SOURCE_DIR) && zip -X0 ../$(EPUB_NAME) mimetype && \
		zip -Xr9D ../$(EPUB_NAME) $(PATTERN_C); \
	elif [ -d "$(SOURCE_DIR)/OPS" ] && [ -d "$(SOURCE_DIR)/js" ] && [ -f "$(SOURCE_DIR)/rights.xml" ]; then \
		echo "Using pattern A (OPS/js/rights.xml)"; \
		cd $(SOURCE_DIR) && zip -X0 ../$(EPUB_NAME) mimetype && \
		zip -Xr9D ../$(EPUB_NAME) $(PATTERN_A); \
	elif [ -d "$(SOURCE_DIR)/OEBPS" ]; then \
		echo "Using pattern B (OEBPS only)"; \
		cd $(SOURCE_DIR) && zip -X0 ../$(EPUB_NAME) mimetype && \
		zip -Xr9D ../$(EPUB_NAME) $(PATTERN_B); \
	else \
		echo "Error: Could not determine valid EPUB structure in '$(SOURCE_DIR)'."; \
		exit 1; \
	fi
	@echo "EPUB created: $(EPUB_NAME)"

clean:
	rm -f $(EPUB_NAME)

使用方法

  1. この Makefile を a/ の1つ上の階層に配置する。
  2. ターミナルでその階層へ移動して:
    make
    

おまけ

Windows上のEPUBリーダーは、実は紀伊国屋のKinoppyがおすすめだ。 KnoppyはテキストベースのEPUBの縦書きに対応しており、とても使いやすい。