diff --git a/CHANGES b/CHANGES index 3250ea2..c9e7feb 100644 --- a/CHANGES +++ b/CHANGES @@ -1,5 +1,10 @@ -0.14.3 August xx, 1996 +0.14.3 August ??, 2026 +- handling of inline SVG has been improved. + - the inline SVG element is now handled whether or not it has set the svg namespace. + - inline SVG is changed to standalone files in EPUB2 + - inline html5 SVG now adapted for EPUB3, which cares about namespaces +- an error is no longer emitted when rst output is requested from html input - refactored boilerplate stripping for txt files so it only needs to be done once per book. boilerplate is now detected in the text Parser, instead of in the text Writer. - `a` tags can't contain block tags in xhtml, so div in `a` and `p` in `a` cause EPUB2 validation errors, so Ebookmaker now changes `p` and `div` occurring in `a` to `span` for EPUB2. I'm not sure why anyone would want to use this markup in a book. Fixes #333 diff --git a/src/ebookmaker/HTMLChunker.py b/src/ebookmaker/HTMLChunker.py index 61c3b0d..043651a 100644 --- a/src/ebookmaker/HTMLChunker.py +++ b/src/ebookmaker/HTMLChunker.py @@ -165,6 +165,9 @@ def shipout_chunk(self, attribs, chunk_id = None, comment = None): for e in xpath(self.chunk, '//mathml:math'): attribs.rel.add('mathml') break + for e in xpath(self.chunk, '//xhtml:svg'): + attribs.rel.add('svg') + break for e in xpath(self.chunk, '//svg:svg'): attribs.rel.add('svg') break diff --git a/src/ebookmaker/parsers/ImageParser.py b/src/ebookmaker/parsers/ImageParser.py index 901a80a..b59200f 100644 --- a/src/ebookmaker/parsers/ImageParser.py +++ b/src/ebookmaker/parsers/ImageParser.py @@ -20,6 +20,7 @@ from PIL import Image, ImageFile from lxml import etree +from libgutenberg.GutenbergGlobals import NS from libgutenberg.Logger import debug, critical, error from libgutenberg.MediaTypes import mediatypes as mt from ebookmaker.parsers import ParserBase @@ -171,6 +172,7 @@ def serialize(self): atts_to_remove = ['data-variant', 'focusable', 'role'] try: tree = etree.parse(io.BytesIO(self.image_data)) + svg = tree.getroot() except etree.XMLSyntaxError as e: critical(f'SVG image {self.attribs.url} was badly formed XML: {e}') return self.image_data @@ -181,5 +183,11 @@ def serialize(self): for att in copy.copy(element.attrib): if att.startswith('aria-'): del element.attrib[att] + # strip namespaces hanging around, perhaps because it's an extracted image + element.tag = etree.QName(element).localname + etree.cleanup_namespaces(tree) + # restore the root namespace + svg.tag = NS.svg.svg + self.image_data = etree.tostring(tree, encoding="utf-8") return self.image_data diff --git a/src/ebookmaker/writers/Epub3Writer.py b/src/ebookmaker/writers/Epub3Writer.py index 04332a5..3a6b989 100644 --- a/src/ebookmaker/writers/Epub3Writer.py +++ b/src/ebookmaker/writers/Epub3Writer.py @@ -560,6 +560,10 @@ def html_for_epub3(xhtml): # add namespace to math elements for e in xpath(xhtml, "//xhtml:math"): e.attrib['xmlns'] = "http://www.w3.org/1998/Math/MathML" + # add namespace to svg + for e in xpath(xhtml, "//xhtml:svg"): + e.attrib['xmlns'] = "http://www.w3.org/2000/svg" + @staticmethod def fix_incompatible_css(sheet): diff --git a/src/ebookmaker/writers/EpubWriter.py b/src/ebookmaker/writers/EpubWriter.py index 6043855..78bfb3f 100644 --- a/src/ebookmaker/writers/EpubWriter.py +++ b/src/ebookmaker/writers/EpubWriter.py @@ -1125,11 +1125,40 @@ def fix_html5(xhtml): tag.clear() tag.text = text + @staticmethod + def extract_svg(xhtml, parent_url): + """ + convert embedded svg elements to stand-alone svg files that work better n EPUB2. - - - - + """ + svgnum = 0 + new_parsers = [] + for svg in xpath(xhtml, '//xhtml:svg'): + # make a new parser + attribs = parsers.ParserAttributes() + svg_parser = parsers.ImageParser.Parser(attribs=attribs) + attribs.mediatype = mt.svg + attribs.id = f'extracted_svg_{svgnum}' + attribs.url = urllib.parse.urljoin(parent_url, f'images/extracted_svg_{svgnum}.svg') + svg_parser.image_data = etree.tostring(svg) + + # turn the svg element into an img element + for att in svg.attrib: + if att not in {"alt", "class", "dir", "height", "id", "ismap", "lang", "longdesc", + "src", "title", "usemap", "width", "xml:lang"}: + del svg.attrib[att] + for title in xpath(svg, '//xhtml:title'): + svg.attrib['alt'] = title.text_content() or '' + break + svg.tag = NS.xhtml.img + svg.attrib['src'] = attribs.url + svg.attrib['id'] = attribs.id + for child in list(svg): + svg.remove(child) + + # return the new parsers + new_parsers.append(svg_parser) + return new_parsers @staticmethod def strip_links(xhtml, manifest): @@ -1352,6 +1381,7 @@ def shipout(self, job, parserlist, ncx): if p.mediatype() == mt.xhtml: opf.spine_item_from_parser(p) else: + debug(f'adding {p.attribs.url} to manifest') opf.manifest_item_from_parser(p) except Exception as what: error("Could not process file %s: %s" % (p.attribs.url, what)) @@ -1457,8 +1487,14 @@ def build(self, job): p.remap_links(idmap) xhtml.make_links_absolute(base_url=p.attribs.url) - self.fix_html5(xhtml) + new_parsers = self.extract_svg(xhtml, p.attribs.url) + # add the new parsers to parser list so the get handled properly + parserlist.extend(new_parsers) + job.spider.parsers.extend(new_parsers) + + self.fix_html5(xhtml) + strip_classes = self.get_classes_with_prop(xhtml) strip_classes = strip_classes.intersection(STRIP_CLASSES) if strip_classes: diff --git a/src/ebookmaker/writers/RSTWriter.py b/src/ebookmaker/writers/RSTWriter.py index 45a0c04..e216fbc 100644 --- a/src/ebookmaker/writers/RSTWriter.py +++ b/src/ebookmaker/writers/RSTWriter.py @@ -31,6 +31,11 @@ def build (self, job): debug ("Creating RST file: %s" % filename) parser = ParserFactory.ParserFactory.create (job.url) + + has_txt_source = 'text/plain' in str(parser.attribs.orig_mediatype) + if not has_txt_source: + debug("needs plain text file for conversion: %s from %s", filename, job.url) + return data = parser.preprocess ('utf-8').encode ('utf-8')