# Copyright (C) 2004-2006 Laurent Sansonetti
# Copyright (C) 2007 Cathal Mc Ginley
# Copyright (C) 2014 Matijs van Zuijlen
#
# Alexandria is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 2 of the
# License, or (at your option) any later version.
#
# Alexandria is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public
# License along with Alexandria; see the file COPYING. If not,
# write to the Free Software Foundation, Inc., 51 Franklin Street,
# Fifth Floor, Boston, MA 02110-1301 USA.
# Export sorting added 23 Oct 2007 by Cathal Mc Ginley
# Classes LibrarySortOrder and SortedLibrary, and changed ExportFormat#invoke
# iPod Notes support added 20 January 2008 by Tim Malone
# require 'cgi'
begin # image_size is optional
$IMAGE_SIZE_LOADED = true
require 'image_size'
rescue LoadError
$IMAGE_SIZE_LOADED = false
puts "Can't load image_size, hence exported libraries are not optimized" if $DEBUG
end
module Alexandria
class LibrarySortOrder
include Logging
def initialize(book_attribute, ascending = true)
@book_attribute = book_attribute
@ascending = ascending
end
def sort(library)
sorted = library.sort_by do |book|
book.send(@book_attribute)
end
unless @ascending
sorted.reverse!
end
sorted
rescue => ex
trace = ex.backtrace.join("\n> ")
log.warn { "Could not sort library by #{@book_attribute} #{ex.message} #{trace}" }
library
end
def to_s
"#{@book_attribute} #{@ascending ? '(ascending)' : '(descending)'}"
end
class Unsorted < LibrarySortOrder
def initialize
end
def sort(library)
library
end
def to_s
'default order'
end
end
end
class SortedLibrary < Library
def initialize(library, sort_order)
super(library.name)
@library = library
sorted = sort_order.sort(library)
sorted.each do |book|
self << book
end
end
def cover(book)
@library.cover(book)
end
def final_cover(book)
@library.final_cover(book)
end
def copy_covers(dest)
@library.copy_covers(dest)
end
end
class ExportFormat
attr_reader :name, :ext, :message
include GetText
include Logging
extend GetText
bindtextdomain(Alexandria::TEXTDOMAIN, charset: 'UTF-8')
def self.all
[
new(_('Archived ONIX XML'), 'onix.tbz2', :export_as_onix_xml_archive),
new(_('Archived Tellico XML'), 'tc', :export_as_tellico_xml_archive),
new(_('BibTeX'), 'bib', :export_as_bibtex),
new(_('CSV list'), 'csv', :export_as_csv_list),
new(_('ISBN List'), 'txt', :export_as_isbn_list),
new(_('iPod Notes'), nil, :export_as_ipod_notes),
new(_('HTML Web Page'), nil, :export_as_html, true)
]
end
def invoke(library, sort_order, filename, *args)
if sort_order
sorted = SortedLibrary.new(library, sort_order)
log.debug { "Exporting library sorted by #{sort_order}" }
sorted.send(@message, filename, *args)
else
library.send(@message, filename, *args)
end
end
def needs_preview?
@needs_preview
end
private
def initialize(name, ext, message, needs_preview = false)
@name = name
@ext = ext
@message = message
@needs_preview = needs_preview
end
end
module Exportable
def export_as_onix_xml_archive(filename)
File.open(File.join(Dir.tmpdir, 'onix.xml'), 'w') do |io|
to_onix_document.write(io, 0)
end
copy_covers(File.join(Dir.tmpdir, 'images'))
Dir.chdir(Dir.tmpdir) do
output = `tar -cjf \"#{filename}\" onix.xml images 2>&1`
raise output unless $CHILD_STATUS.success?
end
FileUtils.rm_rf(File.join(Dir.tmpdir, 'images'))
FileUtils.rm(File.join(Dir.tmpdir, 'onix.xml'))
end
def export_as_tellico_xml_archive(filename)
File.open(File.join(Dir.tmpdir, 'tellico.xml'), 'w') do |io|
begin
to_tellico_document.write(io, 0)
rescue => ex
puts ex.message
puts ex.backtrace
raise ex
end
end
copy_covers(File.join(Dir.tmpdir, 'images'))
Dir.chdir(Dir.tmpdir) do
output = `zip -q -r \"#{filename}\" tellico.xml images 2>&1`
raise output unless $CHILD_STATUS.success?
end
FileUtils.rm_rf(File.join(Dir.tmpdir, 'images'))
FileUtils.rm(File.join(Dir.tmpdir, 'tellico.xml'))
end
def export_as_isbn_list(filename)
File.open(filename, 'w') do |io|
each do |book|
io.puts((book.isbn or ''))
end
end
end
def export_as_html(filename, theme)
FileUtils.mkdir(filename) unless File.exist?(filename)
Dir.chdir(filename) do
copy_covers('pixmaps')
FileUtils.cp_r(theme.pixmaps_directory,
'pixmaps') if theme.has_pixmaps?
FileUtils.cp(theme.css_file, '.')
File.open('index.html', 'w') do |io|
io << to_xhtml(File.basename(theme.css_file))
end
end
end
def export_as_bibtex(filename)
File.open(filename, 'w') do |io|
io << to_bibtex
end
end
def export_as_ipod_notes(filename, _theme)
FileUtils.mkdir(filename) unless File.exist?(filename)
tempdir = Dir.getwd
Dir.chdir(filename)
copy_covers('pixmaps')
File.open('index.linx', 'w') do |io|
io.puts '
' + name + ''
each do |book|
io.puts '' + book.title + ''
end
io.close
end
each do |book|
File.open(book.ident, 'w') do |io|
io.puts "#{book.title} "
# put a link to the book's cover. only works on iPod 5G and above(?).
if File.exist?(cover(book))
io.puts '' + book.title + ''
else
io.puts book.title
end
io.puts book.authors.join(', ')
io.puts book.edition
io.puts((book.isbn or ''))
# we need to close the files so the iPod can be ejected/unmounted without us closing Alexandria
io.close
end
end
# Again, allow the iPod to unmount
Dir.chdir(tempdir)
end
def export_as_csv_list(filename)
File.open(filename, 'w') do |io|
io.puts 'Title' + ';' + 'Authors' + ';' + 'Publisher' + ';' + 'Edition' + ';' + 'ISBN' + ';' + 'Year Published' + ';' + 'Rating' + "(0 to #{UI::MainApp::MAX_RATING_STARS})" + ';' + 'Notes' + ';' + 'Want?' + ';' + 'Read?' + ';' + 'Own?' + ';' + 'Tags'
each do |book|
io.puts book.title + ';' + book.authors.join(', ') + ';' + (book.publisher or '') + ';' + (book.edition or '') + ';' + (book.isbn or '') + ';' + (book.publishing_year.to_s or '') + ';' + (book.rating.to_s or '0') + ';' + (book.notes or '') + ';' + (book.want ? '1' : '0') + ';' + (book.redd ? '1' : '0') + ';' + (book.own ? '1' : '0') + ';' + (book.tags ? book.tags.join(', ') : '')
end
end
end
private
ONIX_DTD_URL = 'http://www.editeur.org/onix/2.1/reference/onix-international.dtd'
def to_onix_document
doc = REXML::Document.new
doc << REXML::XMLDecl.new
doc << REXML::DocType.new('ONIXMessage',
"SYSTEM \"#{ONIX_DTD_URL}\"")
msg = doc.add_element('ONIXMessage')
header = msg.add_element('Header')
header.add_element('FromCompany').text = 'Alexandria'
header.add_element('FromPerson').text = Etc.getlogin
now = Time.now
header.add_element('SentDate').text = '%.4d%.2d%.2d%.2d%.2d' % [
now.year, now.month, now.day, now.hour, now.min
]
header.add_element('MessageNote').text = name
each_with_index do |book, idx|
# fields that are missing: edition and rating.
prod = msg.add_element('Product')
prod.add_element('RecordReference').text = idx
prod.add_element('NotificationType').text = '03' # confirmed
prod.add_element('RecordSourceName').text =
'Alexandria ' + Alexandria::DISPLAY_VERSION
prod.add_element('ISBN').text = (book.isbn or '')
prod.add_element('ProductForm').text = 'BA' # book
prod.add_element('DistinctiveTitle').text = book.title
unless book.authors.empty?
book.authors.each do |author|
elem = prod.add_element('Contributor')
# author
elem.add_element('ContributorRole').text = 'A01'
elem.add_element('PersonName').text = author
end
end
if book.notes and !book.notes.empty?
elem = prod.add_element('OtherText')
# reader description
elem.add_element('TextTypeCode').text = '12'
elem.add_element('TextFormat').text = '00' # ASCII
elem.add_element('Text').text = book.notes
end
if File.exist?(cover(book))
elem = prod.add_element('MediaFile')
# front cover image
elem.add_element('MediaFileTypeCode').text = '04'
elem.add_element('MediaFileFormatCode').text =
(Library.jpeg?(cover(book)) ? '03' : '02')
# filename
elem.add_element('MediaFileLinkTypeCode').text = '06'
elem.add_element('MediaFileLink').text =
File.join('images', final_cover(book))
end
if book.isbn
BookProviders.each do |provider|
elem = prod.add_element('ProductWebsite')
elem.add_element('ProductWebsiteDescription').text =
provider.fullname
elem.add_element('ProductWebsiteLink').text =
provider.url(book)
end
end
elem = prod.add_element('Publisher')
elem.add_element('PublishingRole').text = '01'
elem.add_element('PublisherName').text = book.publisher
prod.add_element('PublicationDate').text = book.publishing_year
end
doc
end
def to_tellico_document
# For the Tellico format, see
# http://periapsis.org/tellico/doc/hacking.html
doc = REXML::Document.new
doc << REXML::XMLDecl.new
doc << REXML::DocType.new('tellico', "PUBLIC \"-//Robby Stephenson/DTD Tellico V7.0//EN\" \"http://periapsis.org/tellico/dtd/v7/tellico.dtd\"")
tellico = doc.add_element('tellico')
tellico.add_attribute('syntaxVersion', '7')
tellico.add_namespace('http://periapsis.org/tellico/')
collection = tellico.add_element('collection')
collection.add_attribute('title', name)
collection.add_attribute('type', '2')
fields = collection.add_element('fields')
field1 = fields.add_element('field')
# a field named _default implies adding all default book
# collection fields
field1.add_attribute('name', '_default')
images = collection.add_element('images')
each_with_index do |book, idx|
entry = collection.add_element('entry')
new_index = (idx + 1).to_s
entry.add_attribute('id', new_index)
# translate the binding
entry.add_element('title').text = book.title
entry.add_element('isbn').text = (book.isbn or '')
entry.add_element('pub_year').text = book.publishing_year
entry.add_element('binding').text = book.edition
entry.add_element('publisher').text = book.publisher
unless book.authors.empty?
authors = entry.add_element('authors')
book.authors.each do |author|
authors.add_element('author').text = author
end
end
entry.add_element('read').text = book.redd.to_s if book.redd
entry.add_element('loaned').text = book.loaned.to_s if book.loaned
unless book.rating == Book::DEFAULT_RATING
entry.add_element('rating').text = book.rating
end
if book.notes and !book.notes.empty?
entry.add_element('comments').text = book.notes
end
if File.exist?(cover(book))
entry.add_element('cover').text = final_cover(book)
image = images.add_element('image')
image.add_attribute('id', final_cover(book))
if $IMAGE_SIZE_LOADED
image_s = ImageSize.new(IO.read(cover(book)))
image.add_attribute('height', image_s.get_height.to_s)
image.add_attribute('width', image_s.get_width.to_s)
image.add_attribute('format', image_s.get_type)
else
image.add_attribute('format',
Library.jpeg?(cover(book)) ? 'JPEG' : 'GIF')
end
end
end
doc
end
def xhtml_escape(str)
escaped = str.dup
# used to occasionally use CGI.escapeHTML
escaped.gsub!(/&/, '&')
escaped.gsub!(/, '<')
escaped.gsub!(/>/, '>')
escaped.gsub!(/\"/, '"')
escaped
end
def to_xhtml(css)
generator = 'Alexandria ' + Alexandria::DISPLAY_VERSION
xhtml = ''
xhtml << <#{xhtml_escape(name)}
#{xhtml_escape(name)}
EOS
each do |book|
xhtml << <
#{book.isbn}
EOS
if File.exist?(cover(book))
xhtml << <
EOS
else
xhtml << <
EOS
end
unless book.title.nil?
xhtml << <#{xhtml_escape(book.title)}
EOS
end
unless book.authors.empty?
xhtml << "
"
book.authors.each do |author|
xhtml << <#{xhtml_escape(author)}
EOS
end
xhtml << '
'
end
unless book.edition.nil?
xhtml << <#{xhtml_escape(book.edition)}
EOS
end
unless book.publisher.nil?
xhtml << <#{xhtml_escape(book.publisher)}
EOS
end
xhtml << <
EOS
end
xhtml << <
Generated on #{xhtml_escape(Date.today.to_s)} by #{xhtml_escape(generator)}.
EOS
end
def to_bibtex
generator = 'Alexandria ' + Alexandria::DISPLAY_VERSION
bibtex = ''
bibtex << "\%Generated on #{Date.today} by: #{generator}\n"
bibtex << "\%\n"
bibtex << "\n"
auths = Hash.new(0)
each do |book|
k = (book.authors[0] or 'Anonymous').split[0]
if auths.key?(k)
auths[k] += 1
else
auths[k] = 1
end
cite_key = k + auths[k].to_s
bibtex << "@BOOK{#{cite_key},\n"
bibtex << "author = \""
if book.authors != []
bibtex << book.authors[0]
book.authors[1..-1].each do |author|
bibtex << " and #{latex_escape(author)}"
end
end
bibtex << "\",\n"
bibtex << "title = \"#{latex_escape(book.title)}\",\n"
bibtex << "publisher = \"#{latex_escape(book.publisher)}\",\n"
if book.notes and !book.notes.empty?
bibtex << "OPTnote = \"#{latex_escape(book.notes)}\",\n"
end
# year is a required field in bibtex @BOOK
bibtex << 'year = ' + (book.publishing_year or "\"n/a\"").to_s + "\n"
bibtex << "}\n\n"
end
bibtex
end
def latex_escape(str)
return '' if str.nil?
my_str = str.dup
my_str.gsub!(/%/, '\\%')
my_str.gsub!(/~/, '\\textasciitilde')
my_str.gsub!(/\&/, '\\\\&')
my_str.gsub!(/\#/, '\\\\#')
my_str.gsub!(/\{/, '\\{')
my_str.gsub!(/\}/, '\\}')
my_str.gsub!(/_/, '\\_')
my_str.gsub!(/\$/, "\\\$")
my_str.gsub!(/\"(.+)\"/, "``\1''")
my_str
end
end
class Library
include Exportable
end
class SmartLibrary
include Exportable
end
end