Skip to content

Instantly share code, notes, and snippets.

@baash05
Last active January 17, 2019 02:31
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save baash05/d2b6dcb0a52f84c6ccc35ea28e1f4b59 to your computer and use it in GitHub Desktop.
Save baash05/d2b6dcb0a52f84c6ccc35ea28e1f4b59 to your computer and use it in GitHub Desktop.
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