mightyscape-1.2/extensions/fablabchemnitz/pixel2svg/pixel2svg.py

336 lines
14 KiB
Python
Raw Normal View History

2022-10-03 02:49:07 +02:00
#!/usr/bin/env python3
"""
Pixel2SVG - Convert the pixels of bitmap images to SVG rects
Idea and original implementation as standalone script:
Copyright (C) 2011 Florian Berger <fberger@florian-berger.de>
Homepage: <http://florian-berger.de/en/software/pixel2svg>
Rewritten as Inkscape extension:
Copyright (C) 2012 ~suv <suv-sf@users.sourceforge.net>
'getFilePath()' is based on code from 'extractimages.py':
Copyright (C) 2005 Aaron Spike, aaron@ekips.org
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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
"""
import os
import sys
import base64
from io import StringIO, BytesIO
import urllib.request as urllib
import inkex
from PIL import Image
from lxml import etree
DEBUG = False
# int r = ( hexcolor >> 16 ) & 0xFF;
# int g = ( hexcolor >> 8 ) & 0xFF;
# int b = hexcolor & 0xFF;
# int hexcolor = (r << 16) + (g << 8) + b;
def hex_to_int_color(v):
if (v[0] == '#'):
v = v[1:]
assert(len(v) == 6)
return int(v[:2], 16), int(v[2:4], 16), int(v[4:6], 16)
class Pixel2SVG(inkex.EffectExtension):
def add_arguments(self, pars):
pars.add_argument("-s", "--squaresize", type=int, default="5", help="Width and height of vector squares in pixels")
pars.add_argument("--same_like_original", type=inkex.Boolean, default=True, help="Same size as original")
pars.add_argument("--transparency", type=inkex.Boolean, default=True, help="Convert transparency to 'fill-opacity'")
pars.add_argument("--overlap", type=inkex.Boolean, default=False, help="Overlap vector squares by 1px")
pars.add_argument("--offset_image", type=inkex.Boolean, default=True, help="Offset traced image")
pars.add_argument("--delete_image", type=inkex.Boolean, default=False, help="Delete bitmap image")
pars.add_argument("--maxsize", type=int, default="256", help="Max. image size (width or height)")
pars.add_argument("--verbose", type=inkex.Boolean, default=False)
pars.add_argument("--color_mode", default="all", help="Which colors to trace.")
pars.add_argument("--color", default="FFFFFF", help="Special color")
pars.add_argument("--tab")
def checkImagePath(self, node):
"""Embed the data of the selected Image Tag element"""
xlink = node.get('xlink:href')
if xlink and xlink[:5] == 'data:':
# No need, data alread embedded
return
url = urllib.urlparse(xlink)
href = urllib.url2pathname(url.path)
# Primary location always the filename itself.
path = self.absolute_href(href or '')
# Backup directory where we can find the image
if not os.path.isfile(path):
path = node.get('sodipodi:absref', path)
if not os.path.isfile(path):
inkex.errormsg('File not found "{}". Unable to embed image.').format(path)
return
if (os.path.isfile(path)):
return path
def drawFilledRect(self, parent, svgpx):
"""
Draw rect based on ((x, y), (width,height), ((r,g,b),a)), add to parent
"""
style = {}
pos = svgpx[0]
dim = svgpx[1]
rgb = svgpx[2][0]
alpha = svgpx[2][1]
style['stroke'] = 'none'
if len(rgb) == 3:
# fill: rgb tuple
style['fill'] = '#%02x%02x%02x' % (rgb[0], rgb[1], rgb[2])
elif len(rgb) == 1:
# fill: color name, or 'none'
style['fill'] = rgb[0]
else:
# fill: 'Unset' (no fill defined)
pass
if alpha < 255:
# only write 'fill-opacity' for non-default value
style['fill-opacity'] = '%s' % round(alpha/255.0, 8)
rect_attribs = {'x': str(pos[0]),
'y': str(pos[1]),
'width': str(dim[0]),
'height': str(dim[1]),
'style': str(inkex.Style(style)), }
rect = etree.SubElement(parent, inkex.addNS('rect', 'svg'), rect_attribs)
return rect
def vectorizeImage(self, node):
"""
Parse RGBA values of linked bitmap image, create a group and
draw the rectangles (SVG pixels) inside the new group
"""
self.path = self.checkImagePath(node) # This also ensures the file exists
if self.path is None: # check if image is embedded or linked
image_string = node.get('{http://www.w3.org/1999/xlink}href')
# find comma position
i = 0
while i < 40:
if image_string[i] == ',':
break
i = i + 1
image = Image.open(BytesIO(base64.b64decode(image_string[i + 1:len(image_string)])))
else:
image = Image.open(self.path)
if image:
# init, set limit (default: 256)
pixel2svg_max = self.options.maxsize
if self.options.verbose:
inkex.utils.debug("ID: %s" % node.get('id'))
inkex.utils.debug("Image size:\t%dx%d" % image.size)
inkex.utils.debug("Image format:\t%s" % image.format)
inkex.utils.debug("Image mode:\t%s" % image.mode)
inkex.utils.debug("Image info:\t%s" % image.info)
if (image.mode == 'P' and 'transparency' in image.info):
inkex.utils.debug(
"Note: paletted image with an alpha channel is handled badly with " +
"current PIL:\n" +
"<http://stackoverflow.com/questions/12462548/pil-image-mode-p-rgba>")
elif not image.mode in ('RGBA', 'LA'):
inkex.utils.debug("No alpha channel or transparency found")
image = image.convert("RGBA")
(width, height) = image.size
if width <= pixel2svg_max and height <= pixel2svg_max:
# color trace modes
trace_color = []
if self.options.color:
trace_color = hex_to_int_color(self.options.color)
# get RGBA data
rgba_values = list(image.getdata())
# create group
nodeParent = node.getparent()
nodeIndex = nodeParent.index(node)
pixel2svg_group = etree.Element(inkex.addNS('g', 'svg'))
pixel2svg_group.set('id', "%s_pixel2svg" % node.get('id'))
nodeParent.insert(nodeIndex + 1, pixel2svg_group)
# draw bbox rectangle at the bottom of group
pixel2svg_bbox_fill = ('none', )
pixel2svg_bbox_alpha = 255
pixel2svg_bbox = ((0, 0),
(width * self.options.squaresize,
height * self.options.squaresize),
(pixel2svg_bbox_fill, pixel2svg_bbox_alpha))
self.drawFilledRect(pixel2svg_group, pixel2svg_bbox)
img_w = node.get('width')
img_h = node.get('height')
img_x = node.get('x')
img_y = node.get('y')
if img_w is not None and img_h is not None and img_x is not None and img_y is not None:
#if width/height are not unitless but end with px, mm, in etc. we have to convert to a float number
if img_w[-1].isdigit() is False:
img_w = self.svg.uutounit(img_w)
if img_h[-1].isdigit() is False:
img_h = self.svg.uutounit(img_h)
if self.options.same_like_original is True:
scale_x = float(img_w) / pixel2svg_bbox[1][0] #px
scale_y = float(img_h) / pixel2svg_bbox[1][1] #px
else:
scale_x = 1.0
scale_y = 1.0
# move group beside original image
if self.options.offset_image:
pixel2svg_offset = float(img_w)
else:
pixel2svg_offset = 0.0
# add transformation to fit previous XY coordinates and width/height
# image might also be influenced by other transformations from parent:
if nodeParent is not None and nodeParent != self.document.getroot():
tpc = nodeParent.composed_transform()
x_offset = tpc.e + pixel2svg_offset
y_offset = tpc.f
else:
x_offset = 0.0 + pixel2svg_offset
y_offset = 0.0
transform = "matrix({:1.6f}, 0, 0, {:1.6f}, {:1.6f}, {:1.6f})"\
.format(scale_x, scale_y, float(img_x) + x_offset, float(img_y) + y_offset)
else:
t = node.composed_transform()
img_w = t.a
img_h = t.d
img_x = t.e
img_y = t.f
if self.options.same_like_original is True:
scale_x = float(img_w) / pixel2svg_bbox[1][0] #px
scale_y = float(img_h) / pixel2svg_bbox[1][1] #px
else:
scale_x = 1.0
scale_y = 1.0
# move group beside original image
if self.options.offset_image:
pixel2svg_offset = float(img_w)
else:
pixel2svg_offset = 0.0
# add transformation to fit previous XY coordinates and width/height
# image might also be influenced by other transformations from parent:
if nodeParent is not None and nodeParent != self.document.getroot():
tpc = nodeParent.composed_transform()
x_offset = tpc.e + pixel2svg_offset
y_offset = tpc.f
else:
x_offset = 0.0 + pixel2svg_offset
y_offset = 0.0
transform = "matrix({:1.6f}, 0, 0, {:1.6f}, {:1.6f}, {:1.6f})"\
.format(scale_x, scale_y, float(img_x) + x_offset, float(img_y) + y_offset)
pixel2svg_group.attrib['transform'] = transform
# reverse list (performance), pop last one instead of first
rgba_values.reverse()
# loop through pixels (per row)
rowcount = 0
while rowcount < height:
colcount = 0
while colcount < width:
rgba_tuple = rgba_values.pop()
# Omit transparent pixels
if rgba_tuple[3] > 0:
# color options
do_trace = True
if (self.options.color_mode != "all"):
if (trace_color == rgba_tuple[:3]):
# colors match
if (self.options.color_mode == "other"):
do_trace = False
else:
# colors don't match
if (self.options.color_mode == "this"):
do_trace = False
if do_trace:
# position
svgpx_x = colcount * self.options.squaresize
svgpx_y = rowcount * self.options.squaresize
# dimension + overlap
svgpx_size = self.options.squaresize + self.options.overlap
# get color, ignore alpha
svgpx_rgb = rgba_tuple[:3]
svgpx_a = 255
# transparency
if self.options.transparency:
svgpx_a = rgba_tuple[3]
svgpx = ((svgpx_x, svgpx_y),
(svgpx_size, svgpx_size),
(svgpx_rgb, svgpx_a)
)
# draw square in group
self.drawFilledRect(pixel2svg_group, svgpx)
colcount = colcount + 1
rowcount = rowcount + 1
# all done
if DEBUG:
inkex.utils.debug("All rects drawn.")
if self.options.delete_image:
nodeParent.remove(node)
else:
# bail out with larger images
inkex.errormsg(
"Bailing out: this extension is not intended for large images.\n" +
"The current limit is %spx for either dimension of the bitmap image."
% pixel2svg_max)
sys.exit(0)
# clean-up?
if DEBUG:
inkex.utils.debug("Done.")
else:
inkex.errormsg("Bailing out: No supported image file or data found")
sys.exit(1)
def effect(self):
"""
Pixel2SVG - Convert the pixels of bitmap images to SVG rects
"""
found_image = False
if (self.options.ids):
for node in self.svg.selected.values():
if node.tag == inkex.addNS('image', 'svg'):
found_image = True
self.vectorizeImage(node)
if not found_image:
inkex.errormsg("Please select one or more bitmap image(s) for Pixel2SVG")
sys.exit(0)
if __name__ == '__main__':
Pixel2SVG().run()