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 (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=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()