You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
596 lines
17 KiB
596 lines
17 KiB
from flask import Flask, Response, request, render_template
|
|
import subprocess
|
|
import os
|
|
import re
|
|
import sys
|
|
import tempfile
|
|
import io
|
|
import requests
|
|
|
|
app = Flask(__name__)
|
|
|
|
title = 'Cobbled paths'
|
|
|
|
fonts_directory = 'static/db/'
|
|
possible_extensions = [".flf"]
|
|
etherpad = 'https://pad.constantvzw.org/p/'
|
|
prefix = 'cobbled-pad-'
|
|
|
|
# VARIABLES 4 CATALOGUE
|
|
# ------------------------------
|
|
|
|
collection = {
|
|
'stroke': { 'ascii': ' | ' , 'fonts': [] },
|
|
'block': { 'ascii': '|_|' , 'fonts': [] },
|
|
'open': { 'ascii': '/ /' , 'fonts': [] },
|
|
'pattern': { 'ascii': '(((' , 'fonts': [] },
|
|
'fill': { 'ascii': '###' , 'fonts': [] },
|
|
'3d': { 'ascii': '_|/' , 'fonts': [] },
|
|
|
|
# 'directions': { 'ascii': '_/', 'fonts': [] },
|
|
# 'frame': { 'ascii': '_/', 'fonts': [] },
|
|
# 'code': { 'ascii': '_/', 'fonts': [] },
|
|
}
|
|
databases = {
|
|
'default': 'fonts made by the figlet developpers and given with the program, early 1993',
|
|
'contributed': 'fonts made by figlet amateurs and submitted to the official figlet ftp, from before 1993 to 2005',
|
|
'jave': 'figlet font library of JavE (a free Ascii drawing Editor)',
|
|
}
|
|
|
|
# VARIABLES 4 REGEX
|
|
# ------------------------------
|
|
|
|
# all the character that svgbob understand
|
|
spec = [".", ",", "’", "'", "`", "+", "*", "o", "O", "V", "\\-", "|", "~", "_", ":", "!", "<", ">", "v", "^", "/", "\\\\", '\\”', '\\"', "(", ")", "=", "#"]
|
|
r_spec = "".join(spec)
|
|
r_nspec = "[^" + r_spec + "\€\$\s]"
|
|
|
|
# autofix regex
|
|
autofix = [
|
|
|
|
# every arrowshead into lines
|
|
[re.compile("[<{]"), "("],
|
|
[re.compile("[>}]"), ")"],
|
|
[re.compile("[vV]"), "-"],
|
|
[re.compile("[\\^]"), "-"],
|
|
|
|
[";", ":"],
|
|
["7", "/"],
|
|
|
|
[re.compile("[1Tlj\\[\\]]"), "|"],
|
|
[re.compile("[Y]"), "+"],
|
|
|
|
# every not in the spec --> block
|
|
[re.compile(r_nspec), "#"],
|
|
]
|
|
|
|
# FUNCTIONS
|
|
# ------------------------------
|
|
|
|
def most_common(lst):
|
|
return max(set(lst), key=lst.count)
|
|
|
|
def text2figlet(text, figfont):
|
|
print('--- FIGLET SUBPROCESS')
|
|
figlet = subprocess.run(["figlet", text, "-f", figfont, "-w", "160"], stdout = subprocess.PIPE, stderr = subprocess.PIPE, text=True)
|
|
print(figlet.stdout)
|
|
|
|
if figlet.returncode == 0:
|
|
answer = (True, figlet.stdout)
|
|
else:
|
|
answer = (False, figlet.stderr)
|
|
|
|
return answer
|
|
|
|
def image2ascii(img, chars, width):
|
|
print('--- JP2A SUBPROCESS')
|
|
jp2a = subprocess.run([
|
|
"jp2a", img, "--background=light", "--width="+width, "--chars="+chars],
|
|
stdout = subprocess.PIPE, stderr = subprocess.PIPE, text=True)
|
|
|
|
# "--background", "light",
|
|
# "--width", width,
|
|
# "--chars", chars
|
|
|
|
print(jp2a)
|
|
|
|
if jp2a.returncode == 0:
|
|
answer = (True, jp2a.stdout)
|
|
else:
|
|
answer = (False, jp2a.stderr)
|
|
|
|
return answer
|
|
|
|
def ascii2svg(ascii, weight='2', scale='1'):
|
|
if ascii:
|
|
print('--- SVGBOB SUBPROCESS')
|
|
svgbob = subprocess.run([
|
|
"svgbob_cli",
|
|
'--stroke-width', weight,
|
|
'--scale', scale],
|
|
input = ascii,
|
|
stdout = subprocess.PIPE, text=True)
|
|
return svgbob.stdout
|
|
else:
|
|
return "ERROR: etherpad request failed"
|
|
|
|
def styleSVG(svg):
|
|
with open('static/css/svg.css', 'r') as svg_css:
|
|
css = svg_css.read()
|
|
svg = svg.replace('<style>', '<style>' + css)
|
|
return svg
|
|
|
|
def simplifySVG(svg):
|
|
|
|
# store as a temporary file
|
|
(svg_file, svg_path) = tempfile.mkstemp('.svg')
|
|
(svg_file_cleaned, svg_path_cleaned) = tempfile.mkstemp('.svg')
|
|
|
|
with open(svg_file, 'w') as svg_handle:
|
|
svg_handle.write(svg)
|
|
|
|
vpype = subprocess.run([
|
|
"vpype",
|
|
"read",
|
|
"--single-layer", svg_path,
|
|
"linesimplify",
|
|
"-t", "0.05mm",
|
|
"linemerge",
|
|
"-t", "0.25mm",
|
|
"linesort",
|
|
"write",
|
|
svg_path_cleaned
|
|
])
|
|
|
|
response = ''
|
|
with open(svg_file_cleaned, 'r') as svg_handle_cleaned:
|
|
response = svg_handle_cleaned.read()
|
|
|
|
# remove tmp file
|
|
os.remove(svg_path)
|
|
os.remove(svg_path_cleaned)
|
|
|
|
return response
|
|
|
|
def ascii_autofix(ascii):
|
|
print('--- REGEX AUTOFIX')
|
|
for regex, replace in autofix:
|
|
ascii = re.sub(regex, replace, ascii)
|
|
return ascii
|
|
|
|
def autofix_indication(ascii):
|
|
for regex, replace in autofix:
|
|
# the two markers have to not appear in any regex
|
|
ascii = re.sub(regex, "$" + replace + "€", ascii)
|
|
ascii = re.sub("[\$]", "<span class='fix'>", ascii)
|
|
ascii = re.sub("[\€]", "</span>", ascii)
|
|
return ascii
|
|
|
|
def get_pad(url):
|
|
# get pad content
|
|
print('--- ETHERPAD REQUEST')
|
|
print(url)
|
|
pad_export = requests.get(url + '/export/txt')
|
|
if pad_export.status_code == requests.codes.ok:
|
|
answer = (True, pad_export.text)
|
|
else:
|
|
answer = (False, "ERROR: etherpad request failed")
|
|
return answer
|
|
|
|
def make_figfont(ascii):
|
|
print('--- MAKE TEMP FIGFONT')
|
|
(figfont_file, figfont_path) = tempfile.mkstemp(suffix='.flf')
|
|
print(figfont_path)
|
|
with open(figfont_path, 'w') as figfont_file:
|
|
figfont_file.write(ascii)
|
|
return figfont_path
|
|
|
|
def parse_collection():
|
|
# walk in the figlet font directory
|
|
for root, dirs, files in os.walk(fonts_directory):
|
|
for name in files:
|
|
|
|
(basename, ext) = os.path.splitext(name)
|
|
if ext in possible_extensions:
|
|
|
|
figfont = os.path.join(root, name)
|
|
print(figfont)
|
|
|
|
# get font databases out of first folder
|
|
database = root.split('/')[-2]
|
|
# get font type out of last folder
|
|
type = root.split('/')[-1]
|
|
|
|
# only include selected types
|
|
if type in collection:
|
|
|
|
f = {}
|
|
f['name'] = name
|
|
f['database'] = database
|
|
f['path'] = figfont
|
|
|
|
# sort them by type
|
|
collection[type]['fonts'].append(f)
|
|
|
|
# make thumbnail
|
|
thumbnail_ascii = text2figlet("Abc", figfont)[1]
|
|
thumbnail_svg = styleSVG(ascii2svg(thumbnail_ascii, '2', '0.66'))
|
|
thumbnail_path = os.path.join(root, basename) + '.svg'
|
|
thumbnail_file = open(thumbnail_path, "w")
|
|
thumbnail_file.write(thumbnail_svg)
|
|
thumbnail_file.close()
|
|
f['thumbnail'] = '/' + thumbnail_path
|
|
|
|
|
|
|
|
# ROUTES
|
|
# ------------------------------
|
|
|
|
# _ _
|
|
# (_)_ __ __| | _____ __
|
|
# | | '_ \ / _` |/ _ \ \/ /
|
|
# | | | | | (_| | __/> <
|
|
# |_|_| |_|\__,_|\___/_/\_\
|
|
#
|
|
# PRESENT THE TOOL
|
|
|
|
@app.route("/")
|
|
def index():
|
|
|
|
return render_template(
|
|
'index.html',
|
|
title = title)
|
|
|
|
@app.route("/gallery.html")
|
|
def gallery():
|
|
|
|
return render_template(
|
|
'gallery.html',
|
|
title = title)
|
|
|
|
# _
|
|
# __| |_ __ __ ___ __
|
|
# / _` | '__/ _` \ \ /\ / /
|
|
# | (_| | | | (_| |\ V V /
|
|
# \__,_|_| \__,_| \_/\_/
|
|
#
|
|
# ETHERPAD 2 SVGBOB INTERFACE
|
|
# one iframe for the etherpad
|
|
# another iframe to dump the generated svg
|
|
|
|
@app.route("/draw.html")
|
|
def draw():
|
|
|
|
params = {
|
|
'pad': request.args.get('p') or 'index',
|
|
'weight': request.args.get('w') or '2',
|
|
}
|
|
params['pad-full'] = etherpad + prefix + params['pad']
|
|
|
|
return render_template(
|
|
'draw.html',
|
|
title = title,
|
|
params = params)
|
|
|
|
# this is the route of the iframe where the svg is generated and dumped
|
|
|
|
@app.route("/drawing/<id>")
|
|
def drawing(id):
|
|
|
|
params = {
|
|
'pad': id or 'default',
|
|
'weight': request.args.get('w') or '2',
|
|
}
|
|
params['pad-full'] = etherpad + prefix + params['pad']
|
|
|
|
pad_answer = get_pad(params['pad-full'])
|
|
if pad_answer[0]:
|
|
ascii = pad_answer[1]
|
|
svg = ascii2svg(ascii, params['weight'])
|
|
else:
|
|
svg = pad_answer[1]
|
|
|
|
return render_template(
|
|
'iframe/drawing.html',
|
|
title = title,
|
|
params = params,
|
|
svg = svg)
|
|
|
|
|
|
# __ _ _ _
|
|
# / _| ___ _ __ | |_ _ __ ___ __ _| | _(_)_ __ __ _
|
|
# | |_ / _ \| '_ \| __| | '_ ` _ \ / _` | |/ / | '_ \ / _` |
|
|
# | _| (_) | | | | |_ | | | | | | (_| | <| | | | | (_| |
|
|
# |_| \___/|_| |_|\__| |_| |_| |_|\__,_|_|\_\_|_| |_|\__, |
|
|
# |___/
|
|
#
|
|
# EDITING A FIGGONT ON A PAD TO THEN USE IT
|
|
|
|
@app.route("/font.html")
|
|
def font():
|
|
|
|
params = {
|
|
'text': request.args.get('t') or 'the quick brown fox jumps over the lazy dog',
|
|
'pad': request.args.get('p') or 'font_index',
|
|
'weight': request.args.get('w') or '2',
|
|
}
|
|
params['pad-full'] = etherpad + prefix + params['pad']
|
|
|
|
return render_template(
|
|
'font.html',
|
|
title = title,
|
|
params = params)
|
|
|
|
@app.route("/writing/<id>")
|
|
def writing(id):
|
|
|
|
params = {
|
|
'text': request.args.get('t') or 'the quick brown fox jumps over the lazy dog',
|
|
'pad': id or 'ascriipt',
|
|
'weight': request.args.get('w') or '2',
|
|
}
|
|
|
|
if '.flf' in params['pad']:
|
|
# it's not a pad it's a local figfont file
|
|
figfont = '/'.join(params['pad'].split('$'))
|
|
figlet_answer = text2figlet(params['text'], figfont)
|
|
|
|
if figlet_answer[0]:
|
|
ascii = figlet_answer[1]
|
|
svg = ascii2svg(figlet_answer[1], params['weight'])
|
|
else:
|
|
ascii = svg = figlet_answer[1]
|
|
|
|
else:
|
|
# we compile a figfont from a pad
|
|
params['pad-full'] = etherpad + prefix + params['pad']
|
|
pad_answer = get_pad(params['pad-full'])
|
|
|
|
if pad_answer[0]:
|
|
ascii = pad_answer[1]
|
|
figfont = make_figfont(ascii)
|
|
|
|
figlet_answer = text2figlet(params['text'], figfont)
|
|
if figlet_answer[0]:
|
|
ascii = figlet_answer[1]
|
|
svg = ascii2svg(figlet_answer[1], params['weight'])
|
|
else:
|
|
ascii = svg = figlet_answer[1]
|
|
else:
|
|
ascii = svg = pad_answer[1]
|
|
|
|
return render_template(
|
|
'iframe/double.html',
|
|
title = title,
|
|
params = params,
|
|
ascii = ascii,
|
|
svg = svg)
|
|
|
|
# _ _
|
|
# ___ __ _| |_ __ _| | ___ __ _ _ _ ___
|
|
# / __/ _` | __/ _` | |/ _ \ / _` | | | |/ _ \
|
|
# | (_| (_| | || (_| | | (_) | (_| | |_| | __/
|
|
# \___\__,_|\__\__,_|_|\___/ \__, |\__,_|\___|
|
|
# |___/
|
|
#
|
|
# FIGLET 2 SVGBOB INTERACTIVE CATALOGUE
|
|
|
|
@app.route("/catalogue.html")
|
|
def catalogue():
|
|
|
|
# text and weight as get parameter
|
|
params = {
|
|
'text': request.args.get('t') or 'the quick brown fox jumps over the lazy dog',
|
|
'weight': request.args.get('w') or '2',
|
|
}
|
|
|
|
return render_template(
|
|
'catalogue.html',
|
|
title = title,
|
|
databases = databases,
|
|
collection = collection,
|
|
params = params)
|
|
|
|
@app.route("/specimen/<type>")
|
|
def specimen(type):
|
|
|
|
params = {
|
|
'text': request.args.get('t') or 'Plotter Station',
|
|
'weight': request.args.get('w') or '2',
|
|
}
|
|
|
|
total = ''
|
|
for figfont in collection[type]['fonts']:
|
|
|
|
figlet_answer = text2figlet(params['text'], figfont['path'])
|
|
if figlet_answer[0]:
|
|
total = total + figlet_answer[1] + '"' + figfont['name'] + '(' + figfont['database'] + ')' + '"\n\n'
|
|
|
|
|
|
svg = ascii2svg(total, params['weight'])
|
|
|
|
return render_template(
|
|
'drawing.html',
|
|
title = title,
|
|
params = params,
|
|
svg = svg)
|
|
|
|
|
|
# _
|
|
# (_)_ __ ___ __ _ __ _ ___
|
|
# | | '_ ` _ \ / _` |/ _` |/ _ \
|
|
# | | | | | | | (_| | (_| | __/
|
|
# |_|_| |_| |_|\__,_|\__, |\___|
|
|
# |___/
|
|
#
|
|
# jp2a to svgbob
|
|
|
|
upload_folder = os.path.join('static', 'upload')
|
|
app.config['UPLOAD'] = upload_folder
|
|
|
|
@app.route("/image.html", methods=['GET', 'POST'])
|
|
def image():
|
|
|
|
# ()/\|'-._=+
|
|
params = {
|
|
'weight': request.args.get('w') or '2',
|
|
'chars': request.args.get('c') or " _|+#",
|
|
'width': request.args.get('width') or "120"
|
|
}
|
|
|
|
if request.method == 'POST':
|
|
file = request.files['img']
|
|
filename = file.filename
|
|
file.save(os.path.join(app.config['UPLOAD'], filename))
|
|
img = os.path.join(app.config['UPLOAD'], filename)
|
|
|
|
return render_template('image.html',
|
|
title = title,
|
|
img = img,
|
|
filename = filename,
|
|
params = params)
|
|
|
|
return render_template(
|
|
'image.html',
|
|
title = title,
|
|
img = os.path.join(app.config['UPLOAD'], 'default.jpg'),
|
|
filename = 'default.jpg',
|
|
params = params)
|
|
|
|
|
|
@app.route("/drawing-image/<id>")
|
|
def drawing_image(id):
|
|
|
|
params = {
|
|
'filename': id or 'default.jpg',
|
|
'weight': request.args.get('w') or '2',
|
|
'chars': request.args.get('chars') or " _|+#",
|
|
'width': request.args.get('width') or "120",
|
|
}
|
|
|
|
if params['filename'] == 'default.jpg':
|
|
ascii = svg = ''
|
|
|
|
elif '.' in params['filename']:
|
|
img = os.path.join(app.config['UPLOAD'], params['filename'])
|
|
answer, ascii = image2ascii(img, params['chars'], params['width'])
|
|
if answer:
|
|
svg = ascii2svg(ascii, params['weight'])
|
|
else:
|
|
ascii = svg = answer
|
|
|
|
return render_template(
|
|
'iframe/double.html',
|
|
title = title,
|
|
params = params,
|
|
ascii = ascii,
|
|
svg = svg)
|
|
|
|
# _ _ _
|
|
# | |__ _ __ __ _| | _____ ___ __ ___ _ __| |_
|
|
# | '_ \| '_ \ / _` | | / _ \ \/ / '_ \ / _ \| '__| __|
|
|
# | | | | |_) | (_| | | | __/> <| |_) | (_) | | | |_
|
|
# |_| |_| .__/ \__, |_| \___/_/\_\ .__/ \___/|_| \__|
|
|
# |_| |___/ |_|
|
|
|
|
def resizeSVG (m):
|
|
width = int(m.group(1))
|
|
height = int(m.group(2))
|
|
|
|
viewbox = f'0 0 {width} {height}'
|
|
|
|
newHeight = 420
|
|
newWidth = (width/height) * newHeight
|
|
|
|
return f'<svg xmlns="http://www.w3.org/2000/svg" viewbox="{viewbox}" width="{newWidth}mm" height="{newHeight}mm" class="svgbob">'
|
|
|
|
@app.route('/svg/<id>')
|
|
def svg (id):
|
|
params = {
|
|
'pad': id or 'default',
|
|
'weight': request.args.get('w') or '2',
|
|
}
|
|
params['pad-full'] = etherpad + prefix + params['pad']
|
|
|
|
# get pad content
|
|
print(' getting ' + params['pad-full'])
|
|
pad_export = requests.get(params['pad-full'] + '/export/txt')
|
|
ascii = pad_export.text
|
|
|
|
# to SVG
|
|
svg = simplifySVG(ascii2svg(ascii, params['weight']))
|
|
# Remove background rect inserted by SVG Bob
|
|
svg = re.sub(r'\<rect class="backdrop" x="\d+" y="\d+" width="\d+" height="\d+">\<\/rect\>', '', svg, flags=re.M)
|
|
svg = re.sub(r'<svg xmlns="http://www.w3.org/2000/svg" width="(\d+)" height="(\d+)" class="svgbob">', resizeSVG,svg)
|
|
|
|
r = Response(svg, mimetype='application/svg')
|
|
r.headers.extend({
|
|
'Content-Disposition': f'attachment; filename="cobbled-paths-{id}.svg"'
|
|
})
|
|
|
|
return r
|
|
|
|
|
|
@app.route('/hpgl/<id>')
|
|
def hpgl (id):
|
|
params = {
|
|
'pad': id or 'default',
|
|
'weight': request.args.get('w') or '2',
|
|
}
|
|
params['pad-full'] = etherpad + prefix + params['pad']
|
|
|
|
# get pad content
|
|
print(' getting ' + params['pad-full'])
|
|
pad_export = requests.get(params['pad-full'] + '/export/txt')
|
|
ascii = pad_export.text
|
|
|
|
# to SVG
|
|
svg = ascii2svg(ascii, params['weight'])
|
|
# Remove background rect inserted by SVG Bob
|
|
svg = re.sub(r'\<rect class="backdrop" x="\d+" y="\d+" width="\d+" height="\d+">\<\/rect\>', '', svg, flags=re.M)
|
|
svg = re.sub(r'<svg xmlns="http://www.w3.org/2000/svg" width="(\d+)" height="(\d+)" class="svgbob">', resizeSVG,svg)
|
|
|
|
|
|
# store as a temporary file
|
|
(svg_file, svg_path) = tempfile.mkstemp('.svg')
|
|
(hpgl_file, hpgl_path) = tempfile.mkstemp('.hpgl')
|
|
|
|
with open(svg_file, 'w') as svg_handle:
|
|
svg_handle.write(svg)
|
|
|
|
output = subprocess.run([
|
|
"vpype",
|
|
"read",
|
|
"--single-layer",
|
|
svg_path,
|
|
"scaleto",
|
|
"297mm", "420mm",
|
|
"linesimplify",
|
|
"-t", "0.05mm",
|
|
"linemerge",
|
|
"-t", "0.25mm",
|
|
"linesort",
|
|
"write",
|
|
"--device", "dxy",
|
|
"--color-mode", "none",
|
|
"--page-size", "a3",
|
|
"--landscape",
|
|
hpgl_path
|
|
])
|
|
|
|
with open(hpgl_file, 'r') as hpgl_handle:
|
|
r = Response(hpgl_handle.read(), mimetype='application/hpgl')
|
|
r.headers.extend({
|
|
'Content-Disposition': f'attachment; filename="cobbled-paths-{id}.hpgl"'
|
|
})
|
|
|
|
# remove tmp file
|
|
os.remove(svg_path)
|
|
os.remove(hpgl_path)
|
|
return r
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
parse_collection()
|
|
app.run(debug=True, host='0.0.0.0')
|