# -*-perl-*-
# epub3.pm: setup an EPUB publication
#
# Copyright 2021-2023 Free Software Foundation, Inc.
#
# This program 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 3 of the License,
# or (at your option) any later version.
#
# This program 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 this program. If not, see .
#
# Originally written by Patrice Dumas.
#
#
# TODO:
#
# Discuss unique identifier on the mailing list
#
# Discuss last change date on the mailing list
#
# Currently the titlepage is used if available, while the Top node
# is not shown. There is a possibility to use an image as cover in
# EPUB, with cover-image property in a manifest item. Add the possibility
# to specify such cover image? In that case, set SHOW_TITLE to 0?
#
# Do not output a TOC in the default case as the readers can always use
# the navigation information?
#
# do something special for indices?
#
# do something special for list of tables/list of floats?
#
# add landmarks? Examples: epub:type="toc", epub:type="loi" (list of illustrations)
# epub:type="bodymatter" (Start of Content)
#
#
# NOTES:
#
# Tests show that the navigation information as a page is not nicely
# rendered, it is better to use the Texinfo TOC if a TOC is needed.
#
# OUTFILE is used for the epub file, but it is reset for the conversion
# to XHTML. This is described in the documentation.
#
# cross manual references:
# The (X)HTML files that are target of links that point to the EPUB container
# (or maybe any relative link, the standard is not easy to understand) need
# to be in the container (they are non foreign Publication Resources).
# Other links are not Publication Resources, and are ok.
# Therefore, it is better to specify the external manuals as web HTML
# absolute urls in hyperlinks, and not as manuals in a collection.
# Therefore we set a warning for external manuals not resolved using htmlxref
# by setting CHECK_HTMLXREF.
#
# Setting up collections of manuals for local browsing:
# It seems that putting more than one manual in an EPUB container
# is incorrect. Each manual on the command line is therefore put in a
# separate epub container file.
# There is no described way to group manuals in a collection, nor
# to refer to a manual in an epub container, and to a specific file in
# that container.
# Collections of epub manunals for local browsing would be an interesting
# feature, but for now cannot be achieved because of those limitations.
# If references to other EPUB files were possible, NODE_FILES would
# probably need to be set.
# For now, external manuals not found in htmlxref are resolved
# to a path that makes no sense, for example for a reference to the
# Pod-Simple-Texinfo manual:
# EPUB/Pod-Simple-Texinfo_epub3/index.html
use strict;
# for accented character in a comment
use utf8;
# To check if there is no erroneous autovivification
#no autovivification qw(fetch delete exists store strict);
use File::Path;
use File::Spec;
use File::Copy;
# for fileparse
use File::Basename;
use Encode qw(decode);
# also for __(
use Texinfo::Common;
use Texinfo::Convert::Utils;
use Texinfo::Convert::Text;
# try to load here, but only complain and return an error later
# when the customization variables are known.
eval { require Archive::Zip; };
my $archive_zip_loading_error = $@;
# the 3.2 spec was used for the implementation. However, it seems to be
# designed to be backward compatible with 3.0 and mandates to use 3.0 as
# version.
my $epub_format_version = '3.0';
# used in tests to avoid creating the .epub file.
texinfo_set_from_init_file('EPUB_CREATE_CONTAINER_FILE', 1);
texinfo_set_format_from_init_file('html');
# output valid XHTML as per the specification
# Any Publication Resource that is an XML-Based Media Type MUST
# be a conformant XML 1.0 Document ... MUST be encoded in UTF-8 or UTF-16.
texinfo_set_from_init_file('HTML_ROOT_ELEMENT_ATTRIBUTES',
'xmlns="http://www.w3.org/1999/xhtml"');
texinfo_set_from_init_file('NO_CUSTOM_HTML_ATTRIBUTE', 1);
texinfo_set_from_init_file('USE_XML_SYNTAX', 1);
texinfo_set_from_init_file('DOCTYPE', ''."\n"
.'');
texinfo_set_from_init_file('USE_NUMERIC_ENTITY', 1);
texinfo_set_from_init_file('OUTPUT_ENCODING_NAME', 'utf-8');
# this is actually the default
texinfo_set_from_init_file('DOC_ENCODING_FOR_OUTPUT_FILE_NAME', 0);
# the specification says "File Names and Paths MUST be UTF-8 [Unicode] encoded."
# This is also needed for Archive::Zip in case there are non ascii
# file name.
# As a consequence, the epub file file name is also always utf-8 encoded.
texinfo_set_from_init_file('OUTPUT_FILE_NAME_ENCODING', 'utf-8');
# an output as similar as possible to a book is expected for epub.
# So we set NO_TOP_NODE_OUTPUT, which in turn sets titlepage to be used
# if not otherwise specified.
texinfo_set_from_init_file('NO_TOP_NODE_OUTPUT', 1);
# no mini_toc nor menus in the default case, to be more like a book.
texinfo_set_from_init_file('FORMAT_MENU', 'nomenu');
# use sections in printindex
# unsetting NODE_NAME_IN_INDEX is not sufficient, as in that case the
# element is used to determine the name used, which will still be the
# node in the default case
texinfo_set_from_init_file('USE_NODES', 0);
#texinfo_set_from_init_file('NODE_NAME_IN_INDEX', 0);
# a footer gets in the way of navigation. It is not set in the default
# case anyway, but it is set in texi2html style.
texinfo_set_from_init_file('PROGRAM_NAME_IN_FOOTER', 0);
# split at chapter such that ebook readers start a new page for
# a new chapter. Splitting at nodes output is not so good as node content
# can be very small.
texinfo_set_from_init_file('SPLIT', 'chapter');
# the copiable anchor paragraph sign is always present and no link is
# shown in the calibre epub reader. Since it looks strange, unset.
texinfo_set_from_init_file('COPIABLE_LINKS', 0);
# this is for the XHTML formatting, the .epub extension is
# also used hardcoded for the container.
texinfo_set_from_init_file('EXTENSION', 'xhtml');
# It is better for external manuals not to be publication resources,
# for that an absolute URL need to be used, so we warn if a manual is not
# found through htmlxref.
texinfo_set_from_init_file('CHECK_HTMLXREF', 1);
# explicit references to TOP_NODE_UP are ignored
texinfo_set_from_init_file('IGNORE_REF_TO_TOP_NODE_UP', 1);
# Better use html for external manuals than the xhtml EXTENSION
texinfo_set_from_init_file('EXTERNAL_CROSSREF_EXTENSION', 'html');
texinfo_set_from_init_file('JS_WEBLABELS_FILE', 'js_licenses.xhtml');
texinfo_set_from_init_file('TOP_FILE', undef);
# no redirections files
texinfo_set_from_init_file('NODE_FILES', 0);
my $epub_images_dir_name = 'images';
texinfo_set_from_init_file('IMAGE_LINK_PREFIX', "../${epub_images_dir_name}/");
#texinfo_set_from_init_file('contents', 1);
texinfo_set_from_init_file('DEFAULT_RULE', '');
texinfo_set_from_init_file('BIG_RULE', '');
texinfo_set_from_init_file('HEADERS', 0);
texinfo_register_formatting_function('format_navigation_header', \&epub_noop);
texinfo_register_formatting_function('format_navigation_panel', \&epub_noop);
texinfo_register_command_formatting('image', \&epub_convert_image_command);
texinfo_register_type_formatting('unit', \&epub_convert_tree_unit_type);
texinfo_register_type_formatting('special_element',
\&epub_convert_special_element_type);
my %epub_images_extensions_mimetypes = (
'.png' => 'image/png',
'.jpg' => 'image/jpeg',
'.jpeg' => 'image/jpeg',
'.gif' => 'image/gif',
);
my %epub_js_extensions_mimetypes = (
'.js', 'text/javascript',
'.css', 'text/css',
);
sub _epub_convert_tree_to_text($$;$)
{
my $converter = shift;
my $tree = shift;
my $options = shift;
$options = {} if (!defined($options));
return &{$converter->formatting_function('format_protect_text')}($converter,
Texinfo::Convert::Text::convert_to_text($tree,
{Texinfo::Convert::Text::copy_options_for_convert_text($converter, 1),
%$options}));
}
sub epub_noop($$)
{
return '';
}
# file scope variables. Beware to reset all that are not constants
# at the beginning of epub_setup for multi input Texinfo manual cases.
my $epub_destination_directory;
my $epub_document_destination_directory;
my $encoded_epub_destination_directory;
my $epub_document_dir_name = 'EPUB';
my %epub_images;
# the image number, to make sure that there is no name clash when
# putting all the images in the same directory.
my $epub_file_nr;
# collect and copy images
sub epub_convert_image_command($$$$)
{
my $self = shift;
my $cmdname = shift;
my $command = shift;
my $args = shift;
if (defined($args->[0]->{'filenametext'})
and $args->[0]->{'filenametext'} ne '') {
my $basefile = $args->[0]->{'filenametext'};
return $basefile if ($self->in_string());
my ($image_file, $image_basefile, $image_extension, $image_path,
$image_path_encoding)
= $self->html_image_file_location_name($cmdname, $command, $args);
if (not defined($image_path)) {
# FIXME using an internal function. Also not clear if it is correct to
# use it, as it is not used for other messages
$self->_noticed_line_warn(sprintf(
__("\@image file `%s' (for HTML) not found, using `%s'"),
$image_basefile, $image_file), $command->{'source_info'});
}
my ($volume, $directories, $image_basefile_name)
= File::Spec->splitpath($image_basefile);
my $protected_image_basefile_name
= Texinfo::Convert::NodeNameNormalization::transliterate_protect_file_name(
$image_basefile_name);
my $protected_image_extension
= Texinfo::Convert::NodeNameNormalization::transliterate_protect_file_name(
$image_extension);
# -5 for the extension and -10 for $epub_file_nr
my $cropped_image_basefile_name
= substr($protected_image_basefile_name, 0,
$self->get_conf('BASEFILENAME_LENGTH') - 15);
my $destination_basefile_name = $epub_file_nr.'-'.$cropped_image_basefile_name
. $protected_image_extension;
$epub_file_nr += 1;
if (defined($image_file)) {
if (not defined($image_path)) {
$self->document_error($self,
sprintf(__("\@image file `%s' can not be copied"),
$image_basefile));
} else {
my $images_destination_dir
= File::Spec->catdir($epub_destination_directory,
$epub_document_dir_name, $epub_images_dir_name);
my ($encoded_images_destination_dir, $images_destination_dir_encoding)
= $self->encoded_output_file_name($images_destination_dir);
my $error_creating_dir;
if (! -d $encoded_images_destination_dir) {
if (!mkdir($encoded_images_destination_dir, oct(755))) {
$self->document_error($self, sprintf(__(
"could not create images directory `%s': %s"),
$images_destination_dir, $!));
$error_creating_dir = 1;
}
}
if (not $error_creating_dir) {
my $image_destination_path_name
= File::Spec->catfile($images_destination_dir,
$destination_basefile_name);
my ($encoded_image_dest_path_name, $image_dest_path_encoding)
= $self->encoded_output_file_name($image_destination_path_name);
my $copy_succeeded = copy($image_path, $encoded_image_dest_path_name);
if (not $copy_succeeded) {
my $image_path_text;
if (defined($image_path_encoding)) {
$image_path_text = decode($image_path_encoding, $image_path);
} else {
$image_path_text = $image_path;
}
$self->document_error($self, sprintf(__(
"could not copy `%s' to `%s': %s"),
$image_path_text, $image_destination_path_name, $!));
}
$epub_images{$destination_basefile_name} = $image_extension;
}
}
}
# Now format. Following code is similar to the default formatting
# code.
my $destination_file_name;
# should always be set
if (defined($self->get_conf('IMAGE_LINK_PREFIX'))) {
$destination_file_name = $self->get_conf('IMAGE_LINK_PREFIX')
. $destination_basefile_name;
} else {
$destination_file_name = $destination_basefile_name;
}
my $alt_string;
if (defined($args->[3]) and defined($args->[3]->{'string'})) {
$alt_string = $args->[3]->{'string'};
}
if (!defined($alt_string) or ($alt_string eq '')) {
$alt_string
= &{$self->formatting_function('format_protect_text')}($self, $basefile);
}
return $self->close_html_lone_element(
$self->html_attribute_class('img', [$cmdname])
. ' src="'.$self->url_protect_file_text($destination_file_name)
."\" alt=\"$alt_string\"");
}
return '';
}
my @epub_tree_units_output_filenames;
# collect filenames in units order
sub epub_convert_tree_unit_type($$$$)
{
my $self = shift;
my $type = shift;
my $element = shift;
my $content = shift;
push @epub_tree_units_output_filenames,
$element->{'structure'}->{'unit_filename'}
unless grep {$_ eq $element->{'structure'}->{'unit_filename'}}
@epub_tree_units_output_filenames;
return &{$self->default_type_conversion($type)}($self,
$type, $element, $content);
}
my @epub_special_elements_filenames;
# collect filenames in order
sub epub_convert_special_element_type($$$$)
{
my $self = shift;
my $type = shift;
my $element = shift;
my $content = shift;
push @epub_special_elements_filenames,
$element->{'structure'}->{'unit_filename'}
unless grep {$_ eq $element->{'structure'}->{'unit_filename'}}
@epub_special_elements_filenames;
return &{$self->default_type_conversion($type)}($self,
$type, $element, $content);
}
sub _epub_remove_container_folder($$)
{
my $self = shift;
my $encoded_epub_destination_directory = shift;
my $err_remove_tree;
File::Path::remove_tree($encoded_epub_destination_directory,
{'error' => $err_remove_tree});
if ($err_remove_tree and scalar(@$err_remove_tree)) {
for my $diag (@$err_remove_tree) {
my ($file, $message) = %$diag;
if ($file eq '') {
$self->document_error($self,
sprintf(__("error removing directory: %s: %s"),
$epub_destination_directory, $message));
}
else {
$self->document_error($self,
sprintf(__("error removing directory: %s: unlinking %s: %s"),
$epub_destination_directory, $file, $message));
}
}
return 1;
}
return 0;
}
my $epub_xhtml_dir = 'xhtml';
# should not clash with generated files. Could clash with
# OUTFILE but this case is explicitly handled below.
my $default_nav_filename = 'nav_toc.xhtml';
my $nav_filename;
my $epub_outfile;
my $epub_info_js_dir_name;
sub epub_setup($)
{
my $self = shift;
$epub_outfile = undef;
$epub_destination_directory = undef;
$epub_document_destination_directory = undef;
$encoded_epub_destination_directory = undef;
@epub_tree_units_output_filenames = ();
@epub_special_elements_filenames = ();
%epub_images = ();
$nav_filename = $default_nav_filename;
$epub_file_nr = 1;
if (not defined($self->get_conf('EPUB_CREATE_CONTAINER_FILE'))) {
if (not $self->get_conf('TEST')) {
$self->set_conf('EPUB_CREATE_CONTAINER_FILE', 1);
}
}
if ($self->get_conf('EPUB_CREATE_CONTAINER_FILE')
and $archive_zip_loading_error) {
$self->document_error($self,
__("Archive::Zip is required for EPUB file output"));
return 150;
}
if (not defined($self->get_conf('EPUB_KEEP_CONTAINER_FOLDER'))) {
if ($self->get_conf('TEST') or $self->get_conf('DEBUG')) {
$self->set_conf('EPUB_KEEP_CONTAINER_FOLDER', 1);
}
}
$epub_info_js_dir_name = undef;
if ($self->get_conf('INFO_JS_DIR')) {
# re-set INFO_JS_DIR up to have the javascript and
# css files in a directory rooted at $epub_document_dir_name
$epub_info_js_dir_name = $self->get_conf('INFO_JS_DIR');
my $updir = File::Spec->updir();
# FIXME INFO_JS_DIR is used both as a filesystem directory name
# and as path in HTML files. As a path in HTML, / should always be
# used. File::Spec->catdir is better for filesystem paths. We finally
# hardocde '/' as separator because it is needed for HTML paths and it
# works for both for Unix and Windows filesystems, but it is not
# clean.
#$self->force_conf('INFO_JS_DIR', File::Spec->catdir($updir,
# $epub_info_js_dir_name));
$self->force_conf('INFO_JS_DIR', join('/', ($updir,
$epub_info_js_dir_name)));
# TODO make sure it is SPLIT and set SPLIT if not?
}
# determine main epub directory and directory for xhtml files,
# reset OUTFILE and SUBDIR to match with the epub directory
# for XHTML output
if (defined($self->get_conf('OUTFILE'))) {
$epub_outfile = $self->get_conf('OUTFILE');
# if not undef, will be used as directory name in
# determine_files_and_directory() which does not make sense
if ($self->get_conf('SPLIT')) {
$self->force_conf('OUTFILE', undef);
}
}
my ($output_file, $destination_directory, $output_filename,
$document_name) = $self->determine_files_and_directory('epub_package');
if (not defined($epub_outfile)) {
$epub_outfile = ${document_name}.'.epub';
}
# the $epub_destination_directory is removed automatically,
# so we try to set it to a directory that the user would not create
# nor populate with files.
if (defined($self->get_conf('SUBDIR'))) {
$epub_destination_directory = File::Spec->catdir($self->get_conf('SUBDIR'),
$document_name . '_epub_package');
} elsif ($self->get_conf('SPLIT')) {
$epub_destination_directory = $destination_directory;
} else {
$epub_destination_directory = $document_name . '_epub_package';
}
$epub_document_destination_directory
= File::Spec->catdir($epub_destination_directory,
$epub_document_dir_name, $epub_xhtml_dir);
# set for XHTML conversion
if ($self->get_conf('SPLIT')) {
$self->force_conf('SUBDIR', $epub_document_destination_directory);
$self->force_conf('OUTFILE', undef);
} else {
my $xhtml_output_file = $document_name;
$xhtml_output_file .= '.'.$self->get_conf('EXTENSION')
if (defined($self->get_conf('EXTENSION'))
and $self->get_conf('EXTENSION') ne '');
# to avoid a clash with nav file name
if ($xhtml_output_file eq $default_nav_filename) {
$nav_filename = 'Gtexinfo_' . $default_nav_filename;
}
$self->force_conf('OUTFILE',
File::Spec->catfile($epub_document_destination_directory, $xhtml_output_file));
}
my $epub_destination_dir_encoding;
($encoded_epub_destination_directory, $epub_destination_dir_encoding)
= $self->encoded_output_file_name($epub_destination_directory);
my $status = _epub_remove_container_folder($self,
$encoded_epub_destination_directory);
return $status if ($status);
my $err_make_path;
my ($encoded_epub_document_destination_directory, $epub_doc_dest_dir_encoding)
= $self->encoded_output_file_name($epub_document_destination_directory);
File::Path::make_path($encoded_epub_document_destination_directory,
{'mode' => 0755, 'error' => $err_make_path});
if ($err_make_path and scalar(@$err_make_path)) {
for my $diag (@$err_make_path) {
my ($file, $message) = %$diag;
if ($file eq '') {
$self->document_error($self,
sprintf(__("error creating directory: %s: %s"),
$epub_document_destination_directory, $message));
}
else {
$self->document_error($self,
sprintf(__("error creating directory: %s: creating %s: %s"),
$epub_document_destination_directory, $file, $message));
}
}
return 150;
}
return 0;
}
texinfo_register_handler('setup', \&epub_setup);
# need to be after tree units and images conversion
sub epub_finish($$)
{
my $self = shift;
my $document_root = shift;
my @epub_output_filenames = (@epub_tree_units_output_filenames,
@epub_special_elements_filenames);
if (scalar(@epub_output_filenames) == 0) {
if (defined($self->{'current_filename'})) {
push @epub_output_filenames, $self->{'current_filename'};
} else {
$self->document_warn($self,
__("epub: no filename output"));
}
}
my $meta_inf_directory_name = 'META-INF';
my $meta_inf_directory = File::Spec->catdir($epub_destination_directory,
$meta_inf_directory_name);
my ($encoded_meta_inf_directory, $meta_inf_directory_encoding)
= $self->encoded_output_file_name($meta_inf_directory);
if (!mkdir($encoded_meta_inf_directory, oct(755))) {
$self->document_error($self, sprintf(__(
"could not create meta informations directory `%s': %s"),
$meta_inf_directory, $!));
return 1;
}
my $container_file_path_name = File::Spec->catfile($meta_inf_directory,
'container.xml');
my ($encoded_container_file_path_name, $container_path_encoding)
= $self->encoded_output_file_name($container_file_path_name);
my ($container_fh, $error_message_container)
= Texinfo::Common::output_files_open_out(
$self->output_files_information(), $self,
$encoded_container_file_path_name, undef, 'utf-8');
if (!defined($container_fh)) {
$self->document_error($self,
sprintf(__("epub3.pm: could not open %s for writing: %s\n"),
$container_file_path_name, $error_message_container));
return 1;
}
my $document_name = $self->get_info('document_name');
my $opf_filename = $document_name . '.opf';
print $container_fh <
EOT
Texinfo::Common::output_files_register_closed(
$self->output_files_information(), $encoded_container_file_path_name);
if (!close ($container_fh)) {
$self->document_error($self,
sprintf(__("epub3.pm: error on closing %s: %s"),
$container_file_path_name, $!));
return 1;
}
my $mimetype_filename = 'mimetype';
my $mimetype_file_path_name = File::Spec->catfile($epub_destination_directory,
$mimetype_filename);
my ($encoded_mimetype_file_path_name, $mimetype_path_encoding)
= $self->encoded_output_file_name($mimetype_file_path_name);
my ($mimetype_fh, $error_message_mimetype)
= Texinfo::Common::output_files_open_out(
$self->output_files_information(), $self,
$encoded_mimetype_file_path_name, undef, 'utf-8');
if (!defined($mimetype_fh)) {
$self->document_error($self,
sprintf(__("epub3.pm: could not open %s for writing: %s\n"),
$mimetype_file_path_name, $error_message_mimetype));
return 1;
}
# There is no end of line. It is not very clear in the standard, but
# example files demonstrate clearly that there should not be end of lines.
print $mimetype_fh 'application/epub+zip';
Texinfo::Common::output_files_register_closed(
$self->output_files_information(), $encoded_mimetype_file_path_name);
if (!close ($mimetype_fh)) {
$self->document_error($self,
sprintf(__("epub3.pm: error on closing %s: %s"),
$mimetype_file_path_name, $!));
return 1;
}
my $nav_id = 'nav';
my $nav_file_path_name;
my $title = _epub_convert_tree_to_text($self, $self->get_info('title_tree'));
if ($self->{'structuring'} and $self->{'structuring'}->{'sectioning_root'}) {
$nav_file_path_name
= File::Spec->catfile($epub_document_destination_directory, $nav_filename);
my ($encoded_nav_file_path_name, $nav_path_encoding)
= $self->encoded_output_file_name($nav_file_path_name);
my ($nav_fh, $error_message_nav)
= Texinfo::Common::output_files_open_out(
$self->output_files_information(), $self,
$encoded_nav_file_path_name, undef, 'utf-8');
if (!defined($nav_fh)) {
$self->document_error($self,
sprintf(__("epub3.pm: could not open %s for writing: %s\n"),
$nav_file_path_name, $error_message_nav));
return 1;
}
my $table_of_content_str = _epub_convert_tree_to_text($self,
$self->gdt('Table of contents'));
my $nav_file_title = $title.' - '.$table_of_content_str;
print $nav_fh <
$nav_file_title
'."\n";
# TODO add landmarks?
print $nav_fh ''."\n".''."\n";
Texinfo::Common::output_files_register_closed(
$self->output_files_information(), $encoded_nav_file_path_name);
if (!close ($nav_fh)) {
$self->document_error($self,
sprintf(__("epub3.pm: error on closing %s: %s"),
$nav_file_path_name, $!));
return 1;
}
}
my $unique_uid = 'texi-uid';
# TODO to discuss on bug-texinfo
my $identifier = 'texinfo:'.$document_name;
# FIXME the dcterms:modified is mandatory, and it is also mandatory that it is a date:
# each Rendition MUST include exactly one [DCTERMS] modified property containing its last modification date. The value of this property MUST be an [XMLSCHEMA-2] dateTime conformant date of the form:
# CCYY-MM-DDThh:mm:ssZ
#
# The last modification date MUST be expressed in Coordinated Universal Time (UTC) and MUST be terminated by the "Z" (Zulu) time zone indicator.
#
# 2012-03-05T12:47:00Z
# to discuss
#
my $opf_file_path_name = File::Spec->catfile($epub_destination_directory,
$epub_document_dir_name, $opf_filename);
my ($encoded_opf_file_path_name, $opf_path_encoding)
= $self->encoded_output_file_name($opf_file_path_name);
my ($opf_fh, $error_message_opf)
= Texinfo::Common::output_files_open_out(
$self->output_files_information(), $self,
$encoded_opf_file_path_name, undef, 'utf-8');
if (!defined($opf_fh)) {
$self->document_error($self,
sprintf(__("epub3.pm: could not open %s for writing: %s\n"),
$opf_file_path_name, $error_message_opf));
return 1;
}
print $opf_fh <
$identifier
$title
EOT
my @relevant_commands = ('author', 'documentlanguage');
my $collected_commands = Texinfo::Common::collect_commands_list_in_tree(
$document_root, \@relevant_commands);
my @authors = ();
my @languages = ();
if (scalar(@{$collected_commands})) {
foreach my $element (@{$collected_commands}) {
my $command = $element->{'cmdname'};
if ($command eq 'author') {
if ($element->{'extra'}->{'titlepage'}
and $element->{'args'}->[0]->{'contents'}) {
my $author_str = _epub_convert_tree_to_text($self,
{'contents' => $element->{'args'}->[0]->{'contents'}});
if ($author_str =~ /\S/) {
push @authors, $author_str;
}
}
} else {
if (defined($element->{'extra'}->{'text_arg'})) {
# TODO the EPUB specification describes specific language
# tags. Not sure there is not a need for some mapping here.
push @languages, $element->{'extra'}->{'text_arg'};
}
}
}
}
# the standard mandates at least one language specifier
if (scalar(@languages) == 0) {
@languages = ('en');
}
foreach my $language (@languages) {
print $opf_fh " $language\n";
}
foreach my $author (@authors) {
print $opf_fh " $author\n";
}
print $opf_fh <
EOT
if (defined($nav_file_path_name)) {
print $opf_fh " - \n";
}
my $spine_uid_str = 'unit';
my @output_filename_ids = ();
my $id_count = 0;
foreach my $output_filename (@epub_output_filenames) {
$id_count++;
my $properties_str = '';
if ($self->get_conf('INFO_JS_DIR')) {
$properties_str = ' properties="scripted"'
}
print $opf_fh "
- \n";
}
my $js_weblabels_id;
if ($self->get_conf('JS_WEBLABELS_FILE')) {
my $js_weblabels_file_name = $self->get_conf('JS_WEBLABELS_FILE');
my $js_licenses_file_path = File::Spec->catfile($epub_document_destination_directory,
$js_weblabels_file_name);
if (-e $js_licenses_file_path) {
$js_weblabels_id = 'jsweblabels';
print $opf_fh "
- \n";
}
}
my $image_count = 0;
foreach my $image_file (sort keys(%epub_images)) {
$image_count++;
my $image_extension = $epub_images{$image_file};
my $image_mimetype;
if (defined($epub_images_extensions_mimetypes{$image_extension})) {
$image_mimetype = $epub_images_extensions_mimetypes{$image_extension};
} else {
my $extension = $image_extension;
$extension =~ s/^\.//;
$image_mimetype = $extension . '/image';
}
print $opf_fh "
- \n";
}
if (defined($epub_info_js_dir_name)) {
my $info_js_destination_dir
= File::Spec->catdir($epub_destination_directory,
$epub_document_dir_name, $epub_info_js_dir_name);
my $opendir_success = opendir(JSPATH, $info_js_destination_dir);
if (not $opendir_success) {
$self->document_error($self,
sprintf(__("epub3.pm: readdir %s error: %s"),
$info_js_destination_dir, $!));
} else {
my $js_count = 0;
foreach my $filename (sort(readdir(JSPATH))) {
my ($parsed_filename, $parsed_directory, $suffix)
= fileparse($filename, keys(%epub_js_extensions_mimetypes));
if (defined($suffix) and $suffix ne '') {
$js_count++;
my $js_mimetype = $epub_js_extensions_mimetypes{$suffix};
print $opf_fh "
- \n";
}
}
closedir(JSPATH);
}
}
print $opf_fh <
EOT
$id_count = 0;
foreach my $output_filename (@epub_output_filenames) {
$id_count++;
print $opf_fh " \n";
}
# Depending on the reader, the js_labels file should better be in the or
# not. The standard allows both showing the linear="no" elements as part
# of the default reading order or not. It is probably better for the
# js_labels to be in the spine if they can be viewed in any way.
# Foliate does not show the js_labels file upon clicking if not in
# the .
# Calibre shows the js_labels file upon clicking if not in the , and
# steps in the js_labels file if in the spine.
if (defined($js_weblabels_id)) {
print $opf_fh " \n";
}
print $opf_fh <
EOT
Texinfo::Common::output_files_register_closed(
$self->output_files_information(), $encoded_opf_file_path_name);
if (!close ($opf_fh)) {
$self->document_error($self,
sprintf(__("epub3.pm: error on closing %s: %s"),
$opf_file_path_name, $!));
return 1;
}
if ($self->get_conf('EPUB_CREATE_CONTAINER_FILE')) {
# this is needed if there are non ascii file names, otherwise, for instance
# with calibre the files cannot be read, one get
# "There is no item named 'EPUB/osé.opf' in the archive"
# even though unzip -l lists the file well. More testing is probably
# needed on other plaforms.
local $Archive::Zip::UNICODE = 1;
my $zip = Archive::Zip->new();
# the standard says that the mimetype file should not be compressed
# The mimetype file MUST NOT be compressed or encrypted
my $mimetype_added
= $zip->addFile($encoded_mimetype_file_path_name, $mimetype_filename,
Archive::Zip->COMPRESSION_LEVEL_NONE);
if (not(defined($mimetype_added))) {
$self->document_error($self,
sprintf(__("epub3.pm: error adding %s to archive"),
$mimetype_file_path_name));
return 1;
}
my $meta_inf_directory_ret_code
= $zip->addTree($encoded_meta_inf_directory, $meta_inf_directory_name);
if ($meta_inf_directory_ret_code != Archive::Zip->AZ_OK) {
$self->document_error($self,
sprintf(__("epub3.pm: error adding %s to archive"),
$meta_inf_directory));
return 1;
}
my $epub_document_dir_path = File::Spec->catdir($epub_destination_directory,
$epub_document_dir_name);
my ($encoded_epub_document_dir_path, $epub_document_dir_path_encoding)
= $self->encoded_output_file_name($epub_document_dir_path);
my $epub_document_dir_name_ret_code
= $zip->addTree($encoded_epub_document_dir_path, $epub_document_dir_name);
if ($epub_document_dir_name_ret_code != Archive::Zip->AZ_OK) {
$self->document_error($self,
sprintf(__("epub3.pm: error adding %s to archive"),
$epub_document_dir_path));
return 1;
}
my ($encoded_epub_outfile, $epub_outfile_encoding)
= $self->encoded_output_file_name($epub_outfile);
unless ($zip->writeToFileNamed($encoded_epub_outfile) == Archive::Zip->AZ_OK) {
$self->document_error($self,
sprintf(__("epub3.pm: error writing archive %s"),
$epub_outfile));
return 1;
}
}
if (not $self->get_conf('EPUB_KEEP_CONTAINER_FOLDER')) {
my $status = _epub_remove_container_folder($self,
$encoded_epub_destination_directory);
return $status if ($status);
}
return 0;
}
texinfo_register_handler('finish', \&epub_finish);
1;