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))