lib/epubmaker/epubv2.rb in review-1.1.0 vs lib/epubmaker/epubv2.rb in review-1.2.0

- old
+ new

@@ -1,9 +1,9 @@ # encoding: utf-8 # = epubv2.rb -- EPUB version 2 producer. # -# Copyright (c) 2010-2012 Kenshi Muto and Masayoshi Takahashi +# Copyright (c) 2010-2013 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". @@ -11,159 +11,212 @@ require 'epubmaker/producer' require 'cgi' module EPUBMaker - + # EPUBv2 is EPUB version 2 producer. class EPUBv2 # Construct object with parameter hash +params+ and message resource hash +res+. def initialize(producer) @producer = producer end - + # Return mimetype content. def mimetype - return "application/epub+zip" + "application/epub+zip" end - + # Return opf file content. def opf s = <<EOT <?xml version="1.0" encoding="UTF-8"?> <package version="2.0" xmlns="http://www.idpf.org/2007/opf" unique-identifier="BookId"> <metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf"> EOT + + s << opf_metainfo + s << opf_coverimage + + s << %Q[ </metadata>\n] + + s << opf_manifest + s << opf_tocx + s << opf_guide + + s << %Q[</package>\n] + + s + end + + def opf_metainfo + s = "" %w[title language date type format source description relation coverage subject rights].each do |item| next if @producer.params[item].nil? if @producer.params[item].instance_of?(Array) s << @producer.params[item].map {|i| %Q[ <dc:#{item}>#{CGI.escapeHTML(i.to_s)}</dc:#{item}>\n]}.join else s << %Q[ <dc:#{item}>#{CGI.escapeHTML(@producer.params[item].to_s)}</dc:#{item}>\n] end end - + # ID if @producer.params["isbn"].nil? s << %Q[ <dc:identifier id="BookId">#{@producer.params["urnid"]}</dc:identifier>\n] else s << %Q[ <dc:identifier id="BookId" opf:scheme="ISBN">#{@producer.params["isbn"]}</dc:identifier>\n] end - - # creator + + # creator (should be array) %w[aut a-adp a-ann a-arr a-art a-asn a-aqt a-aft a-aui a-ant a-bkp a-clb a-cmm a-dsr a-edt a-ill a-lyr a-mdc a-mus a-nrt a-oth a-pht a-prt a-red a-rev a-spn a-ths a-trc a-trl].each do |role| next if @producer.params[role].nil? @producer.params[role].each do |v| s << %Q[ <dc:creator opf:role="#{role.sub('a-', '')}">#{CGI.escapeHTML(v)}</dc:creator>\n] end end - # contributor + + # contributor (should be array) %w[adp ann arr art asn aqt aft aui ant bkp clb cmm dsr edt ill lyr mdc mus nrt oth pht prt red rev spn ths trc trl].each do |role| next if @producer.params[role].nil? @producer.params[role].each do |v| s << %Q[ <dc:contributor opf:role="#{role}">#{CGI.escapeHTML(v)}</dc:contributor>\n] if role == "prt" s << %Q[ <dc:publisher>#{v}</dc:publisher>\n] end end end - + + s + end + + def opf_coverimage + s = "" if @producer.params["coverimage"] + file = nil @producer.contents.each do |item| if item.media =~ /\Aimage/ && item.file =~ /#{@producer.params["coverimage"]}\Z/ - s << %Q[ <meta name="cover" content="#{item.id}"/>\n] + s << %Q[ <meta name="cover" content="#{item.id}"/>\n] + file = item.file break end end + raise "coverimage #{@producer.params["coverimage"]} not found. Abort." if file.nil? end - - s << %Q[ </metadata>\n] - - # manifest + s + end + + def opf_manifest + s = "" s << <<EOT <manifest> <item id="ncx" href="#{@producer.params["bookname"]}.ncx" media-type="application/x-dtbncx+xml"/> <item id="#{@producer.params["bookname"]}" href="#{@producer.params["cover"]}" media-type="application/xhtml+xml"/> EOT - s << %Q[ <item id="toc" href="#{@producer.params["tocfile"]}" media-type="application/xhtml+xml"/>\n] unless @producer.params["mytoc"].nil? - + s << %Q[ <item id="toc" href="#{@producer.params["bookname"]}-toc.#{@producer.params["htmlext"]}" media-type="application/xhtml+xml"/>\n] unless @producer.params["mytoc"].nil? + @producer.contents.each do |item| next if item.file =~ /#/ # skip subgroup s << %Q[ <item id="#{item.id}" href="#{item.file}" media-type="#{item.media}"/>\n] end s << %Q[ </manifest>\n] - - # tocx + s + end + + def opf_tocx + s = "" s << %Q[ <spine toc="ncx">\n] s << %Q[ <itemref idref="#{@producer.params["bookname"]}" linear="no"/>\n] s << %Q[ <itemref idref="toc" />\n] unless @producer.params["mytoc"].nil? - + @producer.contents.each do |item| next if item.media !~ /xhtml\+xml/ # skip non XHTML s << %Q[ <itemref idref="#{item.id}"/>\n] if item.notoc.nil? end s << %Q[ </spine>\n] - - # guide + s + end + + def opf_guide + s = "" s << %Q[ <guide>\n] s << %Q[ <reference type="cover" title="#{@producer.res.v("covertitle")}" href="#{@producer.params["cover"]}"/>\n] - s << %Q[ <reference type="title-page" title="#{@producer.res.v("titlepagetitle")}" href="#{@producer.params["titlepage"]}"/>\n] unless @producer.params["titlepage"].nil? - s << %Q[ <reference type="toc" title="#{@producer.res.v("toctitle")}" href="#{@producer.params["tocfile"]}"/>\n] unless @producer.params["mytoc"].nil? - s << %Q[ <reference type="colophon" title="#{@producer.res.v("colophontitle")}" href="colophon.#{@producer.params["htmlext"]}"/>\n] unless @producer.params["colophon"].nil? # FIXME: path + s << %Q[ <reference type="title-page" title="#{@producer.res.v("titlepagetitle")}" href="titlepage.#{@producer.params["htmlext"]}"/>\n] unless @producer.params["titlepage"].nil? + s << %Q[ <reference type="toc" title="#{@producer.res.v("toctitle")}" href="#{@producer.params["bookname"]}-toc.#{@producer.params["htmlext"]}"/>\n] unless @producer.params["mytoc"].nil? + s << %Q[ <reference type="colophon" title="#{@producer.res.v("colophontitle")}" href="colophon.#{@producer.params["htmlext"]}"/>\n] unless @producer.params["colophon"].nil? s << %Q[ </guide>\n] - s << %Q[</package>\n] - return s + s end - # Return ncx content. +indentarray+ defines prefix string for each level. + # Return ncx content. +indentarray+ has prefix marks for each level. def ncx(indentarray) s = <<EOT <?xml version="1.0" encoding="UTF-8"?> <ncx xmlns="http://www.daisy.org/z3986/2005/ncx/" version="2005-1"> <head> <meta name="dtb:depth" content="1"/> <meta name="dtb:totalPageCount" content="0"/> <meta name="dtb:maxPageNumber" content="0"/> EOT + s << ncx_isbn + + s << <<EOT + </head> +EOT + s << ncx_doctitle + s << ncx_navmap(indentarray) + + s << <<EOT +</ncx> +EOT + s + end + + def ncx_isbn if @producer.params["isbn"].nil? - s << %Q[ <meta name="dtb:uid" content="#{@producer.params["urnid"]}"/>\n] + %Q[ <meta name="dtb:uid" content="#{@producer.params["urnid"]}"/>\n] else - s << %Q[ <meta name="dtb:uid" content="#{@producer.params["isbn"]}"/>\n] + %Q[ <meta name="dtb:uid" content="#{@producer.params["isbn"]}"/>\n] end - - s << <<EOT - </head> + end + + def ncx_doctitle + <<EOT <docTitle> <text>#{CGI.escapeHTML(@producer.params["title"])}</text> </docTitle> <docAuthor> <text>#{@producer.params["aut"].nil? ? "" : CGI.escapeHTML(@producer.params["aut"].join(", "))}</text> </docAuthor> +EOT + end + + def ncx_navmap(indentarray) + s = <<EOT <navMap> <navPoint id="top" playOrder="1"> <navLabel> <text>#{CGI.escapeHTML(@producer.params["title"])}</text> </navLabel> <content src="#{@producer.params["cover"]}"/> </navPoint> EOT nav_count = 2 - + unless @producer.params["mytoc"].nil? s << <<EOT <navPoint id="toc" playOrder="#{nav_count}"> <navLabel> <text>#{@producer.res.v("toctitle")}</text> </navLabel> - <content src="#{@producer.params["tocfile"]}"/> + <content src="#{@producer.params["bookname"]}-toc.#{@producer.params["htmlext"]}"/> </navPoint> EOT nav_count += 1 end - + @producer.contents.each do |item| next if item.title.nil? indent = indentarray.nil? ? [""] : indentarray level = item.level.nil? ? 0 : (item.level - 1) level = indent.size - 1 if level >= indent.size @@ -175,31 +228,30 @@ <content src="#{item.file}"/> </navPoint> EOT nav_count += 1 end - + s << <<EOT </navMap> -</ncx> EOT - return s + s end - + # Return container content. def container s = <<EOT <?xml version="1.0" encoding="UTF-8"?> <container xmlns="urn:oasis:names:tc:opendocument:xmlns:container" version="1.0"> <rootfiles> <rootfile full-path="OEBPS/#{@producer.params["bookname"]}.opf" media-type="application/oebps-package+xml" /> </rootfiles> </container> EOT - return s + s end - + # Return cover content. def cover s = common_header s << <<EOT <title>#{CGI.escapeHTML(@producer.params["title"])}</title> @@ -222,17 +274,17 @@ s << <<EOT <div id="cover-image" class="cover-image"> <img src="#{file}" alt="#{CGI.escapeHTML(@producer.params["title"])}" class="max"/> </div> EOT - end - + end + s << <<EOT </body> </html> EOT - return s + s end # Return title (copying) content. def titlepage s = common_header @@ -247,11 +299,11 @@ s << <<EOT <p> <br /> <br /> </p> - <h2 class="tp-author">#{CGI.escapeHTML(@producer.params["aut"])}</h2> + <h2 class="tp-author">#{CGI.escapeHTML(@producer.params["aut"].join(", "))}</h2> EOT end if @producer.params["prt"] s << <<EOT @@ -259,141 +311,214 @@ <br /> <br /> <br /> <br /> </p> - <h3 class="tp-publisher">#{CGI.escapeHTML(@producer.params["prt"])}</h3> + <h3 class="tp-publisher">#{CGI.escapeHTML(@producer.params["prt"].join(", "))}</h3> EOT end s << <<EOT </body> </html> EOT - return s + + s end # Return colophon content. def colophon s = common_header s << <<EOT <title>#{@producer.res.v("colophontitle")}</title> </head> <body> <div class="colophon"> +EOT + + if @producer.params["subtitle"].nil? + s << <<EOT <p class="title">#{CGI.escapeHTML(@producer.params["title"])}</p> EOT + else + s << <<EOT + <p class="title">#{CGI.escapeHTML(@producer.params["title"])}<br /><span class="subtitle">#{CGI.escapeHTML(@producer.params["subtitle"])}</span></p> +EOT + end - if @producer.params["pubhistory"] - s << %Q[ <div class="pubhistory">\n <p>#{@producer.params["pubhistory"].gsub(/\n/, "<br />")}</p>\n </div>\n] # FIXME: should be array? + if @producer.params["date"] || @producer.params["history"] + s << %Q[ <div class="pubhistory">\n] + if @producer.params["history"] + @producer.params["history"].each_with_index do |items, edit| + items.each_with_index do |item, rev| + editstr = (edit == 0) ? "初版" : "第#{edit + 1}版" # FIXME:i18n + revstr = "第#{rev + 1}刷" + s << %Q[ <p>#{date_to_s(item)} #{editstr}#{revstr} 発行</p>\n] # FIXME:i18n + end + end + else + s << %Q[ <p>#{date_to_s(@producer.params["date"])} 発行</p>\n] #FIXME:i18n + end + s << %Q[ </div>\n] end - + s << %Q[ <table class="colophon">\n] - s << %Q[ <tr><th>#{@producer.res.v("c-aut")}</th><td>#{CGI.escapeHTML(@producer.params["aut"])}</td></tr>\n] if @producer.params["aut"] - s << %Q[ <tr><th>#{@producer.res.v("c-dsr")}</th><td>#{CGI.escapeHTML(@producer.params["dsr"])}</td></tr>\n] if @producer.params["dsr"] - s << %Q[ <tr><th>#{@producer.res.v("c-ill")}</th><td>#{CGI.escapeHTML(@producer.params["ill"])}</td></tr>\n] if @producer.params["ill"] - s << %Q[ <tr><th>#{@producer.res.v("c-edt")}</th><td>#{CGI.escapeHTML(@producer.params["edt"])}</td></tr>\n] if @producer.params["edt"] - s << %Q[ <tr><th>#{@producer.res.v("c-prt")}</th><td>#{CGI.escapeHTML(@producer.params["prt"])}</td></tr>\n] if @producer.params["prt"] + s << %Q[ <tr><th>#{@producer.res.v("c-aut")}</th><td>#{CGI.escapeHTML(@producer.params["aut"].join(", "))}</td></tr>\n] unless @producer.params["aut"].nil? + s << %Q[ <tr><th>#{@producer.res.v("c-csl")}</th><td>#{CGI.escapeHTML(@producer.params["csl"].join(", "))}</td></tr>\n] unless @producer.params["csl"].nil? + s << %Q[ <tr><th>#{@producer.res.v("c-trl")}</th><td>#{CGI.escapeHTML(@producer.params["trl"].join(", "))}</td></tr>\n] unless @producer.params["trl"].nil? + s << %Q[ <tr><th>#{@producer.res.v("c-dsr")}</th><td>#{CGI.escapeHTML(@producer.params["dsr"].join(", "))}</td></tr>\n] unless @producer.params["dsr"].nil? + s << %Q[ <tr><th>#{@producer.res.v("c-ill")}</th><td>#{CGI.escapeHTML(@producer.params["ill"].join(", "))}</td></tr>\n] unless @producer.params["ill"].nil? + s << %Q[ <tr><th>#{@producer.res.v("c-edt")}</th><td>#{CGI.escapeHTML(@producer.params["edt"].join(", "))}</td></tr>\n] unless @producer.params["edt"].nil? + s << %Q[ <tr><th>#{@producer.res.v("c-prt")}</th><td>#{CGI.escapeHTML(@producer.params["prt"].join(", "))}</td></tr>\n] unless @producer.params["prt"].nil? + s << %Q[ <tr><th>#{@producer.res.v("c-pht")}</th><td>#{CGI.escapeHTML(@producer.params["pht"].join(", "))}</td></tr>\n] unless @producer.params["pht"].nil? + if @producer.params["isbn"].to_s =~ /\A\d{10}\Z/ || @producer.params["isbn"].to_s =~ /\A\d{13}\Z/ + isbn = nil + str = @producer.params["isbn"].to_s + if str.size == 10 + isbn = "#{str[0..0]}-#{str[1..5]}-#{str[6..8]}-#{str[9..9]}" + else + isbn = "#{str[0..2]}-#{str[3..3]}-#{str[4..8]}-#{str[9..11]}-#{str[12..12]}" + end + s << %Q[ <tr><th>ISBN</th><td>#{isbn}</td></tr>\n] + end s << <<EOT </table> </div> </body> </html> EOT - return s + s end + def date_to_s(date) + ymd = date.to_s.split('-') + "#{ymd[0]}年#{ymd[1].sub(/\A0/, '')}月#{ymd[2].sub(/\A0/, '')}日" # FIXME:i18n + end + # Return own toc content. def mytoc s = common_header s << <<EOT <title>#{@producer.res.v("toctitle")}</title> </head> <body> <h1 class="toc-title">#{@producer.res.v("toctitle")}</h1> - <ul class="toc-h1"> EOT - # FIXME: indent - current = 1 - init_item = true + if @producer.params["flattoc"].nil? + s << hierarchy_ncx("ul") + else + s << flat_ncx("ul", @producer.params["flattocindent"]) + end + + s << <<EOT +</body> +</html> +EOT + s + end + + def hierarchy_ncx(type) + require 'rexml/document' + level = 1 + find_jump = nil + + # check part existance @producer.contents.each do |item| + if item.notoc.nil? && item.level == 0 + level = 0 + end + end + + doc = REXML::Document.new(%Q[<#{type} class="toc-h#{level}"><li /></#{type}>]) + + e = doc.root.elements[1] # first <li/> + @producer.contents.each do |item| next if !item.notoc.nil? || item.level.nil? || item.file.nil? || item.title.nil? || item.level > @producer.params["toclevel"].to_i - if item.level > current - s << %Q[\n<ul class="toc-h#{item.level}">\n] - current = item.level - elsif item.level < current - (current - 1).downto(item.level) do |n| - s << %Q[</li>\n</ul>\n] + + if item.level == level + e2 = e.parent.add_element("li") + e = e2 + elsif item.level > level + find_jump = true if (item.level - level) > 1 + # deeper + (level + 1).upto(item.level) do |n| + e2 = e.add_element(type, {"class" => "toc-h#{n}"}) + e3 = e2.add_element("li") + e = e3 end - s << %Q[</li>\n] - current = item.level - elsif init_item - # noop - else - s << %Q[</li>\n] + level = item.level + elsif item.level < level + # shallower + (level - 1).downto(item.level) do |n| + e = e.parent.parent + end + e2 = e.parent.add_element("li") + e = e2 + level = item.level end - s << %Q[<li><a href="#{item.file}">#{item.title}</a>] - init_item = false + e2 = e.add_element("a", {"href" => item.file}) + e2.add_text(REXML::Text.new(item.title, true)) end - - (current - 1).downto(1) do |n| - s << %Q[</li>\n</ul>\n] + + warn "found level jumping in table of contents. consider to use 'flattoc: true' for strict ePUB validator." unless find_jump.nil? + + doc.to_s.gsub("<li/>", "").gsub("</li>", "</li>\n").gsub("href='", "href=\"").gsub(" class='", " class=\"").gsub("'>", "\">").gsub("<#{type} ", "\n" + '\&') # ugly + end + + def flat_ncx(type, indent=nil) + s = %Q[<#{type} class="toc-h1">\n] + @producer.contents.each do |item| + next if !item.notoc.nil? || item.level.nil? || item.file.nil? || item.title.nil? || item.level > @producer.params["toclevel"].to_i + is = indent == true ? " " * item.level : "" + s << %Q[<li><a href="#{item.file}">#{is}#{item.title}</a></li>\n] end - if !init_item - s << %Q[</li>\n] - end - s << <<EOT - </ul> -</body> -</html> -EOT - return s + s << %Q[</#{type}>\n] + + s end # Produce EPUB file +epubfile+. # +basedir+ points the directory has contents. # +tmpdir+ defines temporary directory. def produce(epubfile, basedir, tmpdir) + produce_write_common(basedir, tmpdir) + + File.open("#{tmpdir}/OEBPS/#{@producer.params["bookname"]}.ncx", "w") {|f| @producer.ncx(f, @producer.params["ncxindent"]) } + File.open("#{tmpdir}/OEBPS/#{@producer.params["bookname"]}-toc.#{@producer.params["htmlext"]}", "w") {|f| @producer.mytoc(f) } unless @producer.params["mytoc"].nil? + + @producer.call_hook(@producer.params["hook_prepack"], tmpdir) + export_zip(tmpdir, epubfile) + end + + def produce_write_common(basedir, tmpdir) File.open("#{tmpdir}/mimetype", "w") {|f| @producer.mimetype(f) } - + Dir.mkdir("#{tmpdir}/META-INF") unless File.exist?("#{tmpdir}/META-INF") File.open("#{tmpdir}/META-INF/container.xml", "w") {|f| @producer.container(f) } - + Dir.mkdir("#{tmpdir}/OEBPS") unless File.exist?("#{tmpdir}/OEBPS") File.open("#{tmpdir}/OEBPS/#{@producer.params["bookname"]}.opf", "w") {|f| @producer.opf(f) } - File.open("#{tmpdir}/OEBPS/#{@producer.params["bookname"]}.ncx", "w") {|f| @producer.ncx(f, @producer.params["ncxindent"]) } - File.open("#{tmpdir}/OEBPS/#{@producer.params["tocfile"]}", "w") {|f| @producer.mytoc(f) } unless @producer.params["mytoc"].nil? - + if File.exist?("#{basedir}/#{@producer.params["cover"]}") FileUtils.cp("#{basedir}/#{@producer.params["cover"]}", "#{tmpdir}/OEBPS") else File.open("#{tmpdir}/OEBPS/#{@producer.params["cover"]}", "w") {|f| @producer.cover(f) } end - - # FIXME:colophon and titlepage should be included in @producer.contents. - + @producer.contents.each do |item| next if item.file =~ /#/ # skip subgroup fname = "#{basedir}/#{item.file}" raise "#{fname} doesn't exist. Abort." unless File.exist?(fname) FileUtils.mkdir_p(File.dirname("#{tmpdir}/OEBPS/#{item.file}")) unless File.exist?(File.dirname("#{tmpdir}/OEBPS/#{item.file}")) FileUtils.cp(fname, "#{tmpdir}/OEBPS/#{item.file}") end + end - fork { - Dir.chdir(tmpdir) {|d| - exec("zip", "-0X", "#{epubfile}", "mimetype") - } - } - Process.waitall - fork { - Dir.chdir(tmpdir) {|d| - exec("zip", "-Xr9D", "#{epubfile}", "META-INF", "OEBPS") - } - } - Process.waitall + def export_zip(tmpdir, epubfile) + Dir.chdir(tmpdir) {|d| system("#{@producer.params["zip_stage1"]} #{epubfile} mimetype") } + Dir.chdir(tmpdir) {|d| system("#{@producer.params["zip_stage2"]} #{epubfile} META-INF OEBPS") } end private # Return common XHTML headder @@ -409,10 +534,9 @@ EOT @producer.params["stylesheet"].each do |file| s << %Q[ <link rel="stylesheet" type="text/css" href="#{file}"/>\n] end - return s + s end end - end