Last active
January 17, 2019 02:31
-
-
Save baash05/d2b6dcb0a52f84c6ccc35ea28e1f4b59 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require 'zip' | |
require 'open-uri' | |
# EXAMPLE USE AT THE BOTTOM OF THIS FILE (after the __END__) | |
class EpubGenerator | |
# When you add a chapter, naming the div_id according to the guide_roles allows | |
# apple to treat the files differently | |
# I have no idea how it does it, or what it does, but Apple likes it. | |
# it is not required, and naming your div something else will tell apple it's "text" | |
guide_roles = %w(cover | |
title-page | |
toc | |
index | |
glossary | |
acknowledgements | |
bibliography | |
colophon | |
copyright-page | |
dedication | |
epigraph | |
foreword | |
notes | |
preface) | |
guide_roles << 'loi' #list of images | |
guide_roles << 'lot' #list of tables | |
def self.generate | |
dir = "my_epub_#{Time.current}" | |
Dir.mkdir(dir) unless Dir.exist?(dir) | |
Dir.chdir(dir) { | |
service = EpubGenerator.send(:new) | |
yield service if block_given? | |
service.send(:build_support_files) | |
service.send(:zip_files) | |
} | |
File.join(Dir.pwd, dir, 'my.epub') | |
end | |
attr_accessor :author_name, :isbn, :book_title, :publisher, | |
:publication_date, :subject_classification, :table_of_contents_title, | |
:cover_file_name, :styles | |
alias_method :style=, :styles= | |
alias_method :style, :styles | |
def add_chapter(div_id, title, body) | |
Dir.mkdir('OEBPS') unless Dir.exist?('OEBPS') | |
Dir.chdir("OEBPS") { | |
Dir.mkdir('Text') unless Dir.exist?('Text') | |
Dir.chdir("Text") { | |
add_raw_content(div_id, title, body) | |
@content[div_id] = title | |
} | |
} | |
end | |
def add_image(dest_name, sourc_url) | |
Dir.mkdir('OEBPS') unless Dir.exist?('OEBPS') | |
Dir.chdir("OEBPS") { | |
Dir.mkdir('Images') unless Dir.exist?('Images') | |
open("Images/#{dest_name}", 'wb') do |file| | |
file << open(sourc_url).read | |
end | |
} | |
end | |
private | |
def self.new | |
super | |
end | |
def initialize | |
@content = {} | |
@author_name ='David Rawk' | |
@isbn = SecureRandom.uuid | |
@book_title = 'The ePub of wonders' | |
@publisher = 'Bootstrap Dragon' | |
@publication_date = Date.current.to_s | |
@subject_classification = 'NON000000 NON-CLASSIFIABLE' # The list of options is here https://bisg.org/page/BISACEdition | |
@table_of_contents_title = 'Table Of Contents' | |
@cover_file_name = 'cover.jpeg' | |
@styles = "" | |
end | |
def zip_files | |
entries = list_dir Dir.pwd | |
::Zip::OutputStream.open('my.epub') do |stream| | |
# This has to be done without compression, because that's required by the standard | |
stream.put_next_entry('mimetype', nil, nil, ::Zip::Entry::STORED) | |
stream.write 'application/epub+zip' | |
end | |
zip_file = Zip::File.open('my.epub') | |
writeEntries(entries, "", zip_file) | |
zip_file.close | |
end | |
def writeEntries(entries, path, io) | |
entries.each { |e| | |
zipFilePath = path == "" ? e : File.join(path, e) | |
diskFilePath = File.join(Dir.pwd, zipFilePath) | |
if File.directory?(diskFilePath) | |
io.mkdir(zipFilePath) | |
subdir = list_dir(diskFilePath) | |
writeEntries(subdir, zipFilePath, io) | |
else | |
io.get_output_stream(zipFilePath) { |f| f.puts(File.open(diskFilePath, "rb").read())} | |
end | |
} | |
end | |
def build_support_files | |
Dir.mkdir('META-INF') unless Dir.exist?('META-INF') | |
Dir.chdir("META-INF") { | |
make_container | |
make_apple_hack | |
} | |
Dir.mkdir('OEBPS') unless Dir.exist?('OEBPS') | |
Dir.chdir("OEBPS") { | |
Dir.mkdir('Styles') unless Dir.exist?('Styles') | |
Dir.chdir("Styles") { | |
add_style(@styles) | |
} | |
Dir.mkdir('Text') unless Dir.exist?('Text') | |
Dir.chdir("Text") { | |
add_cover | |
create_toc_html | |
} | |
create_toc_ncx | |
create_content_opf | |
} | |
end | |
def add_cover | |
add_raw_content('cover', 'Cover', "<div id=\"cover-image\"> | |
<img alt=\"#{@book_title}\" src=\"../Images/#{@cover_file_name}\" /> | |
</div>") | |
end | |
def add_style(string = "") | |
File.open("style.css", 'w') do |file| | |
text = [] | |
text << "body { line-height: 1.5em;} " | |
text << 'p.first {text-indent: 0;} ' | |
text << '@media amzn-mobi {p.first {text-indent: 0;}} ' | |
text << 'img { max-width: 100%; } #cover-image { text-align: center; }' | |
text << string | |
file.write(text.join("\n")) | |
end | |
end | |
def add_raw_content(div_id, title, body) | |
File.open("#{div_id}.xhtml", 'w') do |file| | |
text = [] | |
text << '<?xml version="1.0" encoding="utf-8" standalone="no"?>' | |
text << '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">' | |
text << '<html xmlns="http://www.w3.org/1999/xhtml">' | |
text << "<head><title>#{title}</title>" | |
text << '<link href="../Styles/style.css" rel="stylesheet" type="text/css" />' | |
text << '</head><body>' | |
text << "<div id=\"#{div_id}\" xml:lang=\"en-US\"> #{body} </div>" | |
text << "</body></html>" | |
file.write(text.join("\n")) | |
end | |
end | |
def make_container | |
File.open('container.xml', 'w') do |file| | |
text = [] | |
text << '<?xml version="1.0" encoding="UTF-8"?>' | |
text << '<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">' | |
text << ' <rootfiles>' | |
text << ' <rootfile full-path="OEBPS/content.opf" media-type="application/oebps-package+xml"/>' | |
text << ' </rootfiles>' | |
text << '</container>' | |
file.write(text.join("\n")) | |
end | |
end | |
def make_apple_hack | |
File.open('com.apple.ibooks.display-options.xml', 'w') do |file| | |
text = <<-FOO | |
<?xml version="1.0" encoding="UTF-8"?> | |
<display_options> | |
<platform name="*"> | |
<option name="specified-fonts">false</option> | |
</platform> | |
</display_options> | |
FOO | |
file.write(text) | |
end | |
end | |
def list_dir(dir) | |
entries = Dir.entries(dir) | |
entries.delete(".") | |
entries.delete("..") | |
entries.delete(".DS_Store") | |
entries | |
end | |
def manifest_text # MUST CONTAIN ALL THE FILES. | |
text = [] | |
# <!-- MANIFEST (mandatory)' | |
# List of all the resources of the book (XHTML, CSS, images,…).' | |
# The order of item elements in the manifest is NOT significant.' | |
# http://idpf.org/epub/20/spec/OPF_2.0.1_draft.htm#Section2.3 ' | |
# -->' | |
text << ' <item href="toc.ncx" id="ncx" media-type="application/x-dtbncx+xml" />' | |
text << ' <item href="Styles/style.css" id="css" media-type="text/css" />' | |
list_dir("Images").each do |entry| | |
type = entry.split('.').last | |
type = 'jpeg' if type == 'jpg' | |
text << " <item href=\"Images/#{entry}\" id=\"#{entry}\" media-type=\"image/#{type}\" />" | |
end | |
list_dir("Text").each do |entry| | |
text << " <item href=\"Text/#{entry}\" id=\"#{entry.split('.').first}\" media-type=\"application/xhtml+xml\" />" | |
end | |
return "<manifest>\n"+ text.join("\n") + "\n</manifest>" | |
end | |
def contributers_text | |
text = [] | |
text << '<!-- List of contributors' | |
text << 'See: MARC Code List for Relators: http://www.loc.gov/marc/relators/relaterm.html' | |
text << 'Examples: ' | |
text << '* Editor [edt]' | |
text << 'Use for a person or organization who prepares for publication a work not primarily his/her own,' | |
text << 'such as by elucidating text, adding introductory or other critical matter, or technically directing' | |
text << 'an editorial staff.' | |
text << '* Cover designer [cov]' | |
text << 'Use for a person or organization responsible for the graphic design of a book cover,' | |
text << 'album cover, slipcase, box, container, etc. For a person or organization responsible ' | |
text << 'for the graphic design of an entire book, use Book designer; for book jackets, use Bookjacket designer.' | |
text << '* Translator [trl]' | |
text << 'Use for a person or organization who renders a text from one language into another, or from an older ' | |
text << 'form of a language into the modern form.' | |
text << '-->' | |
text << '<!--' | |
text << '<dc:contributor opf:file-as="[LASTNAME, NAME]" opf:role="edt">[NAME LASTNAME]</dc:contributor>' | |
text << '<dc:contributor opf:file-as="[LASTNAME, NAME]" opf:role="cov">[NAME LASTNAME]</dc:contributor>' | |
text << '-->' | |
text.join("\n") | |
end | |
def spine_text | |
text = [] | |
# <!-- SPINE (mandatory)' | |
# The spine element defines the default reading order of the content. ' | |
# It does not list every file in the manifest, just the reading order.' | |
# The value of the idref tag in the spine has to match the ID tag for that entry in the manifest.' | |
# For example, if you have the following reference in your manifest:' | |
# <item id="chapter-1" href="chapter01.xhtml" media-type="application/xhtml+xml" />' | |
# your spine entry would be:' | |
# <itemref idref="chapter-1" />' | |
# http://idpf.org/epub/20/spec/OPF_2.0.1_draft.htm#Section2.4' | |
# -->' | |
text << '<spine toc="ncx">' | |
text << ' <itemref idref="cover" />' | |
text << ' <itemref idref="toc" />' | |
@content.keys.each do |div_id| | |
text << " <itemref idref=\"#{div_id}\" />" | |
end | |
text << '</spine>' | |
text.join("\n") | |
end | |
GUIDE_ROLES_MAP = Hash[guide_roles.zip(guide_roles)] | |
def guide_text | |
# <!-- GUIDE (optional, recommended by Apple) | |
# The guide lets you specify the role of the book's files. | |
# Available tags: cover, title-page, toc, index, glossary, acknowledgements, bibliography, | |
# colophon, copyright-page, dedication, epigraph, foreword, loi (list of illustrations), | |
# lot (list of tables), notes, preface, and text. | |
# http://idpf.org/epub/20/spec/OPF_2.0.1_draft.htm#Section2.6 | |
# --> | |
text = [] | |
text << '<guide>' | |
list_dir("Text").each do |entry| | |
name = entry.split('.').first | |
type = GUIDE_ROLES_MAP.fetch(name, 'text') | |
text << " <reference href=\"Text/#{entry}\" title=\"#{name.titlecase}\" type=\"#{type}\" />" | |
end | |
text << '</guide>' | |
text.join("\n") | |
end | |
def create_content_opf | |
text = [] | |
text << '<?xml version="1.0" encoding="utf-8" standalone="yes"?>' | |
text << '<package xmlns="http://www.idpf.org/2007/opf" unique-identifier="bookid" version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/">' | |
text << '<metadata xmlns:opf="http://www.idpf.org/2007/opf" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">' | |
text << " <dc:identifier id=\"bookid\" opf:scheme=\"ISBN\">urn:isbn:#{@isbn}</dc:identifier>" | |
text << " <dc:title>#{@book_title}</dc:title>" | |
text << " <dc:rights>Copyright © #{Date.current.year} #{@author_name}. All rights reserved.</dc:rights>" | |
text << " <dc:subject>#{@subject_classification}</dc:subject>" | |
text << " <dc:creator opf:file-as=\"#{@author_name}\" opf:role=\"aut\">#{@author_name}</dc:creator>" | |
text << ' <dc:source>https://calypso.net</dc:source>' | |
# text << contributers_text | |
text << " <dc:publisher>#{ @publisher }</dc:publisher>" | |
text << " <dc:date opf:event=\"publication\">#{ Date.current }</dc:date>" | |
text << ' <dc:language>en</dc:language>' #http://en.wikipedia.org/wiki/List_of_ISO_639-2_codes | |
text << " <dc:identifier opf:scheme=\"UUID\">urn:uuid:#{ SecureRandom.uuid }</dc:identifier>" | |
text << " <meta name=\"cover\" content=\"#{@cover_file_name}\" />" | |
text << '</metadata>' | |
text << manifest_text | |
text << spine_text | |
text << guide_text | |
text << '</package>' | |
File.open('content.opf', 'w') { |file| file.write(text.join("\n")) } | |
end | |
def create_toc_ncx | |
text = [] | |
text << '<?xml version="1.0" encoding="UTF-8" standalone="no" ?>' | |
text << '<!DOCTYPE ncx PUBLIC "-//NISO//DTD ncx 2005-1//EN" "http://www.daisy.org/z3986/2005/ncx-2005-1.dtd">' | |
text << '<ncx xmlns="http://www.daisy.org/z3986/2005/ncx/" version="2005-1">' | |
text << '<head>' | |
text << "<meta content=\"urn:isbn:#{@isbn}\" name=\"dtb:uid\"/>" | |
text << '<meta content="1" name="dtb:depth"/>' | |
text << '<meta content="0" name="dtb:totalPageCount"/>' | |
text << '<meta content="0" name="dtb:maxPageNumber"/>' | |
text << '</head>' | |
text << "<docTitle><text>#{@book_title}</text></docTitle>" | |
text << '<navMap>' | |
text << '<navPoint id="navpoint-cover"><navLabel><text>Cover</text></navLabel><content src="Text/cover.xhtml" /></navPoint>' | |
@content.each do |div_id, title| | |
text << "<navPoint id=\"#{div_id}\"><navLabel><text>#{title}</text></navLabel><content src=\"Text/#{div_id}.xhtml\" /></navPoint>" | |
end | |
text << '</navMap>' | |
text << '</ncx>' | |
File.open('toc.ncx', 'w') { |file| file.write(text.join("\n")) } | |
end | |
def create_toc_html | |
text = [] | |
text << '<?xml version="1.0" encoding="utf-8" standalone="no"?>' | |
text << '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"' | |
text << '"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">' | |
text << '<html xmlns="http://www.w3.org/1999/xhtml">' | |
text << '<head>' | |
text << " <title>#{@table_of_contents_title}</title>" | |
text << ' <link href="../Styles/style.css" rel="stylesheet" type="text/css" />' | |
text << '</head>' | |
text << '<body>' | |
text << ' <div id="toc" xml:lang="en-US">' | |
text << " <h1>#{@table_of_contents_title}</h1>" | |
text << ' <ul>' | |
@content.each do |div_id, title| | |
text << " <li><a href=\"../Text/#{div_id}.xhtml\"><span>#{title}</span></a></li>" | |
end | |
text << ' </ul>' | |
text << '</div></body></html>' | |
File.open('toc.xhtml', 'w') { |file| file.write(text.join("\n")) } | |
end | |
end | |
__END__ | |
def example_use | |
Dir.chdir("TMP") do | |
EpubGenerator.generate { |service| | |
service.book_title = "Daves Grand Adventure" | |
service.table_of_contents_title = "Steps into the future" | |
service.style = "h2{color: red}" | |
service.style << "h2{background-color: blue}" | |
service.add_chapter('chap_1', "Chapter 1", "<h2>Hello</h2>From the <i>overworld</i>.") | |
service.add_image('cover.jpeg', 'https://images.unsplash.com/photo-1546146477-15a587cd3fcb?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=564&q=80' ) | |
service.cover_file_name = 'cover.jpeg' | |
} | |
end | |
end | |
example_use |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment