aboutsummaryrefslogtreecommitdiff
path: root/imagemap.py
blob: 00b5f64e933b1f819e62a81953ad5c5c9abd90ca (plain)
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
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({'&':'&amp;','<':'&lt;','>':'&gt;'})
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:'&#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, be visually unaffected by `fill-rule`, and not go out of bounds.
		# TODO pay attention to clip-path
		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): 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]
		
		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-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)
		newbytes=inkscape_command(self.svg,actions=command)
		self.svg=self.load(newbytes).getroot()
		
		seen=set()
		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']
			alt=link['alt'] if int(linkindex) not in seen else None
			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,shapemarkup('rect',rectify(coords),href,alt))
				elif len(coords)>=3: shapes[linkindex].insert(0,shapemarkup('poly',coords,href,alt))
				href=None # because subsequent subpaths must be enclaves
				alt=None # TODO make it come first even though the order's reversed
			seen.add(linkindex)
		stream.write(bytes(''.join(sum(shapes,start=[])),'utf-8'))

if __name__ == "__main__":
	ImageMap().run()