diff --git a/src/calibre/ebooks/conversion/cli.py b/src/calibre/ebooks/conversion/cli.py index 3a9d18ed60..49ddffde15 100644 --- a/src/calibre/ebooks/conversion/cli.py +++ b/src/calibre/ebooks/conversion/cli.py @@ -239,7 +239,7 @@ def add_pipeline_options(parser, plumber): 'chapter', 'chapter_mark', 'prefer_metadata_cover', 'remove_first_image', 'insert_metadata', 'page_breaks_before', - 'remove_fake_margins', 'start_reading_at', + 'remove_fake_margins', 'start_reading_at', 'add_alt_text_to_img', ] )), diff --git a/src/calibre/ebooks/conversion/config.py b/src/calibre/ebooks/conversion/config.py index 0fff29dea5..91f46b984f 100644 --- a/src/calibre/ebooks/conversion/config.py +++ b/src/calibre/ebooks/conversion/config.py @@ -260,7 +260,7 @@ OPTIONS = { 'structure_detection': ( 'chapter', 'chapter_mark', 'start_reading_at', 'remove_first_image', 'remove_fake_margins', 'insert_metadata', - 'page_breaks_before'), + 'page_breaks_before', 'add_alt_text_to_img',), 'toc': ( 'level1_toc', 'level2_toc', 'level3_toc', diff --git a/src/calibre/ebooks/conversion/plumber.py b/src/calibre/ebooks/conversion/plumber.py index adbfbb540a..47c4f50e64 100644 --- a/src/calibre/ebooks/conversion/plumber.py +++ b/src/calibre/ebooks/conversion/plumber.py @@ -420,6 +420,11 @@ OptionRecommendation(name='remove_fake_margins', 'case you can disable the removal.') ), +OptionRecommendation(name='add_alt_text_to_img', + recommended_value=False, level=OptionRecommendation.LOW, + help=_('When an tag has no alt attribute, check the associated image file for metadata that specifies alternate text, and' + ' use it to fill in the alt attribute. The alt attribute is used by screen readers for assisting the visually challenged.') +), OptionRecommendation(name='margin_top', recommended_value=5.0, level=OptionRecommendation.LOW, @@ -1203,6 +1208,12 @@ OptionRecommendation(name='search_replace', from calibre.ebooks.oeb.transforms.jacket import Jacket Jacket()(self.oeb, self.opts, self.user_metadata) + pr(0.37) + self.flush() + + if self.opts.add_alt_text_to_img: + from calibre.ebooks.oeb.transforms.alt_text import AddAltText + AddAltText()(self.oeb, self.opts) pr(0.4) self.flush() diff --git a/src/calibre/ebooks/oeb/transforms/alt_text.py b/src/calibre/ebooks/oeb/transforms/alt_text.py new file mode 100644 index 0000000000..383628790c --- /dev/null +++ b/src/calibre/ebooks/oeb/transforms/alt_text.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python +# License: GPLv3 Copyright: 2024, Kovid Goyal + + +from io import BytesIO + +from PIL import Image + +from calibre.ebooks.oeb.base import SVG_MIME, urlnormalize, xpath +from calibre.utils.img import read_alt_text + + +def process_spine_item(item, hrefs, log): + html = item.data + for elem in xpath(html, '//h:img[@src]'): + src = urlnormalize(elem.attrib['src']) + image = hrefs.get(item.abshref(src), None) + if image and image.media_type != SVG_MIME and not elem.attrib.get('alt'): + data = image.bytes_representation + try: + with Image.open(BytesIO(data)) as im: + alt = read_alt_text(im) + except Exception as err: + log.warn(f'Failed to read alt text from image {src} with error: {err}') + else: + if alt: + elem.set('alt', alt) + + +class AddAltText: + + def __call__(self, oeb, opts): + oeb.logger.info('Add alt text to images...') + hrefs = oeb.manifest.hrefs + for item in oeb.spine: + process_spine_item(item, hrefs, oeb.log) diff --git a/src/calibre/gui2/convert/structure_detection.ui b/src/calibre/gui2/convert/structure_detection.ui index 9b99fd93a9..9d68182f90 100644 --- a/src/calibre/gui2/convert/structure_detection.ui +++ b/src/calibre/gui2/convert/structure_detection.ui @@ -14,39 +14,9 @@ Form - - - - Remove &fake margins - - - - - - - The header and footer removal options have been replaced by the Search & replace options. Click the Search & replace category in the bar to the left to use these options. Leave the replace field blank and enter your header/footer removal regexps into the search field. - - - true - - - - + - - - - Insert &metadata as page at start of book - - - - - - - - - @@ -57,12 +27,8 @@ - - - - 20 - - + + @@ -77,14 +43,24 @@ - - - - Remove first &image + + + + 20 - + + + + The header and footer removal options have been replaced by the Search & replace options. Click the Search & replace category in the bar to the left to use these options. Leave the replace field blank and enter your header/footer removal regexps into the search field. + + + true + + + + Qt::Vertical @@ -97,6 +73,37 @@ + + + + Insert &metadata as page at start of book + + + + + + + + + + Remove &fake margins + + + + + + + Remove first &image + + + + + + + Add &alt text to images + + + diff --git a/src/pyj/book_list/conversion_widgets.pyj b/src/pyj/book_list/conversion_widgets.pyj index b404a13cc0..63f915a614 100644 --- a/src/pyj/book_list/conversion_widgets.pyj +++ b/src/pyj/book_list/conversion_widgets.pyj @@ -360,6 +360,7 @@ def structure_detection(container): g.appendChild(choices('chapter_mark', _('Chap&ter mark:'), ['pagebreak', 'rule', 'both', 'none'])) g.appendChild(checkbox('remove_first_image', _('Remove first &image'))) g.appendChild(checkbox('remove_fake_margins', _('Remove &fake margins'))) + g.appendChild(checkbox('add_alt_text_to_img', _('Add &alt text to images'))) g.appendChild(checkbox('insert_metadata', _('Insert metadata at start of book'))) g.appendChild(lineedit('page_breaks_before', _('Insert page breaks before'), 50)) g.appendChild(lineedit('start_reading_at', _('Start reading at'), 50))