From: <mi...@us...> - 2024-09-04 19:33:33
|
Revision: 9920 http://sourceforge.net/p/docutils/code/9920 Author: milde Date: 2024-09-04 19:33:30 +0000 (Wed, 04 Sep 2024) Log Message: ----------- Refactor `HTMLTranslator.image_size()`. Use `nodes.parse_measure()` instead of ad-hoc parsing when scaling size values. Split "spaghetti code" into auxiliary method `read_size_with_PIL()`. Modified Paths: -------------- trunk/docutils/docutils/writers/_html_base.py trunk/docutils/test/test_writers/test_html5_polyglot.py trunk/docutils/test/test_writers/test_html5_polyglot_misc.py Modified: trunk/docutils/docutils/writers/_html_base.py =================================================================== --- trunk/docutils/docutils/writers/_html_base.py 2024-09-04 19:33:12 UTC (rev 9919) +++ trunk/docutils/docutils/writers/_html_base.py 2024-09-04 19:33:30 UTC (rev 9920) @@ -14,18 +14,23 @@ # # .. _2-Clause BSD license: https://opensource.org/licenses/BSD-2-Clause -"""common definitions for Docutils HTML writers""" +"""Common definitions for Docutils HTML writers.""" +from __future__ import annotations + +__docformat__ = 'reStructuredText' + import base64 import mimetypes import os import os.path -from pathlib import Path import re import urllib.parse import urllib.request import warnings import xml.etree.ElementTree as ET # TODO: lazy import in prepare_svg()? +from pathlib import Path +from typing import TYPE_CHECKING import docutils from docutils import frontend, languages, nodes, utils, writers @@ -36,6 +41,10 @@ unichar2tex, wrap_math_code, MathError) +if TYPE_CHECKING: + from numbers import Real + + class Writer(writers.Writer): supported = ('html', 'xhtml') # update in subclass @@ -404,58 +413,67 @@ text = str(text) return text.translate(self.special_characters) - def image_size(self, node): - # Determine the image size from the node arguments or the image file. - # Return a size declaration suitable as "style" argument value, - # e.g., ``'width: 4px; height: 2em;'``. - # TODO: consider feature-request #102? - size = [node.get('width', None), node.get('height', None)] - if 'scale' in node: - if 'width' not in node or 'height' not in node: - # try reading size from image file - reading_problems = [] - uri = node['uri'] - if not PIL: - reading_problems.append('Requires Python Imaging Library.') - if mimetypes.guess_type(uri)[0] in self.videotypes: - reading_problems.append('PIL cannot read video images.') - if not self.settings.file_insertion_enabled: - reading_problems.append('Reading external files disabled.') - if not reading_problems: - try: - imagepath = self.uri2imagepath(uri) - with PIL.Image.open(imagepath) as img: - imgsize = img.size - except (ValueError, OSError, UnicodeEncodeError) as err: - reading_problems.append(str(err)) - else: - self.settings.record_dependencies.add( - imagepath.replace('\\', '/')) - if reading_problems: - msg = ['Cannot scale image!', - f'Could not get size from "{uri}":', - *reading_problems] - self.messages.append(self.document.reporter.warning( - '\n '.join(msg), base_node=node)) - else: - for i in range(2): - size[i] = size[i] or '%dpx' % imgsize[i] - # scale provided/determined size values: - factor = float(node['scale']) / 100 - for i in range(2): - if size[i]: - match = re.match(r'([0-9.]+)(\S*)$', size[i]) - size[i] = '%s%s' % (factor * float(match.group(1)), - match.group(2)) - size_declarations = [] - for i, dimension in enumerate(('width', 'height')): - if size[i]: - # Interpret unitless values as pixels: - if re.match(r'^[0-9.]+$', size[i]): - size[i] += 'px' - size_declarations.append(f'{dimension}: {size[i]};') - return ' '.join(size_declarations) + def image_size(self, node: nodes.image) -> str: + """Determine the image size from node arguments or the image file. + Auxiliary method called from `self.visit_image()`. + + Provisional. + """ + # TODO: Use "width" and "hight" for unitless integers? + # [feature-requests:#102] + + # List with optional width and height measures ((value, unit)-tuples) + measures: list[tuple[Real, str] | None] = [None, None] + dimensions = ('width', 'height') + for i, dimension in enumerate(dimensions): + if dimension in node: + measures[i] = nodes.parse_measure(node[dimension]) + if None in measures and 'scale' in node: + # supplement with (unitless) values read from image file + imgsize = self.read_size_with_PIL(node) + if imgsize: + measures = [measure or (imgvalue, '') + for measure, imgvalue in zip(measures, imgsize)] + # scale values + factor = node.get('scale', 100) / 100 # scaling factor + if factor != 1: + measures = [(measure[0] * factor, measure[1]) + for measure in measures if measure] + # format as CSS declarations and return + return ' '.join(f'{dimension}: {measure[0]:g}{measure[1] or "px"};' + for dimension, measure in zip(dimensions, measures) + if measure) + + def read_size_with_PIL(self, node) -> tuple[int, int] | None: + # Try reading size from image file. + # Internal auxiliary method called from `self.image_size()`. + reading_problems = [] + uri = node['uri'] + if not PIL: + reading_problems.append('Requires Python Imaging Library.') + if mimetypes.guess_type(uri)[0] in self.videotypes: + reading_problems.append('PIL cannot read video images.') + if not self.settings.file_insertion_enabled: + reading_problems.append('Reading external files disabled.') + if not reading_problems: + try: + imagepath = self.uri2imagepath(uri) + with PIL.Image.open(imagepath) as img: + imgsize = img.size + except (ValueError, OSError, UnicodeEncodeError) as err: + reading_problems.append(str(err)) + else: + self.settings.record_dependencies.add(imagepath) + if reading_problems: + msg = ['Cannot scale image!', + f'Could not get size from "{uri}":', + *reading_problems] + self.messages.append(self.document.reporter.warning( + '\n '.join(msg), base_node=node)) + return None + return imgsize + def prepare_svg(self, node, imagedata, size_declaration): # Edit `imagedata` for embedding as SVG image. # Use ElementTree to add node attributes. @@ -607,7 +625,7 @@ child['classes'].append(class_) def uri2imagepath(self, uri): - """Get filesystem path corresponding to an URI. + """Get POSIX filesystem path corresponding to an URI. The image directive expects an image URI__. Some writers require the corresponding image path to read the image size from the file or to Modified: trunk/docutils/test/test_writers/test_html5_polyglot.py =================================================================== --- trunk/docutils/test/test_writers/test_html5_polyglot.py 2024-09-04 19:33:12 UTC (rev 9919) +++ trunk/docutils/test/test_writers/test_html5_polyglot.py 2024-09-04 19:33:30 UTC (rev 9920) @@ -46,7 +46,7 @@ if (tuple(int(i) for i in PIL.__version__.split('.')) >= (10, 3)): DUMMY_PNG_NOT_FOUND = ("[Errno 2] No such file or directory: '%s'" % Path('dummy.png').resolve()) - SCALING_OUTPUT = 'style="width: 32.0px; height: 32.0px;" ' + SCALING_OUTPUT = 'style="width: 32px; height: 32px;" ' NO_PIL_SYSTEM_MESSAGE = '' else: REQUIRES_PIL = '\n Requires Python Imaging Library.' Modified: trunk/docutils/test/test_writers/test_html5_polyglot_misc.py =================================================================== --- trunk/docutils/test/test_writers/test_html5_polyglot_misc.py 2024-09-04 19:33:12 UTC (rev 9919) +++ trunk/docutils/test/test_writers/test_html5_polyglot_misc.py 2024-09-04 19:33:30 UTC (rev 9920) @@ -18,8 +18,8 @@ # so we import the local `docutils` package. sys.path.insert(0, str(Path(__file__).resolve().parents[2])) -from docutils import core -from docutils.writers import html5_polyglot +from docutils import core, frontend, nodes, utils +from docutils.writers import html5_polyglot, _html_base # TEST_ROOT is ./test/ from the docutils root TEST_ROOT = Path(__file__).parents[1] @@ -241,5 +241,21 @@ self.assertNotIn('MathJax', head) +class ImagesTestCase(unittest.TestCase): + """Test image handling routines.""" + + settings = frontend.get_default_settings(_html_base.Writer) + document = utils.new_document('test data', settings) + translator = _html_base.HTMLTranslator(document) + + def test_image_size(self): + image = nodes.image(height='3', width='4em') + self.assertEqual(self.translator.image_size(image), + 'width: 4em; height: 3px;') + image = nodes.image(height='3', width='4em', scale=50) + self.assertEqual(self.translator.image_size(image), + 'width: 2em; height: 1.5px;') + + if __name__ == '__main__': unittest.main() This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |