from __future__ import (absolute_import, division, print_function,
unicode_literals)
import os.path
from PIL import Image, ImageDraw, ImageFont
__here__ = os.path.dirname(__file__)
font_path = os.path.join(__here__, 'data', 'Arial.ttf')
[docs]class ImageRenderer(object):
# TODO:
# - Add a "drawndown only" option
# - Add a default tag (like a small delta symbol) to signal the initial
# shuttle direction
# - Add option to render the backside of the fabric
# - Add option to render a bar graph of the thread crossings along the
# sides
# - Add option to render 'stats table'
# - Number of warp threads
# - Number of weft threads
# - Number of harnesses/shafts
# - Number of treadles
# - Warp unit size / reps
# - Weft unit size / reps
# - Longest warp float
# - Longest weft float
# - Selvedge continuity
# - Add option to rotate orientation
# - Add option to render selvedge continuity
# - Add option to render inset "scale view" rendering of fabric
# - Add option to change thread spacing
# - Support variable thickness threads
# - Add option to render heddle count on each shaft
def __init__(self, draft, liftplan=None, margin_pixels=20, scale=10,
foreground=(127, 127, 127), background=(255, 255, 255),
markers=(0, 0, 0), numbering=(200, 0, 0)):
self.draft = draft
self.liftplan = liftplan
self.margin_pixels = margin_pixels
self.pixels_per_square = scale
self.background = background
self.foreground = foreground
self.markers = markers
self.numbering = numbering
self.font_size = int(round(scale * 1.2))
self.font = ImageFont.truetype(font_path, self.font_size)
[docs] def pad_image(self, im):
w, h = im.size
desired_w = w + (self.margin_pixels * 2)
desired_h = h + (self.margin_pixels * 2)
new = Image.new('RGB', (desired_w, desired_h), self.background)
new.paste(im, (self.margin_pixels, self.margin_pixels))
return new
[docs] def make_pil_image(self):
width_squares = len(self.draft.warp) + 6
if self.liftplan or self.draft.liftplan:
width_squares += len(self.draft.shafts)
else:
width_squares += len(self.draft.treadles)
height_squares = len(self.draft.weft) + 6 + len(self.draft.shafts)
# XXX Not totally sure why the +1 is needed here, but otherwise the
# contents overflows the canvas
width = (width_squares * self.pixels_per_square) + 1
height = (height_squares * self.pixels_per_square) + 1
im = Image.new('RGB', (width, height), self.background)
draw = ImageDraw.Draw(im)
self.paint_warp(draw)
self.paint_threading(draw)
self.paint_weft(draw)
if self.liftplan or self.draft.liftplan:
self.paint_liftplan(draw)
else:
self.paint_tieup(draw)
self.paint_treadling(draw)
self.paint_drawdown(draw)
self.paint_start_indicator(draw)
del draw
im = self.pad_image(im)
return im
[docs] def paint_start_indicator(self, draw):
endy = ((len(self.draft.shafts) + 6) * self.pixels_per_square) - 1
starty = (endy - (self.pixels_per_square // 2))
if self.draft.start_at_lowest_thread:
# right side
endx = len(self.draft.warp) * self.pixels_per_square
startx = endx - self.pixels_per_square
else:
# left side
startx = 0
endx = self.pixels_per_square
vertices = [
(startx, starty),
(endx, starty),
(startx + (self.pixels_per_square // 2), endy),
]
draw.polygon(vertices, fill=self.markers)
[docs] def paint_warp(self, draw):
starty = 0
endy = self.pixels_per_square
for ii, thread in enumerate(self.draft.warp):
# paint box, outlined with foreground color, filled with thread
# color
startx = self.pixels_per_square * ii
endx = startx + self.pixels_per_square
draw.rectangle((startx, starty, endx, endy),
outline=self.foreground,
fill=thread.color.rgb)
[docs] def paint_fill_marker(self, draw, box):
startx, starty, endx, endy = box
draw.rectangle((startx + 2, starty + 2, endx - 2, endy - 2),
fill=self.markers)
[docs] def paint_threading(self, draw):
num_threads = len(self.draft.warp)
num_shafts = len(self.draft.shafts)
for ii, thread in enumerate(self.draft.warp):
startx = (num_threads - ii - 1) * self.pixels_per_square
endx = startx + self.pixels_per_square
for jj, shaft in enumerate(self.draft.shafts):
starty = (4 + (num_shafts - jj)) * self.pixels_per_square
endy = starty + self.pixels_per_square
draw.rectangle((startx, starty, endx, endy),
outline=self.foreground)
if shaft == thread.shaft:
# draw threading marker
self.paint_fill_marker(draw, (startx, starty, endx, endy))
# paint the number if it's a multiple of 4
thread_no = ii + 1
if ((thread_no != num_threads) and
(thread_no != 0) and
(thread_no % 4 == 0)):
# draw line
startx = endx = (num_threads - ii - 1) * self.pixels_per_square
starty = 3 * self.pixels_per_square
endy = (5 * self.pixels_per_square) - 1
draw.line((startx, starty, endx, endy),
fill=self.numbering)
# draw text
draw.text((startx + 2, starty + 2),
str(thread_no),
font=self.font,
fill=self.numbering)
[docs] def paint_weft(self, draw):
offsety = (6 + len(self.draft.shafts)) * self.pixels_per_square
startx_squares = len(self.draft.warp) + 5
if self.liftplan or self.draft.liftplan:
startx_squares += len(self.draft.shafts)
else:
startx_squares += len(self.draft.treadles)
startx = startx_squares * self.pixels_per_square
endx = startx + self.pixels_per_square
for ii, thread in enumerate(self.draft.weft):
# paint box, outlined with foreground color, filled with thread
# color
starty = (self.pixels_per_square * ii) + offsety
endy = starty + self.pixels_per_square
draw.rectangle((startx, starty, endx, endy),
outline=self.foreground,
fill=thread.color.rgb)
[docs] def paint_liftplan(self, draw):
num_threads = len(self.draft.weft)
offsetx = (1 + len(self.draft.warp)) * self.pixels_per_square
offsety = (6 + len(self.draft.shafts)) * self.pixels_per_square
for ii, thread in enumerate(self.draft.weft):
starty = (ii * self.pixels_per_square) + offsety
endy = starty + self.pixels_per_square
for jj, shaft in enumerate(self.draft.shafts):
startx = (jj * self.pixels_per_square) + offsetx
endx = startx + self.pixels_per_square
draw.rectangle((startx, starty, endx, endy),
outline=self.foreground)
if shaft in thread.connected_shafts:
# draw liftplan marker
self.paint_fill_marker(draw, (startx, starty, endx, endy))
# paint the number if it's a multiple of 4
thread_no = ii + 1
if ((thread_no != num_threads) and
(thread_no != 0) and
(thread_no % 4 == 0)):
# draw line
startx = endx
starty = endy
endx = startx + (2 * self.pixels_per_square)
endy = starty
draw.line((startx, starty, endx, endy),
fill=self.numbering)
# draw text
draw.text((startx + 2, starty - 2 - self.font_size),
str(thread_no),
font=self.font,
fill=self.numbering)
[docs] def paint_tieup(self, draw):
offsetx = (1 + len(self.draft.warp)) * self.pixels_per_square
offsety = 5 * self.pixels_per_square
num_treadles = len(self.draft.treadles)
num_shafts = len(self.draft.shafts)
for ii, treadle in enumerate(self.draft.treadles):
startx = (ii * self.pixels_per_square) + offsetx
endx = startx + self.pixels_per_square
treadle_no = ii + 1
for jj, shaft in enumerate(self.draft.shafts):
starty = (((num_shafts - jj - 1) * self.pixels_per_square) +
offsety)
endy = starty + self.pixels_per_square
draw.rectangle((startx, starty, endx, endy),
outline=self.foreground)
if shaft in treadle.shafts:
self.paint_fill_marker(draw, (startx, starty, endx, endy))
# on the last treadle, paint the shaft markers
if treadle_no == num_treadles:
shaft_no = jj + 1
if (shaft_no != 0) and (shaft_no % 4 == 0):
# draw line
line_startx = endx
line_endx = line_startx + (2 * self.pixels_per_square)
line_starty = line_endy = starty
draw.line((line_startx, line_starty,
line_endx, line_endy),
fill=self.numbering)
draw.text((line_startx + 2, line_starty + 2),
str(shaft_no),
font=self.font,
fill=self.numbering)
# paint the number if it's a multiple of 4 and not the first one
if (treadle_no != 0) and (treadle_no % 4 == 0):
# draw line
startx = endx = (treadle_no * self.pixels_per_square) + offsetx
starty = 3 * self.pixels_per_square
endy = (5 * self.pixels_per_square) - 1
draw.line((startx, starty, endx, endy),
fill=self.numbering)
# draw text on left side, right justified
textw, texth = draw.textsize(str(treadle_no), font=self.font)
draw.text((startx - textw - 2, starty + 2),
str(treadle_no),
font=self.font,
fill=self.numbering)
[docs] def paint_treadling(self, draw):
num_threads = len(self.draft.weft)
offsetx = (1 + len(self.draft.warp)) * self.pixels_per_square
offsety = (6 + len(self.draft.shafts)) * self.pixels_per_square
for ii, thread in enumerate(self.draft.weft):
starty = (ii * self.pixels_per_square) + offsety
endy = starty + self.pixels_per_square
for jj, treadle in enumerate(self.draft.treadles):
startx = (jj * self.pixels_per_square) + offsetx
endx = startx + self.pixels_per_square
draw.rectangle((startx, starty, endx, endy),
outline=self.foreground)
if treadle in thread.treadles:
# draw treadling marker
self.paint_fill_marker(draw, (startx, starty, endx, endy))
# paint the number if it's a multiple of 4
thread_no = ii + 1
if ((thread_no != num_threads) and
(thread_no != 0) and
(thread_no % 4 == 0)):
# draw line
startx = endx
starty = endy
endx = startx + (2 * self.pixels_per_square)
endy = starty
draw.line((startx, starty, endx, endy),
fill=self.numbering)
# draw text
draw.text((startx + 2, starty - 2 - self.font_size),
str(thread_no),
font=self.font,
fill=self.numbering)
[docs] def paint_drawdown(self, draw):
offsety = (6 + len(self.draft.shafts)) * self.pixels_per_square
floats = self.draft.compute_floats()
for start, end, visible, length, thread in floats:
if visible:
startx = start[0] * self.pixels_per_square
starty = (start[1] * self.pixels_per_square) + offsety
endx = (end[0] + 1) * self.pixels_per_square
endy = ((end[1] + 1) * self.pixels_per_square) + offsety
draw.rectangle((startx, starty, endx, endy),
outline=self.foreground,
fill=thread.color.rgb)
[docs] def show(self):
im = self.make_pil_image()
im.show()
[docs] def save(self, filename):
im = self.make_pil_image()
im.save(filename)
svg_preamble = '<?xml version="1.0" encoding="utf-8" standalone="no"?>'
svg_header = '''<svg width="{width}" height="{height}"
viewBox="0 0 {width} {height}"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">'''
[docs]class TagGenerator(object):
def __getattr__(self, name):
def tag(*children, **attrs):
inner = ''.join(children)
if attrs:
attrs = ' '.join(['%s="%s"' % (key.replace('_', '-'), val)
for key, val in attrs.items()])
return '<%s %s>%s</%s>' % (name, attrs, inner, name)
else:
return '<%s>%s</%s>' % (name, inner, name)
return tag
SVG = TagGenerator()
[docs]class SVGRenderer(object):
def __init__(self, draft, liftplan=None, scale=10,
foreground='#7f7f7f', background='#ffffff',
markers='#000000', numbering='#c80000'):
self.draft = draft
self.liftplan = liftplan
self.scale = scale
self.background = background
self.foreground = foreground
self.markers = markers
self.numbering = numbering
self.font_family = 'Arial, sans-serif'
self.font_size = 12
[docs] def make_svg_doc(self):
width_squares = len(self.draft.warp) + 6
if self.liftplan or self.draft.liftplan:
width_squares += len(self.draft.shafts)
else:
width_squares += len(self.draft.treadles)
height_squares = len(self.draft.weft) + 6 + len(self.draft.shafts)
width = width_squares * self.scale
height = height_squares * self.scale
doc = []
# Use a negative starting point so we don't have to offset everything
# in the drawing.
doc.append(svg_header.format(width=width, height=height))
self.write_metadata(doc)
self.paint_warp(doc)
self.paint_threading(doc)
self.paint_weft(doc)
if self.liftplan or self.draft.liftplan:
self.paint_liftplan(doc)
else:
self.paint_tieup(doc)
self.paint_treadling(doc)
self.paint_drawdown(doc)
doc.append('</svg>')
return '\n'.join(doc)
[docs] def paint_warp(self, doc):
starty = 0
grp = []
for ii, thread in enumerate(self.draft.warp):
# paint box, outlined with foreground color, filled with thread
# color
startx = self.scale * ii
grp.append(SVG.rect(
x=startx, y=starty,
width=self.scale, height=self.scale,
style='stroke:%s; fill:%s' % (self.foreground,
thread.color.css)))
doc.append(SVG.g(*grp))
[docs] def paint_weft(self, doc):
offsety = (6 + len(self.draft.shafts)) * self.scale
startx_squares = len(self.draft.warp) + 5
if self.liftplan or self.draft.liftplan:
startx_squares += len(self.draft.shafts)
else:
startx_squares += len(self.draft.treadles)
startx = startx_squares * self.scale
grp = []
for ii, thread in enumerate(self.draft.weft):
# paint box, outlined with foreground color, filled with thread
# color
starty = (self.scale * ii) + offsety
grp.append(SVG.rect(
x=startx, y=starty,
width=self.scale, height=self.scale,
style='stroke:%s; fill:%s' % (self.foreground,
thread.color.css)))
doc.append(SVG.g(*grp))
[docs] def paint_fill_marker(self, doc, box):
startx, starty, endx, endy = box
# XXX FIXME make box setback generated from scale fraction
assert self.scale > 8
doc.append(SVG.rect(
x=startx + 2,
y=starty + 2,
width=self.scale - 4,
height=self.scale - 4,
style='fill:%s' % self.markers))
[docs] def paint_threading(self, doc):
num_threads = len(self.draft.warp)
num_shafts = len(self.draft.shafts)
grp = []
for ii, thread in enumerate(self.draft.warp):
startx = (num_threads - ii - 1) * self.scale
endx = startx + self.scale
for jj, shaft in enumerate(self.draft.shafts):
starty = (4 + (num_shafts - jj)) * self.scale
endy = starty + self.scale
grp.append(SVG.rect(
x=startx, y=starty,
width=self.scale, height=self.scale,
style='stroke:%s; fill:%s' % (self.foreground,
self.background)))
if shaft == thread.shaft:
# draw threading marker
self.paint_fill_marker(grp, (startx, starty, endx, endy))
# paint the number if it's a multiple of 4
thread_no = ii + 1
if ((thread_no != num_threads) and
(thread_no != 0) and
(thread_no % 4 == 0)):
# draw line
startx = endx = (num_threads - ii - 1) * self.scale
starty = 3 * self.scale
endy = (5 * self.scale) - 1
grp.append(SVG.line(
x1=startx,
y1=starty,
x2=endx,
y2=endy,
style='stroke:%s' % self.numbering))
# draw text
grp.append(SVG.text(
str(thread_no),
x=(startx + 3),
y=(starty + self.font_size),
style='font-family:%s; font-size:%s; fill:%s' % (
self.font_family,
self.font_size,
self.numbering)))
doc.append(SVG.g(*grp))
[docs] def paint_liftplan(self, doc):
num_threads = len(self.draft.weft)
offsetx = (1 + len(self.draft.warp)) * self.scale
offsety = (6 + len(self.draft.shafts)) * self.scale
grp = []
for ii, thread in enumerate(self.draft.weft):
starty = (ii * self.scale) + offsety
endy = starty + self.scale
for jj, shaft in enumerate(self.draft.shafts):
startx = (jj * self.scale) + offsetx
endx = startx + self.scale
grp.append(SVG.rect(
x=startx,
y=starty,
width=self.scale,
height=self.scale,
style='stroke:%s; fill:%s' % (self.foreground,
self.background)))
if shaft in thread.connected_shafts:
# draw liftplan marker
self.paint_fill_marker(grp, (startx, starty, endx, endy))
# paint the number if it's a multiple of 4
thread_no = ii + 1
if ((thread_no != num_threads) and
(thread_no != 0) and
(thread_no % 4 == 0)):
# draw line
startx = endx
starty = endy
endx = startx + (2 * self.scale)
endy = starty
grp.append(SVG.line(
x1=startx,
y1=starty,
x2=endx,
y2=endy,
style='stroke:%s' % self.numbering))
# draw text
grp.append(SVG.text(
str(thread_no),
x=(startx + 3),
y=(starty - 4),
style='font-family:%s; font-size:%s; fill:%s' % (
self.font_family,
self.font_size,
self.numbering)))
doc.append(SVG.g(*grp))
[docs] def paint_tieup(self, doc):
offsetx = (1 + len(self.draft.warp)) * self.scale
offsety = 5 * self.scale
num_treadles = len(self.draft.treadles)
num_shafts = len(self.draft.shafts)
grp = []
for ii, treadle in enumerate(self.draft.treadles):
startx = (ii * self.scale) + offsetx
endx = startx + self.scale
treadle_no = ii + 1
for jj, shaft in enumerate(self.draft.shafts):
starty = (((num_shafts - jj - 1) * self.scale) +
offsety)
endy = starty + self.scale
grp.append(SVG.rect(
x=startx,
y=starty,
width=self.scale,
height=self.scale,
style='stroke:%s; fill:%s' % (self.foreground,
self.background)))
if shaft in treadle.shafts:
self.paint_fill_marker(grp, (startx, starty, endx, endy))
# on the last treadle, paint the shaft markers
if treadle_no == num_treadles:
shaft_no = jj + 1
if (shaft_no != 0) and (shaft_no % 4 == 0):
# draw line
line_startx = endx
line_endx = line_startx + (2 * self.scale)
line_starty = line_endy = starty
grp.append(SVG.line(
x1=line_startx,
y1=line_starty,
x2=line_endx,
y2=line_endy,
style='stroke:%s' % self.numbering))
grp.append(SVG.text(
str(shaft_no),
x=(line_startx + 3),
y=(line_starty + 2 + self.font_size),
style='font-family:%s; font-size:%s; fill:%s' % (
self.font_family,
self.font_size,
self.numbering)))
# paint the number if it's a multiple of 4 and not the first one
if (treadle_no != 0) and (treadle_no % 4 == 0):
# draw line
startx = endx = (treadle_no * self.scale) + offsetx
starty = 3 * self.scale
endy = (5 * self.scale) - 1
grp.append(SVG.line(
x1=startx,
y1=starty,
x2=endx,
y2=endy,
style='stroke:%s' % self.numbering))
# draw text on left side, right justified
grp.append(SVG.text(
str(treadle_no),
x=(startx - 3),
y=(starty + self.font_size),
text_anchor='end',
style='font-family:%s; font-size:%s; fill:%s' % (
self.font_family,
self.font_size,
self.numbering)))
doc.append(SVG.g(*grp))
[docs] def paint_treadling(self, doc):
num_threads = len(self.draft.weft)
offsetx = (1 + len(self.draft.warp)) * self.scale
offsety = (6 + len(self.draft.shafts)) * self.scale
grp = []
for ii, thread in enumerate(self.draft.weft):
starty = (ii * self.scale) + offsety
endy = starty + self.scale
for jj, treadle in enumerate(self.draft.treadles):
startx = (jj * self.scale) + offsetx
endx = startx + self.scale
grp.append(SVG.rect(
x=startx,
y=starty,
width=self.scale,
height=self.scale,
style='stroke:%s; fill:%s' % (self.foreground,
self.background)))
if treadle in thread.treadles:
# draw treadling marker
self.paint_fill_marker(grp, (startx, starty, endx, endy))
# paint the number if it's a multiple of 4
thread_no = ii + 1
if ((thread_no != num_threads) and
(thread_no != 0) and
(thread_no % 4 == 0)):
# draw line
startx = endx
starty = endy
endx = startx + (2 * self.scale)
endy = starty
grp.append(SVG.line(
x1=startx,
y1=starty,
x2=endx,
y2=endy,
style='stroke:%s' % self.numbering))
# draw text
grp.append(SVG.text(
str(thread_no),
x=(startx + 3),
y=(starty - 4),
style='font-family:%s; font-size:%s; fill:%s' % (
self.font_family,
self.font_size,
self.numbering)))
doc.append(SVG.g(*grp))
[docs] def paint_drawdown(self, doc):
offsety = (6 + len(self.draft.shafts)) * self.scale
floats = self.draft.compute_floats()
grp = []
for start, end, visible, length, thread in floats:
if visible:
startx = start[0] * self.scale
starty = (start[1] * self.scale) + offsety
endx = (end[0] + 1) * self.scale
endy = ((end[1] + 1) * self.scale) + offsety
width = endx - startx
height = endy - starty
grp.append(SVG.rect(
x=startx,
y=starty,
width=width,
height=height,
style='stroke:%s; fill:%s' % (self.foreground,
thread.color.css)))
doc.append(SVG.g(*grp))
[docs] def render_to_string(self):
return self.make_svg_doc()
[docs] def save(self, filename):
s = svg_preamble + '\n' + self.make_svg_doc()
with open(filename, 'w') as f:
f.write(s)