1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
|
import inkex
from inkex import bezier
from inkex.command import inkscape_command
# (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('{http://www.w3.org/1999/xlink}title')
} # TODO target
SHAPE_MARKUP = {
'HTML': lambda shape, coords, href, alt:
f"<area shape={shape} coords={','.join(str(c) for c in sum(coords,start=[]))}"
+(f' href{htmlval(href)}' if href is not None else '')
+(f' alt{htmlval(alt)}' if alt is not None else '')+">\n",
'XHTML': lambda shape, coords, href, alt:
f"<area shape=\"{shape}\" coords=\"{','.join(str(c) for c in sum(coords,start=[]))}\""
+(f" href={quotedval(href)}" if href is not None else '')
+(f' alt={quotedval(alt)}' if alt is not None else '')+"/>\n",
'mod_imagemap': lambda shape, coords, href, alt:
f"{shape} {href if href is not None else 'nocontent'} {' '.join(f'{i[0]},{i[1]}' for i in coords)}"
+(f" \"{alt.translate({34:'"'})}\"" if alt is not None else '')+'\n'
} # 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, and be visually unaffected by `fill-rule`.
# TODO pay attention to clip-path
links=[]
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): continue
style=el.effective_style()
style[CSS_LINK_INDEX]=f'" {CSS_LINK_INDEX}-{len(links)} "'
links += [link]
command=\
';'.join(f'select-clear;select-by-selector:[style~="{CSS_LINK_INDEX}-{i}"];object-stroke-to-path;path-union;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)
newbytes=inkscape_command(self.svg,actions=command)
self.svg=self.load(newbytes).getroot()
seen=set()
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']
alt=link['alt'] if int(linkindex) not in seen else None
shapes=[]
path=el.get_path().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<len(coords):
if coords[i]==coords[(i+1)%len(coords)]: coords.pop(i)
else: i+=1
if rectifiable(coords): shapes.append(shapemarkup('rect',rectify(coords),href,alt))
elif len(coords)>=3: shapes.append(shapemarkup('poly',coords,href,alt))
href=None # because subsequent subpaths must be enclaves
alt=None
seen.add(linkindex)
stream.write(bytes(''.join(reversed(shapes)),'utf-8'))
if __name__ == "__main__":
ImageMap().run()
|