import gettext
import inkex
from inkex import bezier
from inkex.command import inkscape_command
from inkex.localization import inkex_gettext as _i
from lxml.builder import E
_ = gettext.translation('imagemap','locale',fallback=True).gettext
# (X)HTML stuff:
ESCAPE=str.maketrans({'&':'&','<':'<','>':'>'})
def quotedval(val):
quote="'" if val.count('"')>val.count("'") else '"'
return quote+val.translate(ESCAPE).translate({ord(quote):f'{ord(quote)}'})+quote
def htmlval(val):
if val=='': return ''
elif not any(i in val for i in ' \t\r\n\f"\'=`'):
return '='+val.translate(ESCAPE)
else:
return '='+quotedval(val)
AREA_ATTRS={
'href':lambda a:a.get('href',a.get('{http://www.w3.org/1999/xlink}href')),
'alt':lambda a:a.get('aria-label',a.get('{http://www.w3.org/1999/xlink}title',a.title)),
'target': lambda a:a.get('target','_blank' if a.get('{http://www.w3.org/1999/xlink}show')=='new' else None),
'download': lambda a:a.get('download'),
'ping': lambda a:a.get('ping'),
'rel': lambda a:a.get('rel'),
'referrerpolicy': lambda a:a.get('referrerpolicy')
}
SHAPE_MARKUP = {
'HTML': lambda attrs:
f"\n",
'XHTML': lambda attrs:
f"\n",
'mod_imagemap': lambda attrs:
f"{attrs['shape']} {attrs['href'] or 'nocontent'} {' '.join(f'{i[0]},{i[1]}' for i in attrs['coords'])}"
+(' "'+alt.translate({34:"''"})+'"' if alt is not None else '')+'\n' # no way to get quotation mark in text in httpd
} # if we ever implement `circ` we gotta handle it specially
CSS_LINK_INDEX='-computer-viatrix-inx-imagemap-linkindex'
# "rectifiable" as in "can be made into a rect"
def rectifiable(coords):
if len(coords)!=4: return False
else: return (coords[0][0]==coords[1][0] and coords[1][1]==coords[2][1] and coords[2][0]==coords[3][0] and coords[3][1]==coords[0][1] ) \
or (coords[0][1]==coords[1][1] and coords[1][0]==coords[2][0] and coords[2][1]==coords[3][1] and coords[0][0]==coords[3][0])
def rectify(coords):
return [[min(coords[0][0],coords[2][0]),min(coords[0][1],coords[2][1])],[max(coords[0][0],coords[2][0]),max(coords[0][1],coords[2][1])]]
class ImageMap(inkex.OutputExtension):
def add_arguments(self,pars):
pars.add_argument("--maptype")
def save(self,stream):
assert self.options.maptype in {"HTML","XHTML","mod_imagemap"}
shapemarkup=SHAPE_MARKUP[self.options.maptype]
viewBox=self.svg.get_viewbox()
wscale=self.svg.viewport_width/viewBox[2] if viewBox[2]!=0 else 1
hscale=self.svg.viewport_height/viewBox[3] if viewBox[3]!=0 else 1
# preprocess shapes for our purposes.
# after this, the shapes within the image must: look the same as before (barring colour/alpha), not be clones, have no stroke, not intersect, be visually unaffected by `fill-rule`, not be clipped, and not go out of bounds.
links=[]
rects=[]
svgIDs=[i.get_id() for i in self.svg.iterdescendants('{http://www.w3.org/2000/svg}svg')]
for a in self.svg.iterdescendants('{http://www.w3.org/2000/svg}a'):
# save link attributes because they get removed when flattening
link={attr:AREA_ATTRS[attr](a) for attr in AREA_ATTRS.keys()}
for el in a.iterdescendants(): # CSS is preserved when flattening (for paths)
if not isinstance(el,inkex.ShapeElement) or isinstance(el,inkex.Group): continue
style=el.effective_style()
style[CSS_LINK_INDEX]=f'" {CSS_LINK_INDEX}-{len(links)} "'
if el.tag=='{http://www.w3.org/2000/svg}image':
el.tag='{http://www.w3.org/2000/svg}rect' # because flattening an image creates a clip-path
style['stroke']='none'
style['fill']='#000'
links += [link]
# for clipping out-of-bounds elements
newid=self.svg.get_unique_id('intersect')
rect=E('{http://www.w3.org/2000/svg}rect',
x=str(viewBox[0]),
y=str(viewBox[1]),
width='100%',
height='100%',
style='fill:#000;stroke:none',
id=newid)
self.svg.append(rect)
rects+=[newid]
#clip-paths
clipped=[]
clippedpaths=set()
for clippedEl in self.svg.iterdescendants():
if not isinstance(clippedEl,inkex.BaseElement): continue
if clippedEl.cascaded_style().get('clip-path','none')=='none': continue
clipped.append([clippedEl.get_id(),0])
for el in clippedEl.descendants():
if not isinstance(el,inkex.ShapeElement) or isinstance(el,inkex.elements._groups.GroupBase):
clipped[-1][1]+=1 # can overshoot number of groups but works
continue
clippedpaths.add((el.get_id(),el.cascaded_style().get(CSS_LINK_INDEX)))
if len(links)==0:
raise inkex.AbortExtension(_("Image has no hyperlinks.\nAdd a hyperlink to an object with right-click → \"{}\".").format(_i("Create Anchor (Hyperlink)")))
command=\
''.join(f'select-clear;select-by-id:{i};selection-ungroup;' for i in reversed(svgIDs)) \
+''.join(f'select-clear;select-by-id:{i[0]};{"selection-ungroup;"*i[1]}' for i in reversed(clipped) if i[1]>0) \
+''.join(f'select-clear;select-by-id:{i[0]};object-release-clip;unselect-by-id:{i[0]};selection-set-backup;select-clear;select-by-id:{i[0]};object-stroke-to-path;selection-ungroup;path-union;object-set-attribute:id,{i[0]};selection-restore-backup;select-by-id:{i[0]};path-intersection;object-set-attribute:style,{CSS_LINK_INDEX}:{i[1]};' for i in clippedpaths) \
+''.join(f'select-clear;select-by-selector:[style~="{CSS_LINK_INDEX}-{i}"];object-stroke-to-path;selection-ungroup;path-union;select-by-id:{rects[i]};path-intersection;object-set-attribute:style,{CSS_LINK_INDEX}:" {CSS_LINK_INDEX}-{i} ";' for i in range(len(links))) \
+'select-all;path-flatten;path-split'
# (we re-set the existing style attribute in case it got unset on non-paths)
from lxml import etree
newbytes=inkscape_command(self.svg,actions=command)
self.svg=self.load(newbytes).getroot()
# preprocessing done, now for map generation
shapes=[[] for i in range(len(links))]
for el in self.svg.iterdescendants():
if not isinstance(el,inkex.ShapeElement): continue
linkindex=el.cascaded_style().get(CSS_LINK_INDEX)
if linkindex is None: continue
linkindex=int(linkindex[len(CSS_LINK_INDEX)+3:-2])
link=links[linkindex]
href=link['href']
path=el.get_path().transform(el.composed_transform()).to_superpath()
bezier.cspsubdiv(path,0.5)
for subpath in path:
coords=[[round((c[0][0]-viewBox[0])*wscale),round((c[0][1]-viewBox[1])*hscale)] for c in subpath]
i=0
while i=3: shapes[linkindex].insert(0,{'shape':'poly','coords':coords,'href':href})
href=None # because subsequent subpaths must be enclaves
for i in range(len(shapes)):
alt=links[i]['alt']
if len(shapes[i])==0: inkex.errormsg(_("The hyperlink \"{}\" could not be added to the output.").format(links[i]['href']))
for j in shapes[i]:
attrs=links[i].copy()
attrs['alt']=alt
attrs.update(j)
stream.write(bytes(shapemarkup(attrs),'utf-8'))
alt=None
if __name__ == "__main__":
ImageMap().run()