aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorViatrix2026-03-11 06:12:19 -0700
committerViatrix2026-03-11 06:12:19 -0700
commitf138950cbf5908fb75fbf1de17d41eb2bf28007e (patch)
tree02a22e69c39d95a85b95b9972f6b1002ec02d495
Good a time as any for an initial commit
It sort-of works.
-rw-r--r--imagemap.inx19
-rw-r--r--imagemap.py98
-rw-r--r--tests/__init__.py0
-rw-r--r--tests/data/refs/imagemap__--maptype__HTML__intersect__svg.out2
-rw-r--r--tests/data/refs/imagemap__--maptype__HTML__overlap__svg.out4
-rw-r--r--tests/data/refs/imagemap__--maptype__HTML__rect__svg.out1
-rw-r--r--tests/data/refs/imagemap__--maptype__HTML__rounding__svg.out1
-rw-r--r--tests/data/refs/imagemap__--maptype__HTML__viewbox__svg.out1
-rw-r--r--tests/data/svg/intersect.svg5
-rw-r--r--tests/data/svg/overlap.svg8
-rw-r--r--tests/data/svg/rect.svg5
-rw-r--r--tests/data/svg/rounding.svg5
-rw-r--r--tests/data/svg/text.svg14
-rw-r--r--tests/data/svg/viewbox.svg5
-rw-r--r--tests/test_imagemap_comparison.py17
15 files changed, 185 insertions, 0 deletions
diff --git a/imagemap.inx b/imagemap.inx
new file mode 100644
index 0000000..96d13a0
--- /dev/null
+++ b/imagemap.inx
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
+ <name>Generate Image Map</name>
+ <id>computer.viatrix.inx.imagemap</id>
+ <output>
+ <extension>.map</extension>
+ <mimetype>text/plain</mimetype>
+ <filetypename>WWW Image Map</filetypename>
+ <dataloss>true</dataloss>
+ </output>
+ <param type="optiongroup" appearance="radio" name="maptype" gui-text="Map Type">
+ <option value="HTML">HTML</option>
+ <option value="XHTML">XHTML</option>
+ <option value="mod_imagemap">mod_imagemap</option>
+ </param>
+ <script>
+ <command location="inx" interpreter="python">imagemap.py</command>
+ </script>
+</inkscape-extension>
diff --git a/imagemap.py b/imagemap.py
new file mode 100644
index 0000000..4c4c931
--- /dev/null
+++ b/imagemap.py
@@ -0,0 +1,98 @@
+import inkex
+from inkex import bezier
+from inkex.command import inkscape_command
+
+# (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, 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 BUT NOT IN TEXT SO WE GOTTA FIGURE OUT HOW TODO TEXT
+ if not isinstance(el,inkex.ShapeElement): continue
+ style=el.effective_style()
+ style[CSS_LINK_INDEX]=f'" {CSS_LINK_INDEX}-{len(links)} "'
+ links += [link]
+ command='select-all;path-flatten;'+ \
+ ';'.join(f'select-clear;select-by-selector:[style~="{CSS_LINK_INDEX}-{i}"];path-union;path-split' for i in range(len(links)))
+ 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()
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/__init__.py
diff --git a/tests/data/refs/imagemap__--maptype__HTML__intersect__svg.out b/tests/data/refs/imagemap__--maptype__HTML__intersect__svg.out
new file mode 100644
index 0000000..cab9492
--- /dev/null
+++ b/tests/data/refs/imagemap__--maptype__HTML__intersect__svg.out
@@ -0,0 +1,2 @@
+<area shape=poly coords=60,40,60,60,40,60,40,80,80,80,80,40 href=http://example.com>
+<area shape=poly coords=20,20,20,60,40,60,40,40,60,40,60,20 href=http://example.com>
diff --git a/tests/data/refs/imagemap__--maptype__HTML__overlap__svg.out b/tests/data/refs/imagemap__--maptype__HTML__overlap__svg.out
new file mode 100644
index 0000000..8f8d95d
--- /dev/null
+++ b/tests/data/refs/imagemap__--maptype__HTML__overlap__svg.out
@@ -0,0 +1,4 @@
+<area shape=poly coords=30,40,30,70,60,70,60,60,40,60,40,40 href=http://example.com/1>
+<area shape=poly coords=30,10,30,30,40,30,40,20,80,20,80,60,70,60,70,70,90,70,90,10 href=http://example.com/1>
+<area shape=rect coords=20,40,60,80>
+<area shape=rect coords=10,30,70,90 href=http://example.com/2>
diff --git a/tests/data/refs/imagemap__--maptype__HTML__rect__svg.out b/tests/data/refs/imagemap__--maptype__HTML__rect__svg.out
new file mode 100644
index 0000000..9399c1c
--- /dev/null
+++ b/tests/data/refs/imagemap__--maptype__HTML__rect__svg.out
@@ -0,0 +1 @@
+<area shape=rect coords=25,25,75,75 href=http://example.com>
diff --git a/tests/data/refs/imagemap__--maptype__HTML__rounding__svg.out b/tests/data/refs/imagemap__--maptype__HTML__rounding__svg.out
new file mode 100644
index 0000000..0fc648a
--- /dev/null
+++ b/tests/data/refs/imagemap__--maptype__HTML__rounding__svg.out
@@ -0,0 +1 @@
+<area shape=poly coords=50,25,25,75,75,75 href=http://example.com>
diff --git a/tests/data/refs/imagemap__--maptype__HTML__viewbox__svg.out b/tests/data/refs/imagemap__--maptype__HTML__viewbox__svg.out
new file mode 100644
index 0000000..0fc648a
--- /dev/null
+++ b/tests/data/refs/imagemap__--maptype__HTML__viewbox__svg.out
@@ -0,0 +1 @@
+<area shape=poly coords=50,25,25,75,75,75 href=http://example.com>
diff --git a/tests/data/svg/intersect.svg b/tests/data/svg/intersect.svg
new file mode 100644
index 0000000..5f07634
--- /dev/null
+++ b/tests/data/svg/intersect.svg
@@ -0,0 +1,5 @@
+<svg width="100" height="100" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <a xlink:href="http://example.com">
+ <path d="M20 20 60 20 60 60 20 60ZM40 40 40 80 80 80 80 40Z"/>
+ </a>
+</svg>
diff --git a/tests/data/svg/overlap.svg b/tests/data/svg/overlap.svg
new file mode 100644
index 0000000..7a3e7aa
--- /dev/null
+++ b/tests/data/svg/overlap.svg
@@ -0,0 +1,8 @@
+<svg width="100" height="100" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" stroke-width="10" fill="none">
+ <a xlink:href="http://example.com/1">
+ <rect x="35" y="15" width="50" height="50" stroke="red"/>
+ </a>
+ <a xlink:href="http://example.com/2">
+ <rect x="15" y="35" width="50" height="50" stroke="blue"/>
+ </a>
+</svg>
diff --git a/tests/data/svg/rect.svg b/tests/data/svg/rect.svg
new file mode 100644
index 0000000..445e6e0
--- /dev/null
+++ b/tests/data/svg/rect.svg
@@ -0,0 +1,5 @@
+<svg width="100" height="100" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <a xlink:href="http://example.com">
+ <rect x="25" y="25" width="50" height="50"/>
+ </a>
+</svg>
diff --git a/tests/data/svg/rounding.svg b/tests/data/svg/rounding.svg
new file mode 100644
index 0000000..e3f98e6
--- /dev/null
+++ b/tests/data/svg/rounding.svg
@@ -0,0 +1,5 @@
+<svg width="100" height="100" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <a xlink:href="http://example.com">
+ <polyline points="50.3,24.6 74.8,75.4 25.2,75.01"/>
+ </a>
+</svg>
diff --git a/tests/data/svg/text.svg b/tests/data/svg/text.svg
new file mode 100644
index 0000000..6b0a130
--- /dev/null
+++ b/tests/data/svg/text.svg
@@ -0,0 +1,14 @@
+<svg width="300" height="200" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <a xlink:href="http://example.com/1">
+ <ellipse cx="75" cy="50" rx="60" ry="30"/>
+ <text x="75" y="50" fill="#fff" text-anchor="middle" font-size="16">A</text>
+ </a>
+ <a xlink:href="http://example.com/2">
+ <ellipse cx="225" cy="50" rx="60" ry="30"/>
+ <text x="225" y="50" fill="#fff" text-anchor="middle" font-size="16">B</text>
+ </a>
+ <a xlink:href="http://example.com/1">
+ <ellipse cx="150" cy="150" rx="60" ry="30"/>
+ <text x="150" y="150" fill="#fff" text-anchor="middle" font-size="16">C</text>
+ </a>
+</svg>
diff --git a/tests/data/svg/viewbox.svg b/tests/data/svg/viewbox.svg
new file mode 100644
index 0000000..a0cdc1d
--- /dev/null
+++ b/tests/data/svg/viewbox.svg
@@ -0,0 +1,5 @@
+<svg width="100" height="100" viewBox="-100 -100 200 200" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <a xlink:href="http://example.com">
+ <polyline points="0,-50 50,50 -50,50"/>
+ </a>
+</svg>
diff --git a/tests/test_imagemap_comparison.py b/tests/test_imagemap_comparison.py
new file mode 100644
index 0000000..302162c
--- /dev/null
+++ b/tests/test_imagemap_comparison.py
@@ -0,0 +1,17 @@
+from inkex.tester import ComparisonMixin, TestCase
+from imagemap import ImageMap
+
+class ImageMapComparisonTest(ComparisonMixin, TestCase):
+ effect_class = ImageMap
+ compare_file = (
+ 'svg/intersect.svg',
+ 'svg/overlap.svg',
+ 'svg/rect.svg',
+ 'svg/rounding.svg',
+ 'svg/text.svg',
+ 'svg/viewbox.svg'
+ )
+ comparisons=[('--maptype=HTML',)]
+ compare_file_extension='map'
+ def _base_compare(self, data_a, data_b, compare_mode):
+ self.assertEqual(data_a, data_b)