#!/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("--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)

                # move group beside original image
                if self.options.offset_image:
                    pixel2svg_offset = width
                else:
                    pixel2svg_offset = 0.0
                pixel2svg_translate = ('translate(%s, %s)' %
                                       (float(node.get('x') or 0.0) + pixel2svg_offset,
                                        node.get('y') or 0.0))
                pixel2svg_group.set('transform', pixel2svg_translate)

                # 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)

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