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)
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)
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)
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)
def test_util_convertlen_invalid(self): self.assertIsNone(convert_length(None, None))
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']