# Flexible code for colorful raster images with randomized colors

# Language: Simple Inkscape Scripting (SIS, apparently)
# Details: https://inkscape.org/~pakin/%E2%98%85simple-inkscape-scripting
# Github: https://github.com/spakin/SimpInkScr

# rasterscript version 0.7
#
# Creates a multicolor background grid consisting of squares or circles (cells)
# Multicolor circles (eyes) are placed on top of it
# Colors used for cell and eyes are to a degree random:
# - default behavior is to use entirely random colors
# - the set of available colors can be limited by supplying a palette file
# The whole thing can be topped by a 'raster': a unicolor layer with round holes in it
# The result is supposed to look cool! Or at least it can be useful for further processing.
# Only circles and squares are created (no ovals or rectangles)
# Next step: import user-defines shapes

# Since this is already getting somewhat serious code, let's plant a license on it:
#
# Copyright 2022 Joseph Smith
#
# Permission to use, copy, modify, and/or distribute this software for any purpose with or without
# fee is hereby granted, provided that the above copyright notice and this permission notice
# appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS
# SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE 
# AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE
# OF THIS SOFTWARE.

configfile = 'rasterscript.cf'


# FUNCTIONS

# function for creating a shape; default is square
def makeshape(kind, xpos, ypos, size, sfill, swidth, objfill):
  if kind == 'square':
    offs = (CELLSIZE - size) / 2
    obj = rect((xpos + offs, ypos + offs), (xpos + size + offs, ypos + size + offs), stroke=sfill, stroke_width=swidth, fill=objfill)
  else:
    offs = CELLSIZE / 2
    radius = size / 2
    obj = circle((xpos + offs, ypos + offs), radius, stroke=sfill, stroke_width=swidth, fill=objfill)
  return obj

# Different ways for fetching a color, depending on config
# With a matrix, cell colors are accessed by index (flat list)
def getcolor(mindx):
  if matrix == 'yes':
    code = matrixlist[mindx]
    index = colorlist.index(code)
    clr = '#%s' % colorlist[index+1]
  elif palette == 'yes':
    rgbindex = randrange(rgbmax)
    clr = '#%s' % rgblist[rgbindex]
  else:
    clr = '#%02x%02x%02x' % (randrange(256), randrange(256), randrange(256))
  return clr

# function for getting a 'random' object position; used for eyes
def getwobble():
  # For 'random' positioning of eye
  xwob = 0
  ywob = 0
  if wobbly == 'yes':
    xwob = uniform(-2, 2)
    ywob = uniform(-2.5, 2.5)
  return (xwob, ywob)

# Create both a matrix and a definition list
def colormatrix():
  with open(matrixfile) as fp:
    rowcount = 0
    colcount = 0
    line = fp.readline().lstrip()
    while line:
      if '#' in line:
        parts = line.split('#')
        line = parts[0].rstrip()
      else:
        line = line.rstrip()
      if line == '':
        pass
      elif '=' in line:
        entry = line.split('=')
        for i in range(len(entry)):
          colorlist.append(entry[i].strip())
      else:
        entry = line.split()
        colcount = len(entry)
        for i in range(colcount):
          code = entry[i].split(':')
          for j in range(len(code)):
            matrixlist.append(code[j])
        rowcount += 1
      line = fp.readline()
    return(rowcount, colcount)


# CONFIG

# Parse configuration file; sets globals directly
def loadconfig(cffile):
  # For maintainers to maintain: expected types CAN be speciified; without, everything returns as str
  expectint = ['pagewidth', 'pageheight', 'cellsize', 'cellstrokewidth', 'eyesize', 'eyestrokewidth', 'holesize']
  expectfloat = []
  cflist = []
  with open(cffile) as fp:
    lines = fp.readlines()
    for line in lines:
      if '#' in line:
        parts = line.split('#')
        line = parts[0].rstrip()
      else:
        line = line.rstrip()
      if line == '':
        pass
      elif '=' in line:
        entry = line.split('=')
        cfname = entry[0].strip()
        cfval = entry[1].strip()
        cfval = cfval.replace("'", "")
        cfval = cfval.replace('"', "")
        configentry = [cfname, cfval]
        cflist.append(configentry)
  fp.close()
  for i in range(len(cflist)):
    name = cflist[i][0]
    value = cflist[i][1]
    # Hex strings atarting with 0x are always converted
    if (value.startswith("0x")):
      iv = int(value[2:], 16)
      globals()[name] = int(iv)
    elif name in expectint:
      iv = int('%s' % value, 0)
      globals()[name] = iv
    elif name in expectfloat:
      fv = float('%s' % value)
      globals()[name] = float(fv)
    else:
      globals()[name] = value
  return()

loadconfig(configfile)
#print(globals())

# This ugly construct allows user to comment out definitions for convenience
if not 'cellshape' in globals(): cellshape = 'square'
if not 'cellstrokewidth' in globals(): cellstrokewidth = '0'
if not 'cellstrokecolor' in globals(): cellstrokecolor = '000000'
if not 'eyeshape' in globals(): eyeshape = 'circle'
if not 'eyestrokewidth' in globals(): eyestrokewidth = '0'
if not 'eyestrokecolor' in globals(): eyestrokecolor = '000000'
if not 'wobbly' in globals(): wobbly = 'no'
if not 'raster' in globals(): raster = 'no'
if not 'holeshape' in globals(): holeshape = 'circle'
if not 'rastercolor' in globals(): rastercolor = 'ffffff'
if not 'palette' in globals(): palette = 'no'
if not 'matrix' in globals(): matrix = 'no'


# INIT

# ALL-CAPS vars are fundemental
CELLSIZE = cellsize
CELLROWS = 0
CELLCOLS = 0

# Set page dimensions if not set already. Doc: "SVG works in nominal "pixel" units."
if ('pageheight' in globals()) and ('pagewidth' in globals()):
  svg_root.set('width', '%spx' % pagewidth)
  svg_root.set('height', '%spx' % pageheight)
  width, height = svg_root.width, svg_root.height
  svg_root.set('viewBox', '0 0 %.0f %.0f' % (width, height))
else:
  pagewidth = float(svg_root.get('width').rstrip('px'))
  pageheight = float(svg_root.get('height').rstrip('px'))

# MAIN CODE

if matrix == 'yes':
  import os
  colorlist = []
  matrixlist = []
  (CELLROWS, CELLCOLS) = colormatrix()
  # Unused: user should take care of setting sensible page dimensions
  #colwidth = CELLCOLS * CELLSIZE
  #rowheight = CELLROWS * CELLSIZE
else:
  # Keep the output within the page boundaries
  CELLROWS = int(pagewidth // cellsize)
  CELLCOLS = int(pageheight // cellsize)

# Hanlde matrix or palette; matrix has priority
if (matrix != 'yes') and (palette == 'yes'):
  with open(palettefile, 'r') as pf:
    rgblist = pf.read().splitlines()
    rgbmax = len(rgblist) - 1
  pf.close()

# Three layers: 1. cells (background), 2. eyes, 3. raster with holes
l1 = layer('Background')
l2 = layer('Eyes')
if (raster == 'yes') and (rastercolor != 'transparent'): l3 = layer('Raster')

# Every single element is created and positioned individually, then added to its layer
for i in range(CELLROWS):

  for j in range(CELLCOLS):

    # For getcolor() with matrix
    mindex = (i + j) * 2

    # Cell position
    (y, x) = (i * CELLSIZE, j * CELLSIZE)

    # background layer
    color = getcolor(mindex)
    scolor = '#%s' % cellstrokecolor
    if (raster == 'yes') and (rastercolor == 'transparent'):
      # Why clip afterwards if you can make cellshape fit holeshape to begin with
      bgcell = makeshape(holeshape, x, y, holesize, scolor, cellstrokewidth, color)
    else:
      bgcell = makeshape(cellshape, x, y, CELLSIZE, scolor, cellstrokewidth, color)
    l1.add(bgcell)

    # eyes layer
    color = getcolor(mindex+1)
    scolor = '#%s' % eyestrokecolor
    (xwobble, ywobble) = getwobble()
    eye = makeshape(eyeshape, x + xwobble, y + ywobble, eyesize, scolor, eyestrokewidth, color)
    l2.add(eye)

    # raster layer: a new cell covering the background cell, with a hole punched into it
    if (raster == 'yes') and (rastercolor != 'transparent'):

      # raster cell
      color = '#%s' % rastercolor
      rstcell = makeshape('square', x, y, CELLSIZE, 'unused', 0, color).to_path()

      # Hole shape
      color = '#ffffff'
      rsthole = makeshape(holeshape, x, y, holesize, 'unused', 0, color).to_path()

      # Punch hole. Follows even-odd fill rule and draw direction (?)
      if holeshape == 'circle':
        rstcell.append(rsthole)
      else:
        rstcell.append(rsthole.reverse())

      l3.add(rstcell)