SF.net SVN: docutils:[9920] trunk/docutils

milde--- via Docutils-checkins <[email protected]>
Newsgroups gmane.text.docutils.cvs
Message-ID <[email protected]>
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.
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.