470 lines
18 KiB
Python
Executable file
470 lines
18 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
#
|
|
# Copyright (C) 2010 Aurelio A. Heckert, aurium (a) gmail dot com
|
|
#
|
|
# This program is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; either version 2 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, write to the Free Software
|
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
#
|
|
import subprocess
|
|
import os
|
|
import sys
|
|
import tempfile
|
|
|
|
from lxml import etree
|
|
|
|
import inkex
|
|
from inkex.localization import inkex_gettext as _
|
|
from webslicer_effect import WebSlicerMixin, is_empty
|
|
|
|
|
|
class Export(WebSlicerMixin, inkex.OutputExtension):
|
|
def add_arguments(self, pars):
|
|
pars.add_argument("--tab")
|
|
pars.add_argument("--dir")
|
|
pars.add_argument("--create-dir", type=inkex.Boolean, dest="create_dir")
|
|
pars.add_argument("--with-code", type=inkex.Boolean, dest="with_code")
|
|
|
|
svgNS = "{http://www.w3.org/2000/svg}"
|
|
|
|
def validate_inputs(self):
|
|
# The user must supply a directory to export:
|
|
if is_empty(self.options.dir):
|
|
raise inkex.AbortExtension(
|
|
_("You must give a directory to export the slices.")
|
|
)
|
|
# No directory separator at the path end:
|
|
if self.options.dir[-1] == "/" or self.options.dir[-1] == "\\":
|
|
self.options.dir = self.options.dir[0:-1]
|
|
# Test if the directory exists:
|
|
if not os.path.exists(self.options.dir):
|
|
if self.options.create_dir:
|
|
# Try to create it:
|
|
try:
|
|
os.makedirs(self.options.dir)
|
|
except Exception as e:
|
|
raise inkex.AbortExtension(
|
|
_("Can't create '{}': {}.".format(self.options.dir, e))
|
|
)
|
|
else:
|
|
raise inkex.AbortExtension(
|
|
_("Dir doesn't exist '{}'.".format(self.options.dir))
|
|
)
|
|
# Check whether slicer layer exists (bug #1198826)
|
|
slicer_layer = self.get_slicer_layer()
|
|
if slicer_layer is None:
|
|
raise inkex.AbortExtension(_("No slicer layer found."))
|
|
else:
|
|
self.unique_html_id(slicer_layer)
|
|
return None
|
|
|
|
def get_cmd_output(self, cmd):
|
|
try:
|
|
pipe = subprocess.Popen(cmd)
|
|
except FileNotFoundError:
|
|
return 1, ""
|
|
stdout, _ = pipe.communicate()
|
|
sts = pipe.returncode
|
|
if sts is None:
|
|
sts = 0
|
|
if stdout is not None and stdout[-1:] == "\n":
|
|
stdout = stdout[:-1]
|
|
return sts, stdout
|
|
|
|
_html_ids = []
|
|
|
|
def unique_html_id(self, el):
|
|
for child in el.getchildren():
|
|
if child.tag in [
|
|
self.svgNS + "rect",
|
|
self.svgNS + "path",
|
|
self.svgNS + "circle",
|
|
self.svgNS + "g",
|
|
]:
|
|
conf = self.get_el_conf(child)
|
|
if conf["html-id"] in self._html_ids:
|
|
inkex.errormsg(
|
|
'You have more than one element with "{}" html-id.'.format(
|
|
conf["html-id"]
|
|
)
|
|
)
|
|
n = 2
|
|
while (conf["html-id"] + "-" + str(n)) in self._html_ids:
|
|
n += 1
|
|
conf["html-id"] += "-" + str(n)
|
|
self._html_ids.append(conf["html-id"])
|
|
self.save_conf(conf, child)
|
|
self.unique_html_id(child)
|
|
|
|
def test_if_has_imagemagick(self):
|
|
(status, output) = self.get_cmd_output("convert --version")
|
|
self.has_magick = status == 0 and "ImageMagick" in output
|
|
|
|
def save(self, stream):
|
|
self.test_if_has_imagemagick()
|
|
error = self.validate_inputs()
|
|
if error:
|
|
return error
|
|
# Register the basic CSS code:
|
|
self.reg_css("body", "text-align", "center")
|
|
# Create the temporary SVG with invisible Slicer layer to export image pieces
|
|
self.create_the_temporary_svg()
|
|
# Start what we really want!
|
|
self.export_chids_of(self.get_slicer_layer())
|
|
# Write the HTML and CSS files if asked for:
|
|
if self.options.with_code:
|
|
self.make_html_file()
|
|
self.make_css_file()
|
|
# Delete the temporary SVG with invisible Slicer layer
|
|
self.delete_the_temporary_svg()
|
|
# TODO: prevent inkex to return svg code to update Inkscape
|
|
|
|
def make_html_file(self):
|
|
f = open(os.path.join(self.options.dir, "layout.html"), "w")
|
|
f.write(
|
|
"<html>\n<head>\n"
|
|
+ " <title>Web Layout Testing</title>\n"
|
|
+ ' <style type="text/css" media="screen">\n'
|
|
+ ' @import url("style.css")\n'
|
|
+ " </style>\n"
|
|
+ "</head>\n<body>\n"
|
|
+ self.html_code()
|
|
+ ' <p style="position:absolute; bottom:10px">\n'
|
|
+ " This HTML code is not done to the web. <br/>\n"
|
|
+ " The automatic HTML and CSS code are only a helper."
|
|
+ "</p>\n</body>\n</html>"
|
|
)
|
|
f.close()
|
|
|
|
def make_css_file(self):
|
|
f = open(os.path.join(self.options.dir, "style.css"), "w")
|
|
f.write(
|
|
"/*\n"
|
|
+ "** This CSS code is not done to the web.\n"
|
|
+ "** The automatic HTML and CSS code are only a helper.\n"
|
|
+ "*/\n"
|
|
+ self.css_code()
|
|
)
|
|
f.close()
|
|
|
|
def create_the_temporary_svg(self):
|
|
(self.tmp_svg_ref, self.tmp_svg) = tempfile.mkstemp(".svg")
|
|
layer = self.get_slicer_layer()
|
|
current_style = layer.style
|
|
layer.style = "display:none"
|
|
self.document.write(self.tmp_svg)
|
|
layer.style = current_style
|
|
|
|
def delete_the_temporary_svg(self):
|
|
try:
|
|
os.close(self.tmp_svg_ref)
|
|
os.remove(self.tmp_svg)
|
|
except (IOError, OSError, PermissionError):
|
|
pass
|
|
|
|
noid_element_count = 0
|
|
|
|
def get_el_conf(self, el):
|
|
desc = el.find(self.svgNS + "desc")
|
|
conf = {}
|
|
if desc is None:
|
|
desc = etree.SubElement(el, "desc")
|
|
if desc.text is None:
|
|
desc.text = ""
|
|
for line in desc.text.split("\n"):
|
|
if line.find(":") > 0:
|
|
line = line.split(":")
|
|
conf[line[0].strip()] = line[1].strip()
|
|
if not "html-id" in conf:
|
|
if el == self.get_slicer_layer():
|
|
return {"html-id": "#body#"}
|
|
else:
|
|
self.noid_element_count += 1
|
|
conf["html-id"] = "element-" + str(self.noid_element_count)
|
|
desc.text += "\nhtml-id:" + conf["html-id"]
|
|
return conf
|
|
|
|
def save_conf(self, conf, el):
|
|
desc = el.find("{http://www.w3.org/2000/svg}desc")
|
|
if desc is not None:
|
|
conf_a = []
|
|
for k in conf:
|
|
conf_a.append(k + " : " + conf[k])
|
|
desc.text = "\n".join(conf_a)
|
|
|
|
def export_chids_of(self, parent):
|
|
parent_id = self.get_el_conf(parent)["html-id"]
|
|
for el in parent.getchildren():
|
|
el_conf = self.get_el_conf(el)
|
|
if el.tag == self.svgNS + "g":
|
|
if self.options.with_code:
|
|
self.register_group_code(el, el_conf)
|
|
else:
|
|
self.export_chids_of(el)
|
|
if el.tag in [
|
|
self.svgNS + "rect",
|
|
self.svgNS + "path",
|
|
self.svgNS + "circle",
|
|
]:
|
|
if self.options.with_code:
|
|
self.register_unity_code(el, el_conf, parent_id)
|
|
self.export_img(el, el_conf)
|
|
|
|
def register_group_code(self, group, conf):
|
|
self.reg_html("div", group)
|
|
selec = "#" + conf["html-id"]
|
|
self.reg_css(selec, "position", "absolute")
|
|
geometry = self.get_relative_el_geometry(group)
|
|
self.reg_css(selec, "top", str(int(geometry["y"])) + "px")
|
|
self.reg_css(selec, "left", str(int(geometry["x"])) + "px")
|
|
self.reg_css(selec, "width", str(int(geometry["w"])) + "px")
|
|
self.reg_css(selec, "height", str(int(geometry["h"])) + "px")
|
|
self.export_chids_of(group)
|
|
|
|
def __validate_slice_conf(self, conf):
|
|
if not "layout-disposition" in conf:
|
|
conf["layout-disposition"] = "bg-el-norepeat"
|
|
if not "layout-position-anchor" in conf:
|
|
conf["layout-position-anchor"] = "mc"
|
|
return conf
|
|
|
|
def register_unity_code(self, el, conf, parent_id):
|
|
conf = self.__validate_slice_conf(conf)
|
|
css_selector = "#" + conf["html-id"]
|
|
bg_repeat = "no-repeat"
|
|
img_name = self.img_name(el, conf)
|
|
if conf["layout-disposition"][0:2] == "bg":
|
|
if conf["layout-disposition"][0:9] == "bg-parent":
|
|
if parent_id == "#body#":
|
|
css_selector = "body"
|
|
else:
|
|
css_selector = "#" + parent_id
|
|
if conf["layout-disposition"] == "bg-parent-repeat":
|
|
bg_repeat = "repeat"
|
|
if conf["layout-disposition"] == "bg-parent-repeat-x":
|
|
bg_repeat = "repeat-x"
|
|
if conf["layout-disposition"] == "bg-parent-repeat-y":
|
|
bg_repeat = "repeat-y"
|
|
lay_anchor = conf["layout-position-anchor"]
|
|
if lay_anchor == "tl":
|
|
lay_anchor = "top left"
|
|
if lay_anchor == "tc":
|
|
lay_anchor = "top center"
|
|
if lay_anchor == "tr":
|
|
lay_anchor = "top right"
|
|
if lay_anchor == "ml":
|
|
lay_anchor = "middle left"
|
|
if lay_anchor == "mc":
|
|
lay_anchor = "middle center"
|
|
if lay_anchor == "mr":
|
|
lay_anchor = "middle right"
|
|
if lay_anchor == "bl":
|
|
lay_anchor = "bottom left"
|
|
if lay_anchor == "bc":
|
|
lay_anchor = "bottom center"
|
|
if lay_anchor == "br":
|
|
lay_anchor = "bottom right"
|
|
self.reg_css(
|
|
css_selector,
|
|
"background",
|
|
'url("{}") {} {}'.format(img_name, bg_repeat, lay_anchor),
|
|
)
|
|
else: # conf['layout-disposition'][0:9] == 'bg-el...'
|
|
self.reg_html("div", el)
|
|
self.reg_css(
|
|
css_selector,
|
|
"background",
|
|
'url("{}") {}'.format(img_name, "no-repeat"),
|
|
)
|
|
self.reg_css(css_selector, "position", "absolute")
|
|
geo = self.get_relative_el_geometry(el, True)
|
|
self.reg_css(css_selector, "top", geo["y"])
|
|
self.reg_css(css_selector, "left", geo["x"])
|
|
self.reg_css(css_selector, "width", geo["w"])
|
|
self.reg_css(css_selector, "height", geo["h"])
|
|
else: # conf['layout-disposition'] == 'img...'
|
|
self.reg_html("img", el)
|
|
if conf["layout-disposition"] == "img-pos":
|
|
self.reg_css(css_selector, "position", "absolute")
|
|
geo = self.get_relative_el_geometry(el)
|
|
self.reg_css(css_selector, "left", str(geo["x"]) + "px")
|
|
self.reg_css(css_selector, "top", str(geo["y"]) + "px")
|
|
if conf["layout-disposition"] == "img-float-left":
|
|
self.reg_css(css_selector, "float", "right")
|
|
if conf["layout-disposition"] == "img-float-right":
|
|
self.reg_css(css_selector, "float", "right")
|
|
|
|
el_geo = {}
|
|
|
|
def register_all_els_geometry(self):
|
|
ink_cmm = "inkscape --query-all " + self.tmp_svg
|
|
(status, output) = self.get_cmd_output(ink_cmm)
|
|
self.el_geo = {}
|
|
if status == 0:
|
|
for el in output.split("\n"):
|
|
el = el.split(",")
|
|
if len(el) == 5:
|
|
self.el_geo[el[0]] = {
|
|
"x": float(el[1]),
|
|
"y": float(el[2]),
|
|
"w": float(el[3]),
|
|
"h": float(el[4]),
|
|
}
|
|
doc_w = self.svg.unittouu(self.document.getroot().get("width"))
|
|
doc_h = self.svg.unittouu(self.document.getroot().get("height"))
|
|
self.el_geo["webslicer-layer"] = {"x": 0, "y": 0, "w": doc_w, "h": doc_h}
|
|
|
|
def get_relative_el_geometry(self, el, value_to_css=False):
|
|
# This method return a dictionary with x, y, w and h keys.
|
|
# All values are float, if value_to_css is False, otherwise
|
|
# that is a string ended with "px". The x and y values are
|
|
# relative to parent position.
|
|
if not self.el_geo:
|
|
self.register_all_els_geometry()
|
|
parent = el.getparent()
|
|
geometry = self.el_geo[el.attrib["id"]]
|
|
geometry["x"] -= self.el_geo[parent.attrib["id"]]["x"]
|
|
geometry["y"] -= self.el_geo[parent.attrib["id"]]["y"]
|
|
if value_to_css:
|
|
for k in geometry:
|
|
geometry[k] = str(int(geometry[k])) + "px"
|
|
return geometry
|
|
|
|
def img_name(self, el, conf):
|
|
return el.attrib["id"] + "." + conf["format"]
|
|
|
|
def export_img(self, el, conf):
|
|
if not self.has_magick:
|
|
inkex.errormsg(_("You must install the ImageMagick to get JPG and GIF."))
|
|
conf["format"] = "png"
|
|
img_name = os.path.join(self.options.dir, self.img_name(el, conf))
|
|
img_name_png = img_name
|
|
if conf["format"] != "png":
|
|
img_name_png = img_name + ".png"
|
|
opts = ""
|
|
if "bg-color" in conf:
|
|
opts += ' -b "' + conf["bg-color"] + '" -y 1'
|
|
if "dpi" in conf:
|
|
opts += " -d " + conf["dpi"]
|
|
if "dimension" in conf:
|
|
dim = conf["dimension"].split("x")
|
|
opts += " -w " + dim[0] + " -h " + dim[1]
|
|
(status, output) = self.get_cmd_output(
|
|
'inkscape {} -i "{}" -o "{}" "{}"'.format(
|
|
opts, el.attrib["id"], img_name_png, self.tmp_svg
|
|
)
|
|
)
|
|
if conf["format"] != "png":
|
|
opts = ""
|
|
if conf["format"] == "jpg":
|
|
opts += " -quality " + str(conf["quality"])
|
|
if conf["format"] == "gif":
|
|
if conf["gif-type"] == "grayscale":
|
|
opts += " -type Grayscale"
|
|
else:
|
|
opts += " -type Palette"
|
|
if conf["palette-size"] < 256:
|
|
opts += " -colors " + str(conf["palette-size"])
|
|
(status, output) = self.get_cmd_output(
|
|
'convert "{}" {} "{}"'.format(img_name_png, opts, img_name)
|
|
)
|
|
if status != 0:
|
|
inkex.errormsg("Upss... ImageMagick error: " + output)
|
|
os.remove(img_name_png)
|
|
|
|
_html = {}
|
|
|
|
def reg_html(self, el_tag, el):
|
|
parent = el.getparent()
|
|
parent_id = self.get_el_conf(parent)["html-id"]
|
|
if parent == self.get_slicer_layer():
|
|
parent_id = "body"
|
|
conf = self.get_el_conf(el)
|
|
el_id = conf["html-id"]
|
|
if "html-class" in conf:
|
|
el_class = conf["html-class"]
|
|
else:
|
|
el_class = ""
|
|
if not parent_id in self._html:
|
|
self._html[parent_id] = []
|
|
self._html[parent_id].append({"tag": el_tag, "id": el_id, "class": el_class})
|
|
|
|
def html_code(self, parent="body", ident=" "):
|
|
# inkex.errormsg( self._html )
|
|
if not parent in self._html:
|
|
return ""
|
|
code = ""
|
|
for el in self._html[parent]:
|
|
child_code = self.html_code(el["id"], ident + " ")
|
|
tag_class = ""
|
|
if el["class"] != "":
|
|
tag_class = ' class="' + el["class"] + '"'
|
|
if el["tag"] == "img":
|
|
code += (
|
|
ident
|
|
+ '<img id="'
|
|
+ el["id"]
|
|
+ '"'
|
|
+ tag_class
|
|
+ ' src="'
|
|
+ self.img_name(el, self.get_el_conf(el))
|
|
+ '"/>\n'
|
|
)
|
|
else:
|
|
code += (
|
|
ident
|
|
+ "<"
|
|
+ el["tag"]
|
|
+ ' id="'
|
|
+ el["id"]
|
|
+ '"'
|
|
+ tag_class
|
|
+ ">\n"
|
|
)
|
|
if child_code:
|
|
code += child_code
|
|
else:
|
|
code += ident + " Element " + el["id"] + "\n"
|
|
code += ident + "</" + el["tag"] + '><!-- id="' + el["id"] + '" -->\n'
|
|
return code
|
|
|
|
_css = []
|
|
|
|
def reg_css(self, selector, att, val):
|
|
pos = i = -1
|
|
for s in self._css:
|
|
i += 1
|
|
if s["selector"] == selector:
|
|
pos = i
|
|
if pos == -1:
|
|
pos = i + 1
|
|
self._css.append({"selector": selector, "atts": {}})
|
|
if not att in self._css[pos]["atts"]:
|
|
self._css[pos]["atts"][att] = []
|
|
self._css[pos]["atts"][att].append(val)
|
|
|
|
def css_code(self):
|
|
code = ""
|
|
for s in self._css:
|
|
code += "\n" + s["selector"] + " {\n"
|
|
for att in s["atts"]:
|
|
val = s["atts"][att]
|
|
if att == "background" and len(val) > 1:
|
|
code += " /* the next attribute needs a CSS3 enabled browser */\n"
|
|
code += " " + att + ": " + (", ".join(val)) + ";\n"
|
|
code += "}\n"
|
|
return code
|
|
|
|
|
|
if __name__ == "__main__":
|
|
Export().run()
|