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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
|
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"<area shape={attrs['shape']} coords={','.join(str(c) for c in sum(attrs['coords'],start=[]))}"
+(''.join(f' {i}{htmlval(attrs[i])}' for i in attrs if attrs[i] is not None and i not in {'shape','coords'}))
+">\n",
'XHTML': lambda attrs:
f"<area shape=\"{attrs['shape']}\" coords=\"{','.join(str(c) for c in sum(attrs['coords'],start=[]))}\""
+(''.join(f" {i}={quotedval(attrs[i])}" for i in attrs if attrs[i] is not None and i not in {'shape','coords'}))
+"/>\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<len(coords):
if coords[i]==coords[(i+1)%len(coords)]: coords.pop(i)
else: i+=1
if rectifiable(coords): shapes[linkindex].insert(0,{'shape':'rect','coords':rectify(coords),'href':href})
elif len(coords)>=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 \"{}\" is not present in 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()
|