plone / plonetheme.barceloneta

The default theme for Plone Classic UI
https://pypi.python.org/pypi/plonetheme.barceloneta
19 stars 41 forks source link

add inline svg transform #201

Closed iham closed 3 years ago

iham commented 4 years ago

it would be nice to see img-tags, that src refers to a .svg file, to be replaced by the svg's content. plus add the alt and title as meta-data to the svg

my approach was to write an adapter: configure.zcml

  <adapter
      name="plone.inline-svg"
      for="zope.interface.Interface <a theme/addon Layer>"
      factory=".transform.InlineSVGTransform"
      />

and the transform itself: transform.py

# -*- coding: utf-8 -*-
"""Transforming svg images to inline."""
from lxml import etree
from lxml.etree import Element
from lxml.html import fromstring
from repoze.xmliter.serializer import XMLSerializer

from zope.component import getUtility
from zope.interface import implementer

from plone import api
from plone.transformchain.interfaces import ITransform
from plone.uuid.interfaces import IUUIDGenerator

@implementer(ITransform)
class InlineSVGTransform(object):
    """Transforming SVG images to inline svg &
        adding attributes given from the image.
        See http://web-accessibility.carnegiemuseums.org/code/svg/ for details.
    """

    # right before diazo
    order = 8840
    xpath_expr = '//body[not(contains(@class, "zmi"))]//img[not(contains(@src, "++")) and substring(@src,string-length(@src) -string-length(".svg") +1) = ".svg"]'  # noqa E501

    def __init__(self, published, request):
        self.published = published
        self.request = request

    def transformBytes(self, result, encoding):  # noqa N802
        return self.transformIterable([result], encoding)

    def transformUnicode(self, result, encoding):  # noqa N802
        return self.transformIterable([result], encoding)

    def transformIterable(self, result, encoding):  # noqa N802
        content_type = self.request.response.getHeader('Content-Type')
        if content_type and content_type.startswith('text/html') and result:
            tree = None
            if isinstance(result, XMLSerializer):
                tree = result.tree.getroot()
            else:
                if result[0]:
                    tree = fromstring(unicode(result[0].decode('utf-8')))

            if tree is not None:
                portal = api.portal.get()
                changed = False
                for node in tree.xpath(self.xpath_expr):
                    src = node.attrib['src']
                    if len(src) > 0:
                        rel_path = src.replace(
                            self.request.SERVER_URL,
                            '',
                        ).split('/@@images/')[0]

                        img = portal.restrictedTraverse(rel_path)
                        if img:
                            svg = fromstring(
                                unicode(img.image.data.decode('utf-8')),
                            )
                            self._attributes_manipulation(img, node, svg)
                            node.getparent().replace(node, svg)
                            changed = True
                if changed:
                    if not isinstance(result, XMLSerializer):
                        result[0] = etree.tostring(tree, encoding=encoding)

        return result

    def _attributes_manipulation(self, img, node, svg):
        """setting class, title, alt."""
        # classes
        classes = []
        classes.append(node.attrib['class'])
        classes.append(svg.attrib.get('class', ''))
        svg.set('class', ' '.join(classes))

        # alt
        desc = Element('desc')
        desc.text = node.attrib['alt']
        desc.set('id', 'desc')
        svg.insert(0, desc)

        # title
        title = Element('title')
        title.text = node.attrib['title']
        title.set('id', 'title')
        svg.insert(0, title)

        # inline svg id's still must be unique including the svg id itself
        self._unify_ids(svg.getparent())

        # set aria attributes
        svg.set('role', 'img')
        # and point to the corrected title and desc id's
        svg.set(
            'aria-labelledby',
            '{title} {desc}'.format(
                title=svg.xpath('title/@id')[0],
                desc=svg.xpath('desc/@id')[0],
            ),
        )

    def _unify_ids(self, svg):
        """As the ids of inline svgs are not allowed to occur multiple times,
           we need to unify them."""
        uid = getUtility(IUUIDGenerator)()
        for node in svg.xpath('//*[@id and string-length(@id)!=0]'):
            node.set(
                'id',
                '{id}-{uid}'.format(
                    id=node.attrib['id'],
                    uid=uid,
                ),
            )

feel free to use this approach.

agitator commented 3 years ago

@iham obsolete with icon resolver in plone 6?

iham commented 3 years ago

@agitator not sure, didn't take a look at plone 6 and how it deals with inlining svg images

agitator commented 3 years ago

Functionality in https://github.com/plone/Products.CMFPlone/blob/master/Products/CMFPlone/browser/icons.py

Usage <tal:icon tal:replace="structure python:icons.tag('archive', tag_class='custom-class', tag_alt='foobar')" />

iham commented 3 years ago

@agitator sort of ... i guess.

this looks like a manual method you could call on an object inside a template or something else.

my approach was to replace img tags with ref to a svg file into svg-markup.

by calling that icons-view via a transform, maybe the same goal is achieved.

iham commented 3 years ago

Moved issue to cmfplone