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

324 lines
16 KiB
Python

#!/usr/bin/env python3
'''
Created by Danylo Horbatenko 2018, dnkxyz@gmail.com
Copyright (C) 2018 George Fomitchev, gf@endurancerobots.com
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
'''
#Version control: last edited by 01.03.2018 8:20
import os
import tempfile
import shutil
import subprocess
import math
import inkex
import sys
import png
from lxml import etree
from inkex.paths import Path
def saw(x):
#The function returns a symmetric triangle wave with period 4 and varying between -1 and 1
x = math.fmod(x, 4.0)
x = math.fabs(x)
if x > 2.0:
y = 3 - x
else:
y = x - 1
return y
def square(x):
#The function returns a square wave with period 4 and varying between -1 and 1
x = math.fmod(x, 4.0)
if 1.0 < x < 3.0:
y = 1.0
else:
y = -1.0
return y
class LineShading(inkex.EffectExtension):
def add_arguments(self, pars):
pars.add_argument('--palette', help='Choose the colors...')
pars.add_argument("--waveform", help="Select the shape of the curve")
pars.add_argument("--num_lines", type=int, help="Number of lines")
pars.add_argument("--min_period", type=float, help="Minimum period (corresponds to black pixels)")
pars.add_argument("--max_period", type=float, help="Maximum period (corresponds to white pixels)")
pars.add_argument("--min_amplitude", type=float, help="Minimum amplitude (corresponds to white pixels)")
pars.add_argument("--max_amplitude", type=float, help="Maximum amplitude (corresponds to black pixels)")
pars.add_argument("--gamma", type=float, help="Maximum amplitude (corresponds to black pixels)")
pars.add_argument("--line_width", type=float, help="Line width")
pars.add_argument("--units", help="Units for line thickness")
pars.add_argument("--remove", type=inkex.Boolean, help="If True, source image is removed")
pars.add_argument("--active-tab", help="The selected UI-tab when OK was pressed")
def drawfunction(self, image_w, image_h, file):
reader = png.Reader(file)
w, h, pixels, metadata = reader.read_flat()
matrice = [[1.0 for i in range(w)]for j in range(h)]
if metadata['alpha']:
n = 4
else:
n = 3
#RGB convert to grayscale 0.21R + 0.72G + 0.07B
for y in range(h):
for x in range(w):
pixel_pos = (x + y * w)*n
p = 1.0 - (pixels[pixel_pos]*0.21 + pixels[(pixel_pos+1)]*0.72 + pixels[(pixel_pos+2)]*0.07)/255.0
matrice[y][x] = math.pow(p, 1.0/self.options.gamma)
points = []
step_y = image_h/h
step_x = image_w/(w-1)
min_amplitude = self.options.min_amplitude*step_y/2
max_amplitude = self.options.max_amplitude*step_y/2
min_period = self.options.min_period*step_y
max_period = self.options.max_period*step_y
min_frequency = 1.0/max_period
max_frequency = 1.0/min_period
#Sinusoidal wave (optimized)
if self.options.waveform == 'sin':
for y in range(h):
pi = math.pi
phase = 0.0
coord_x = 0.0
amplitude = 0.0
n_step = 0
x0 = 0.0
y0 = math.sin(phase)*(min_amplitude + (max_amplitude - min_amplitude)*matrice[y][x]) + (y+0.5)*step_y
points.append(['M',[x0, y0]])
for x in range(w):
period = min_period + (max_period - min_period)*(1-matrice[y][x])
#period = 1.0/(min_frequency + (max_frequency - min_frequency)*(matrice[y][x]))
d_phase = 2.0*pi/period*step_x
#calculate y
if phase > 2.0*pi:
if n_step > 0:
x3 = coord_x
y3 = -amplitude/n_step + (y+0.5)*step_y
x2 = x3 - (x3-x0)*0.32
y2 = y3
x1 = x0 + (x3-x0)*0.34
y1 = y0
x0 = x3
y0 = y3
points.append(['C',[x1, y1, x2, y2, x3, y3]])
n_step = 0
amplitude = 0
elif phase < pi < (phase + d_phase):
if n_step > 0:
x3 = coord_x
y3 = amplitude/n_step + (y+0.5)*step_y
x2 = x3 - (x3-x0)*0.34
y2 = y3
x1 = x0 + (x3-x0)*0.32
y1 = y0
x0 = x3
y0 = y3
points.append(['C',[x1, y1, x2, y2, x3, y3]])
n_step = 0
amplitude = 0
phase = math.fmod(phase, 2.0*pi)
#calculate x
if phase < 0.5*pi < (phase + d_phase):
coord_x = (x - (phase - 0.5*pi)/d_phase)*step_x
elif phase < 1.5*pi < (phase + d_phase):
coord_x = (x - (phase - 1.5*pi)/d_phase)*step_x
phase += d_phase
amplitude += (min_amplitude + (max_amplitude - min_amplitude)*matrice[y][x])
n_step += 1
#add last point
if n_step > 0:
phase = math.fmod(phase, 2.0*pi)
if (0 < phase < 0.5*pi) or (pi < phase < 1.5*pi):
x3 = (w-1)*step_x
y3 = amplitude*math.sin(phase)/n_step + (y+0.5)*step_y
x2 = x3
y2 = y3
x1 = x0 + (x3-x0)*0.33
y1 = y0
points.append(['C',[x1, y1, x2, y2, x3, y3]])
else:
if coord_x > (w-1)*step_x:
coord_x = (w-1)*step_x
x3 = coord_x
y3 = math.copysign( amplitude , math.sin(phase))/n_step + (y+0.5)*step_y
x2 = x3 - (x3-x0)*0.32
y2 = y3
x1 = x0 + (x3-x0)*0.34
y1 = y0
points.append(['C',[x1, y1, x2, y2, x3, y3]])
if coord_x < (w-1)*step_x:
x0 = x3
y0 = y3
x3 = (w-1)*step_x
y3 = amplitude*math.sin(phase)/n_step + (y+0.5)*step_y
x2 = x3
y2 = y3
x1 = x0 + (x3-x0)*0.33
y1 = y0
points.append(['C',[x1, y1, x2, y2, x3, y3]])
#Sinusoidal wave (Brute-force)
elif self.options.waveform == 'sin_b':
pi2 = math.pi*2.0
for y in range(h):
phase = - pi2/4.0
for x in range(w):
period = min_period + (max_period - min_period)*(1-matrice[y][x])
amplitude = min_amplitude + (max_amplitude - min_amplitude)*matrice[y][x]
phase += pi2*step_x/period
phase = math.fmod(phase, pi2)
if x == 0:
points.append(['M',[x*step_x, amplitude*math.sin(phase) + (y+0.5)*step_y]])
else:
points.append(['L',[x*step_x, amplitude*math.sin(phase) + (y+0.5)*step_y]])
#Saw wave
elif self.options.waveform == 'saw':
for y in range(h):
phase = 0.0
coord_x = 0.0
amplitude = 0.0
n_step = 0.0
for x in range(w):
period = min_period + (max_period - min_period)*(1-matrice[y][x])
#period = 1.0/(min_frequency + (max_frequency - min_frequency)*(matrice[y][x]))
d_phase = 4.0/period*step_x
if phase > 4.0:
coord_x = (x - (phase - 4.0)/d_phase)*step_x
elif phase < 2.0 < (phase + d_phase):
coord_x = (x - (phase - 2.0)/d_phase)*step_x
phase = math.fmod(phase, 4.0)
if (phase < 1.0 < (phase + d_phase)) or (phase < 3.0 < (phase + d_phase)):
if n_step > 0:
if coord_x == 0.0:
points.append(['M',[coord_x, amplitude*square(phase - 1.0)/n_step + (y+0.5)*step_y]])
else:
points.append(['L',[coord_x, amplitude*square(phase - 1.0)/n_step + (y+0.5)*step_y]])
n_step = 0
amplitude = 0
phase += d_phase
n_step += 1.0
amplitude += (min_amplitude + (max_amplitude - min_amplitude)*matrice[y][x])
if n_step > 0:
points.append(['L',[(w-1)*step_x, amplitude*saw(phase - 1.0)/n_step + (y+0.5)*step_y]])
#Square wave
else:
for y in range(h):
phase = 0.0
coord_x = 0.0
amplitude = 0.0
n_step = 0
for x in range(w):
period = min_period + (max_period - min_period)*(1-matrice[y][x])
#period = 1.0/(min_frequency + (max_frequency - min_frequency)*(matrice[y][x]))
d_phase = 4.0/period*step_x
if phase > 4.0:
coord_x = (x - (phase - 4.0)/d_phase)*step_x
elif phase < 2.0 < (phase + d_phase):
coord_x = (x - (phase - 2.0)/d_phase)*step_x
phase = math.fmod(phase, 4.0)
if phase < 1.0 < (phase + d_phase):
if n_step > 0:
if coord_x == 0.0:
points.append(['M',[coord_x, amplitude/n_step + (y+0.5)*step_y]])
else:
points.append(['L',[coord_x, -amplitude/n_step + (y+0.5)*step_y]])
points.append(['L',[coord_x, amplitude/n_step + (y+0.5)*step_y]])
n_step = 0
amplitude = 0
elif phase < 3.0 < (phase + d_phase):
if n_step > 0:
if coord_x == 0.0:
points.append(['M',[coord_x, -amplitude/n_step + (y+0.5)*step_y]])
else:
points.append(['L',[coord_x, amplitude/n_step + (y+0.5)*step_y]])
points.append(['L',[coord_x, -amplitude/n_step + (y+0.5)*step_y]])
n_step = 0
amplitude = 0
phase += d_phase
n_step += 1
amplitude += (min_amplitude + (max_amplitude - min_amplitude)*matrice[y][x])
if n_step > 0:
if 3.0 > phase > 1.0:
points.append(['L',[(w-1)*step_x, amplitude/n_step + (y+0.5)*step_y]])
else:
points.append(['L',[(w-1)*step_x, -amplitude/n_step + (y+0.5)*step_y]])
return points
def draw_path(self, node, file):
newpath = etree.Element(inkex.addNS('path','svg'))
line_width = self.options.line_width
units = self.options.units
s = {'stroke': '#000000', 'fill': 'none', 'stroke-linejoin': 'round', 'stroke-linecap': 'round', 'stroke-width': str(self.svg.unittouu(str(line_width) + units))}
newpath.set('style', str(inkex.Style(s)))
x = node.get('x')
y = node.get('y')
if x is None:
x = "0"
if y is None:
y = "0"
t = 'translate('+ x +','+ y +')'
newpath.set('transform', t)
image_w = float(node.get('width'))
image_h = float(node.get('height'))
newpath.set('d', str(Path(self.drawfunction(image_w, image_h, file))))
newpath.set('title', 'Line_Shading')
node.getparent().append(newpath)
newpath.set('x', x)
def export_png(self, node, file):
image_w = float(node.get('width'))
image_h = float(node.get('height'))
min_period = self.options.min_period
max_period = self.options.min_period
poinnt_per_min_period = 8.0
current_file = self.options.input_file
h_png = str(self.options.num_lines)
if min_period < max_period:
w_png = str(round(poinnt_per_min_period*image_w*float(h_png)/min_period/image_h))
else:
w_png = str(round(poinnt_per_min_period*image_w*float(h_png)/max_period/image_h))
id = node.get('id')
cmd = "inkscape " + current_file + " --export-type=\"png\" --export-filename=" + file + " --actions=\"export-width:"+w_png+";export-height:"+h_png+";export-background:rgb(255,255,255);export-background-opacity:255;export-id:"+id+"\""
#inkex.errormsg(cmd)
proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
#inkex.utils.debug(cmd)
#inkex.utils.debug(proc.communicate())
#sys.exit(0)
#return_code = proc.wait()
#sys.exit(0)
f = proc.stdout
err = proc.stderr
f.close()
err.close()
proc.wait()
#inkex.errormsg(proc.stdout.read())
def effect(self):
image_selected_flag = False
for id, node in self.svg.selected.items():
if node.tag == inkex.addNS('image','svg'):
image_selected_flag = True
tmp_dir = tempfile.mkdtemp()
png_temp_file = os.path.join(tmp_dir, "LineShading.png")
self.export_png(node, png_temp_file)
self.draw_path(node, png_temp_file)
shutil.rmtree(tmp_dir)
if self.options.remove:
node.delete()
return
if not image_selected_flag:
inkex.errormsg("Please select an image")
if __name__ == '__main__':
LineShading().run()