From f138950cbf5908fb75fbf1de17d41eb2bf28007e Mon Sep 17 00:00:00 2001 From: Viatrix Date: Wed, 11 Mar 2026 06:12:19 -0700 Subject: Good a time as any for an initial commit It sort-of works. --- imagemap.inx | 19 +++++ imagemap.py | 98 ++++++++++++++++++++++ tests/__init__.py | 0 .../imagemap__--maptype__HTML__intersect__svg.out | 2 + .../imagemap__--maptype__HTML__overlap__svg.out | 4 + .../refs/imagemap__--maptype__HTML__rect__svg.out | 1 + .../imagemap__--maptype__HTML__rounding__svg.out | 1 + .../imagemap__--maptype__HTML__viewbox__svg.out | 1 + tests/data/svg/intersect.svg | 5 ++ tests/data/svg/overlap.svg | 8 ++ tests/data/svg/rect.svg | 5 ++ tests/data/svg/rounding.svg | 5 ++ tests/data/svg/text.svg | 14 ++++ tests/data/svg/viewbox.svg | 5 ++ tests/test_imagemap_comparison.py | 17 ++++ 15 files changed, 185 insertions(+) create mode 100644 imagemap.inx create mode 100644 imagemap.py create mode 100644 tests/__init__.py create mode 100644 tests/data/refs/imagemap__--maptype__HTML__intersect__svg.out create mode 100644 tests/data/refs/imagemap__--maptype__HTML__overlap__svg.out create mode 100644 tests/data/refs/imagemap__--maptype__HTML__rect__svg.out create mode 100644 tests/data/refs/imagemap__--maptype__HTML__rounding__svg.out create mode 100644 tests/data/refs/imagemap__--maptype__HTML__viewbox__svg.out create mode 100644 tests/data/svg/intersect.svg create mode 100644 tests/data/svg/overlap.svg create mode 100644 tests/data/svg/rect.svg create mode 100644 tests/data/svg/rounding.svg create mode 100644 tests/data/svg/text.svg create mode 100644 tests/data/svg/viewbox.svg create mode 100644 tests/test_imagemap_comparison.py diff --git a/imagemap.inx b/imagemap.inx new file mode 100644 index 0000000..96d13a0 --- /dev/null +++ b/imagemap.inx @@ -0,0 +1,19 @@ + + + Generate Image Map + computer.viatrix.inx.imagemap + + .map + text/plain + WWW Image Map + true + + + + + + + + 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({'&':'&','<':'<','>':'>'}) +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"\n", + 'XHTML': lambda shape, coords, href, alt: + f"{quotedval(alt)}'\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 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=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 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 @@ + + 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 @@ + + + + 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 @@ + 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 @@ + 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 @@ + 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 @@ + + + + + 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 @@ + + + + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + A + + + + B + + + + C + + 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 @@ + + + + + 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) -- cgit