# Copyright (c) 2010-2020 Kenshi Muto and Masayoshi Takahashi # # This program is free software. # You can distribute or modify this program under the terms of # the GNU LGPL, Lesser General Public License version 2.1. # For details of the GNU LGPL, see the file "COPYING". # require 'tmpdir' require 'review/i18n' require 'review/book' require 'review/configure' require 'review/converter' require 'review/latexbuilder' require 'review/yamlloader' require 'review/version' require 'review/htmltoc' require 'review/htmlbuilder' require 'review/yamlloader' require 'rexml/document' require 'rexml/streamlistener' require 'epubmaker' require 'review/epubmaker/reviewheaderlistener' require 'review/makerhelper' module ReVIEW class EPUBMaker include ::EPUBMaker include REXML include MakerHelper def initialize @producer = nil @htmltoc = nil @buildlogtxt = 'build-log.txt' @logger = ReVIEW.logger end def error(msg) @logger.error msg exit 1 end def warn(msg) @logger.warn msg end def log(msg) @logger.debug(msg) end def load_yaml(yamlfile) loader = ReVIEW::YAMLLoader.new @config = ReVIEW::Configure.values begin @config.deep_merge!(loader.load_file(yamlfile)) rescue => e error "yaml error #{e.message}" end @producer = Producer.new(@config) @producer.load(yamlfile) @config = @producer.config @config.maker = 'epubmaker' end def self.execute(*args) self.new.execute(*args) end def parse_opts(args) cmd_config = {} opts = OptionParser.new @buildonly = nil opts.banner = 'Usage: review-epubmaker [options] configfile [export_filename]' opts.version = ReVIEW::VERSION opts.on('--help', 'Prints this message and quit.') do puts opts.help exit 0 end opts.on('--[no-]debug', 'Keep temporary files.') { |debug| cmd_config['debug'] = debug } opts.on('-y', '--only file1,file2,...', 'Build only specified files.') { |v| @buildonly = v.split(/\s*,\s*/).map { |m| m.strip.sub(/\.re\Z/, '') } } opts.parse!(args) if args.size < 1 || args.size > 2 puts opts.help exit 0 end [cmd_config, args[0], args[1]] end def execute(*args) @config = ReVIEW::Configure.values @config.maker = 'epubmaker' cmd_config, yamlfile, exportfile = parse_opts(args) error "#{yamlfile} not found." unless File.exist?(yamlfile) load_yaml(yamlfile) @config.deep_merge!(cmd_config) update_log_level log("Loaded yaml file (#{yamlfile}).") produce(yamlfile, exportfile) end def update_log_level if @config['debug'] @logger.level = Logger::DEBUG else @logger.level = Logger::INFO end end def build_path if @config['debug'] path = File.expand_path("#{@config['bookname']}-epub", Dir.pwd) if File.exist?(path) FileUtils.rm_rf(path, secure: true) end Dir.mkdir(path) path else Dir.mktmpdir("#{@config['bookname']}-epub-") end end def produce(yamlfile, bookname = nil) I18n.setup(@config['language']) bookname ||= @config['bookname'] booktmpname = "#{bookname}-epub" begin @config.check_version(ReVIEW::VERSION) rescue ReVIEW::ConfigError => e warn e.message end log("#{bookname}.epub will be created.") FileUtils.rm_f("#{bookname}.epub") if @config['debug'] FileUtils.rm_rf(booktmpname) end cleanup_mathimg basetmpdir = build_path begin log("Created first temporary directory as #{basetmpdir}.") call_hook('hook_beforeprocess', basetmpdir) @htmltoc = ReVIEW::HTMLToc.new(basetmpdir) ## copy all files into basetmpdir copy_stylesheet(basetmpdir) copy_frontmatter(basetmpdir) call_hook('hook_afterfrontmatter', basetmpdir) build_body(basetmpdir, yamlfile) call_hook('hook_afterbody', basetmpdir) copy_backmatter(basetmpdir) math_dir = "./#{@config['imagedir']}/_review_math" if @config['imgmath'] && File.exist?(File.join(math_dir, '__IMGMATH_BODY__.map')) make_math_images(math_dir) end call_hook('hook_afterbackmatter', basetmpdir) ## push contents in basetmpdir into @producer push_contents(basetmpdir) if @config['epubmaker']['verify_target_images'].present? verify_target_images(basetmpdir) copy_images(@config['imagedir'], basetmpdir) else copy_images(@config['imagedir'], File.join(basetmpdir, @config['imagedir'])) end copy_resources('covers', File.join(basetmpdir, @config['imagedir'])) copy_resources('adv', File.join(basetmpdir, @config['imagedir'])) copy_resources(@config['fontdir'], File.join(basetmpdir, 'fonts'), @config['font_ext']) call_hook('hook_aftercopyimage', basetmpdir) @producer.import_imageinfo(File.join(basetmpdir, @config['imagedir']), basetmpdir) @producer.import_imageinfo(File.join(basetmpdir, 'fonts'), basetmpdir, @config['font_ext']) check_image_size(basetmpdir, @config['image_maxpixels'], @config['image_ext']) epubtmpdir = nil if @config['debug'].present? epubtmpdir = File.join(basetmpdir, booktmpname) Dir.mkdir(epubtmpdir) end log('Call ePUB producer.') @producer.produce("#{bookname}.epub", basetmpdir, epubtmpdir) log('Finished.') rescue ApplicationError => e raise if @config['debug'] error(e.message) ensure FileUtils.remove_entry_secure(basetmpdir) unless @config['debug'] end end def call_hook(hook_name, *params) filename = @config['epubmaker'][hook_name] log("Call #{hook_name}. (#{filename})") if filename.present? && File.exist?(filename) && FileTest.executable?(filename) if ENV['REVIEW_SAFE_MODE'].to_i & 1 > 0 warn 'hook is prohibited in safe mode. ignored.' else system(filename, *params) end end end def verify_target_images(basetmpdir) @producer.contents.each do |content| case content.media when 'application/xhtml+xml' File.open("#{basetmpdir}/#{content.file}") do |f| Document.new(File.new(f)).each_element('//img') do |e| @config['epubmaker']['force_include_images'].push(e.attributes['src']) if e.attributes['src'] =~ /svg\Z/i content.properties.push('svg') end end end when 'text/css' File.open(File.join(basetmpdir, content.file)) do |f| f.each_line do |l| l.scan(/url\((.+?)\)/) do |_m| @config['epubmaker']['force_include_images'].push($1.strip) end end end end end @config['epubmaker']['force_include_images'] = @config['epubmaker']['force_include_images'].compact.sort.uniq end def copy_images(resdir, destdir, allow_exts = nil) return nil unless File.exist?(resdir) allow_exts ||= @config['image_ext'] FileUtils.mkdir_p(destdir) if @config['epubmaker']['verify_target_images'].present? @config['epubmaker']['force_include_images'].each do |file| unless File.exist?(file) if file !~ /\Ahttp[s]?:/ warn "#{file} is not found, skip." end next end basedir = File.dirname(file) FileUtils.mkdir_p(File.join(destdir, basedir)) log("Copy #{file} to the temporary directory.") FileUtils.cp(file, File.join(destdir, basedir)) end else recursive_copy_files(resdir, destdir, allow_exts) end end def copy_resources(resdir, destdir, allow_exts = nil) return nil unless File.exist?(resdir) allow_exts ||= @config['image_ext'] FileUtils.mkdir_p(destdir) recursive_copy_files(resdir, destdir, allow_exts) end def recursive_copy_files(resdir, destdir, allow_exts) Dir.open(resdir) do |dir| dir.each do |fname| next if fname.start_with?('.') if FileTest.directory?(File.join(resdir, fname)) recursive_copy_files(File.join(resdir, fname), File.join(destdir, fname), allow_exts) elsif fname =~ /\.(#{allow_exts.join('|')})\Z/i FileUtils.mkdir_p(destdir) log("Copy #{resdir}/#{fname} to the temporary directory.") FileUtils.cp(File.join(resdir, fname), destdir) end end end end def check_compile_status return unless @compile_errors $stderr.puts 'compile error, No EPUB file output.' exit 1 end def build_body(basetmpdir, yamlfile) @precount = 0 @bodycount = 0 @postcount = 0 @manifeststr = '' @ncxstr = '' @tocdesc = [] basedir = File.dirname(yamlfile) base_path = Pathname.new(basedir) book = ReVIEW::Book.load(basedir) book.config = @config @converter = ReVIEW::Converter.new(book, ReVIEW::HTMLBuilder.new) @compile_errors = nil book.parts.each do |part| if part.name.present? if part.file? build_chap(part, base_path, basetmpdir, true) else htmlfile = "part_#{part.number}.#{@config['htmlext']}" build_part(part, basetmpdir, htmlfile) title = ReVIEW::I18n.t('part', part.number) if part.name.strip.present? title += ReVIEW::I18n.t('chapter_postfix') + part.name.strip end @htmltoc.add_item(0, htmlfile, title, chaptype: 'part') write_buildlogtxt(basetmpdir, htmlfile, '') end end part.chapters.each do |chap| build_chap(chap, base_path, basetmpdir, false) end end check_compile_status end def build_part(part, basetmpdir, htmlfile) log("Create #{htmlfile} from a template.") File.open(File.join(basetmpdir, htmlfile), 'w') do |f| @body = '' @body << %Q(