Beispiel #1
0
    def visit_image(self, node):
        if 'uri' not in node or not node['uri']:
            self.verbose('skipping image with no uri')
            raise nodes.SkipNode

        uri = node['uri']
        uri = self.encode(uri)

        dochost = None
        img_key = None
        img_sz = None
        internal_img = uri.find('://') == -1 and not uri.startswith('data:')
        is_svg = uri.startswith('data:image/svg+xml') or \
            guess_mimetype(uri) == 'image/svg+xml'

        if internal_img:
            asset_docname = None
            if 'single' in self.builder.name:
                asset_docname = self.docname

            img_key, dochost, img_path = \
                self.assets.fetch(node, docname=asset_docname)

            # if this image has not already be processed (injected at a later
            # stage in the sphinx process); try processing it now
            if not img_key:
                # if this is an svg image, additional processing may also needed
                if is_svg:
                    confluence_supported_svg(self.builder, node)

                if not asset_docname:
                    asset_docname = self.docname

                img_key, dochost, img_path = \
                    self.assets.process_image_node(
                        node, asset_docname, standalone=True)

            if not img_key:
                self.warn('unable to find image: ' + uri)
                raise nodes.SkipNode

        # extract height, width and scale values on this image
        height, hu = extract_length(node.get('height'))
        scale = node.get('scale')
        width, wu = extract_length(node.get('width'))

        # if a scale value is provided and a height/width is not set, attempt to
        # determine the size of the image so that we can apply a scale value on
        # the detected size values
        if scale and not height and not width:
            if internal_img:
                img_sz = get_image_size(img_path)
                if img_sz is None:
                    self.warn('could not obtain image size; :scale: option is '
                        'ignored for ' + img_path)
                else:
                    width = img_sz[0]
                    wu = 'px'
            else:
                self.warn('cannot not obtain image size for external image; '
                    ':scale: option is ignored for ' + node['uri'])

        # apply scale factor to height/width fields
        if scale:
            if height:
                height = int(round(float(height) * scale / 100))
            if width:
                width = int(round(float(width) * scale / 100))

        # confluence only supports pixel sizes and percentage sizes in select
        # cases (e.g. applying a percentage width for an attached image can
        # result in an macro render error) -- adjust any other unit type (if
        # possible) to an acceptable pixel/percentage length
        if height:
            height = convert_length(height, hu)
            if height is None:
                self.warn('unsupported unit type for confluence: ' + hu)
        if width:
            width = convert_length(width, wu)
            if width is None:
                self.warn('unsupported unit type for confluence: ' + wu)

        # disable height/width entries for attached svgs as using these
        # attributes can result in a "broken image" rendering; instead, we will
        # track any desired height/width entry and inject them when publishing
        if internal_img and is_svg and (height or width):
            height = None
            hu = None
            width = None
            wu = None

        # [sphinx-gallery] create "thumbnail" images for sphinx-gallery
        #
        # If a sphinx-gallery-specific class type is detected for an image,
        # assume there is a desire for thumbnail-like images. Images are then
        # restricted with a specific height (a pattern observed when restricting
        # images to a smaller size with a Confluence editor). Although, if the
        # detected image size is smaller than our target, ignore any forced size
        # changes.
        if height is None and width is None and internal_img and not is_svg:
            if 'sphx-glr-multi-img' in node.get('class', []):
                if not img_sz:
                    img_sz = get_image_size(img_path)

                if not img_sz or img_sz[1] > 250:
                    height = '250'
                    hu = 'px'

        # forward image options
        opts = {}
        opts['dochost'] = dochost
        opts['height'] = height
        opts['hu'] = hu
        opts['key'] = img_key
        opts['width'] = width
        opts['wu'] = wu

        self._visit_image(node, opts)
Beispiel #2
0
 def test_util_convertlen_units(self):
     self.assertEqual(convert_length(321, 'px'), 321)
     self.assertEqual(convert_length('654', 'px'), 654)
     self.assertEqual(convert_length('987.6', 'px'), 988)
     self.assertEqual(convert_length('987.3', 'px'), 987)
     self.assertEqual(convert_length(1, 'in'), 96)
     self.assertEqual(convert_length(1, 'pc'), 16)
     self.assertGreater(convert_length(100, 'em'), 100)
     self.assertGreater(convert_length(100, 'ex'), 100)
     self.assertGreater(convert_length(100, 'mm'), 100)
     self.assertGreater(convert_length(100, 'cm'), 100)
     self.assertGreater(convert_length(100, 'pt'), 100)
Beispiel #3
0
 def test_util_convertlen_unitless(self):
     self.assertEqual(convert_length(123, None), 123)
     self.assertEqual(convert_length(456.2, None), 456)
     self.assertEqual(convert_length('789.7', None), 790)
Beispiel #4
0
 def test_util_convertlen_percentages(self):
     self.assertEqual(convert_length(852, '%'), 852)
     self.assertEqual(convert_length(369.9, '%'), 370)
     self.assertEqual(convert_length('741.3', '%'), 741)
Beispiel #5
0
 def test_util_convertlen_invalid(self):
     self.assertIsNone(convert_length(None, None))
Beispiel #6
0
def confluence_supported_svg(builder, node):
    """
    process an image node and ensure confluence-supported svg  (if applicable)

    SVGs have some limitations when being presented on a Confluence instance.
    The following have been observed issues:

    1) If an SVG file does not have an XML declaration, Confluence will fail to
       render an image.
    2) If an `ac:image` macro is applied custom width/height values on an SVG,
       Confluence Confluence will fail to render the image.

    This call will process a provided image node and ensure an SVG is in a ready
    state for publishing. If a node is not an SVG, this method will do nothing.

    To support custom width/height fields for an SVG image, the image file
    itself will be modified to an expected lengths. Any hints in the
    documentation using width/height or scale, the desired width and height
    fields of an image will calculated and replaced/injected into the SVG image.

    Any SVG files which do not have an XML declaration will have on injected.

    Args:
        builder: the builder
        node: the image node to check
    """

    uri = node['uri']

    # ignore external/embedded images
    if uri.find('://') != -1 or uri.startswith('data:'):
        return

    # invalid uri/path
    uri_abspath = find_env_abspath(builder.env, builder.outdir, uri)
    if not uri_abspath:
        return

    # ignore non-svgs
    mimetype = guess_mimetype(uri_abspath)
    if mimetype != 'image/svg+xml':
        return

    try:
        with open(uri_abspath, 'rb') as f:
            svg_data = f.read()
    except (IOError, OSError) as err:
        builder.warn('error reading svg: %s' % err)
        return

    modified = False
    svg_root = xml_et.fromstring(svg_data)

    # determine (if possible) the svgs desired width/height
    svg_height = None
    if 'height' in svg_root.attrib:
        svg_height = svg_root.attrib['height']

    svg_width = None
    if 'width' in svg_root.attrib:
        svg_width = svg_root.attrib['width']

    # try to fallback on the viewbox attribute
    viewbox = False
    if svg_height is None or svg_width is None:
        if 'viewBox' in svg_root.attrib:
            try:
                _, _, svg_width, svg_height = \
                    svg_root.attrib['viewBox'].split(' ')
                viewbox = True
            except ValueError:
                pass

    # if tracking an svg width/height, ensure the sizes are in pixels
    if svg_height:
        svg_height, svg_height_units = extract_length(svg_height)
        svg_height = convert_length(svg_height, svg_height_units, pct=False)
    if svg_width:
        svg_width, svg_width_units = extract_length(svg_width)
        svg_width = convert_length(svg_width, svg_width_units, pct=False)

    # extract length/scale properties from the node
    height, hu = extract_length(node.get('height'))
    scale = node.get('scale')
    width, wu = extract_length(node.get('width'))

    # if a percentage is detected, ignore these lengths when attempting to
    # perform any adjustments; percentage hints for internal images will be
    # managed with container tags in the translator
    if hu == '%':
        height = None
        hu = None

    if wu == '%':
        width = None
        wu = None

    # confluence can have difficulty rendering svgs with only a viewbox entry;
    # if a viewbox is used, use it for the height/width if these options have
    # not been explicitly configured on the directive
    if viewbox and not height and not width:
        height = svg_height
        width = svg_width

    # if only one size is set, fetch (and scale) the other
    if width and not height:
        if svg_height and svg_width:
            height = float(width) / svg_width * svg_height
        else:
            height = width
        hu = wu

    if height and not width:
        if svg_height and svg_width:
            width = float(height) / svg_height * svg_width
        else:
            width = height
        wu = hu

    # if a scale value is provided and a height/width is not set, attempt to
    # determine the size of the image so that we can apply a scale value on
    # the detected size values
    if scale:
        if not height and svg_height:
            height = svg_height
            hu = 'px'

        if not width and svg_width:
            width = svg_width
            wu = 'px'

    # apply scale factor to height/width fields
    if scale:
        if height:
            height = int(round(float(height) * scale / 100))
        if width:
            width = int(round(float(width) * scale / 100))

    # confluence only supports pixel sizes -- adjust any other unit type
    # (if possible) to a pixel length
    if height:
        height = convert_length(height, hu, pct=False)
        if height is None:
            builder.warn('unsupported svg unit type for confluence: ' + hu)
    if width:
        width = convert_length(width, wu, pct=False)
        if width is None:
            builder.warn('unsupported svg unit type for confluence: ' + wu)

    # if we have a height/width to apply, adjust the svg
    if height and width:
        svg_root.attrib['height'] = str(height)
        svg_root.attrib['width'] = str(width)
        svg_data = xml_et.tostring(svg_root)
        modified = True

    # ensure xml declaration exists
    if not svg_data.lstrip().startswith(b'<?xml'):
        svg_data = XML_DEC + b'\n' + svg_data
        modified = True

    # ignore svg file if not modifications are needed
    if not modified:
        return

    fname = sha256(svg_data).hexdigest() + '.svg'
    outfn = os.path.join(builder.outdir, builder.imagedir, 'svgs', fname)

    # write the new svg file (if needed)
    if not os.path.isfile(outfn):
        logger.verbose('generating compatible svg of: %s' % uri)
        logger.verbose('generating compatible svg to: %s' % outfn)

        ensuredir(os.path.dirname(outfn))
        try:
            with open(outfn, 'wb') as f:
                f.write(svg_data)
        except (IOError, OSError) as err:
            builder.warn('error writing svg: %s' % err)
            return

    # replace the required node attributes
    node['uri'] = outfn

    if 'height' in node:
        del node['height']
    if 'scale' in node:
        del node['scale']
    if 'width' in node:
        del node['width']