Add png export feature

pull/223/head
Ishak BELAHMAR 2019-09-29 21:41:34 +02:00 committed by Benjamin Tissoires
parent 538480f022
commit bbff179a4d
6 changed files with 207 additions and 23 deletions

View File

@ -7,7 +7,7 @@ jobs:
steps: steps:
- run: - run:
command: | command: |
dnf install -y meson gettext python3-devel pygobject3-devel python3-flake8 desktop-file-utils libappstream-glib python3-pytest python3-pyxdg python3-pyyaml python3-svgwrite dnf install -y meson gettext python3-devel pygobject3-devel python3-flake8 desktop-file-utils libappstream-glib python3-pytest python3-pyxdg python3-pyyaml python3-svgwrite python3-cairo
- checkout - checkout
- run: - run:
command: | command: |

View File

@ -36,6 +36,7 @@ python_modules = [
'xdg', 'xdg',
'gi', 'gi',
'yaml', 'yaml',
'cairo',
] ]
if meson.version().version_compare('>=0.51') if meson.version().version_compare('>=0.51')
py3 = pymod.find_installation(modules: python_modules) py3 = pymod.find_installation(modules: python_modules)

View File

@ -29,14 +29,14 @@ import configparser
from pathlib import Path from pathlib import Path
try: try:
from tuhi.svg import JsonSvg from tuhi.export import JsonSvg, JsonPng
import tuhi.dbusclient import tuhi.dbusclient
except ModuleNotFoundError: except ModuleNotFoundError:
# If PYTHONPATH isn't set up or we never installed Tuhi, the module # If PYTHONPATH isn't set up or we never installed Tuhi, the module
# isn't available. And since we don't install kete, we can assume that # isn't available. And since we don't install kete, we can assume that
# we're still in the git repo, so messing with the path is "fine". # we're still in the git repo, so messing with the path is "fine".
sys.path.insert(0, os.path.dirname(os.path.realpath(__file__)) + '/..') # noqa sys.path.insert(0, os.path.dirname(os.path.realpath(__file__)) + '/..') # noqa
from tuhi.svg import JsonSvg from tuhi.export import JsonSvg, JsonPng
import tuhi.dbusclient import tuhi.dbusclient
@ -289,6 +289,7 @@ class Fetcher(Worker):
super(Fetcher, self).__init__(manager) super(Fetcher, self).__init__(manager)
self.device = None self.device = None
self.timestamps = None self.timestamps = None
self.format = args.format
address = args.address address = args.address
index = args.index index = args.index
@ -326,8 +327,12 @@ class Fetcher(Worker):
data = json.loads(jsondata) data = json.loads(jsondata)
t = time.localtime(data['timestamp']) t = time.localtime(data['timestamp'])
t = time.strftime('%Y-%m-%d-%H-%M', t) t = time.strftime('%Y-%m-%d-%H-%M', t)
path = f'{data["devicename"]}-{t}.svg' if self.format == 'png':
JsonSvg(data, self.orientation, filename=path) path = f'{data["devicename"]}-{t}.png'
JsonPng(data, self.orientation, filename=path)
else:
path = f'{data["devicename"]}-{t}.svg'
JsonSvg(data, self.orientation, filename=path)
logger.info(f'{data["devicename"]}: saved file "{path}"') logger.info(f'{data["devicename"]}: saved file "{path}"')
@ -699,6 +704,9 @@ class TuhiKeteShell(cmd.Cmd):
type=is_index_or_all, type=is_index_or_all,
const='all', nargs='?', default='all', const='all', nargs='?', default='all',
help='the index of the drawing to fetch or a literal "all"') help='the index of the drawing to fetch or a literal "all"')
parser.add_argument('--format', metavar='{svg|png}',
default='svg',
help='output file format')
try: try:
parsed_args = parser.parse_args(args.split()) parsed_args = parser.parse_args(args.split())

View File

@ -31,7 +31,7 @@ sys.path.insert(0, os.path.dirname(os.path.realpath(__file__)) + '/..') # noqa
from tuhi.util import flatten from tuhi.util import flatten
from tuhi.drawing import Drawing from tuhi.drawing import Drawing
from tuhi.protocol import StrokeFile from tuhi.protocol import StrokeFile
from tuhi.svg import JsonSvg from tuhi.export import JsonSvg, JsonPng
from tuhi.wacom import WacomProtocolSpark, WacomProtocolIntuosPro, WacomProtocolSlate from tuhi.wacom import WacomProtocolSpark, WacomProtocolIntuosPro, WacomProtocolSlate
logging.basicConfig(format='%(asctime)s %(levelname)s: %(name)s: %(message)s', logging.basicConfig(format='%(asctime)s %(levelname)s: %(name)s: %(message)s',
@ -68,6 +68,7 @@ def parse_file(filename, file_format, tablet_model, orientation):
# gotta convert to Drawings, then to json string, then to json, then # gotta convert to Drawings, then to json string, then to json, then
# to svg. ffs. # to svg. ffs.
svgname = f'{stem}.svg' svgname = f'{stem}.svg'
pngname = f'{stem}.png'
jsonname = f'{stem}.json' jsonname = f'{stem}.json'
d = Drawing(svgname, (width * point_size, height * point_size), timestamp) d = Drawing(svgname, (width * point_size, height * point_size), timestamp)
@ -85,10 +86,13 @@ def parse_file(filename, file_format, tablet_model, orientation):
with open(jsonname, 'w') as fd: with open(jsonname, 'w') as fd:
fd.write(d.to_json()) fd.write(d.to_json())
return return
else:
from io import StringIO from io import StringIO
js = json.load(StringIO(d.to_json())) js = json.load(StringIO(d.to_json()))
JsonSvg(js, orientation, d.name) if file_format == 'svg':
JsonSvg(js, orientation, d.name)
elif file_format == 'png':
JsonPng(js, orientation, pngname)
def fetch_files(): def fetch_files():
@ -142,7 +146,7 @@ def main(args=sys.argv):
parser.add_argument('--format', parser.add_argument('--format',
help='The format to generate. Default: svg', help='The format to generate. Default: svg',
default='svg', default='svg',
choices=['svg', 'json']) choices=['svg', 'png', 'json'])
ns = parser.parse_args(args[1:]) ns = parser.parse_args(args[1:])
if ns.verbose: if ns.verbose:

144
tuhi/export.py Normal file
View File

@ -0,0 +1,144 @@
#!/usr/bin/env python3
#
# 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.
#
import os.path
from gi.repository import GObject
import svgwrite
from svgwrite import mm
import cairo
class ImageExportBase(GObject.Object):
def __init__(self, json, orientation, filename, *args, **kwargs):
super().__init__(*args, **kwargs)
self.json = json
self.timestamp = json['timestamp']
self.filename = filename
self.orientation = orientation.lower()
self._convert()
@property
def output_dimensions(self):
dimensions = self.json['dimensions']
if dimensions == [0, 0]:
width, height = 100, 100
else:
# Original dimensions are too big for most Standards
# so we scale them down
width = dimensions[0] / self._output_scaling_factor
height = dimensions[1] / self._output_scaling_factor
if self.orientation in ['portrait', 'reverse-portrait']:
return height, width
else:
return width, height
@property
def output_strokes(self):
width, height = self.output_dimensions
strokes = []
for s in self.json['strokes']:
points_with_sk_width = []
for p in s['points']:
x, y = p['position']
# Scaling coordinates
x = x / self._output_scaling_factor
y = y / self._output_scaling_factor
if self.orientation == 'reverse-portrait':
x, y = y, height - x
elif self.orientation == 'portrait':
x, y = width - y, x
elif self.orientation == 'reverse-landscape':
x, y = width - x, height - y
# Pressure normalized range is [0, 0xffff]
delta = (p['pressure'] - 0x8000) / 0x8000
stroke_width = self._base_pen_width + self._pen_pressure_width_factor * delta
points_with_sk_width.append((x, y, stroke_width))
strokes.append(points_with_sk_width)
return strokes
class JsonSvg(ImageExportBase):
_output_scaling_factor = 1000
_base_pen_width = 0.4
_pen_pressure_width_factor = 0.2
def _convert(self):
width, height = self.output_dimensions
size = width * mm, height * mm
svg = svgwrite.Drawing(filename=self.filename, size=size)
g = svgwrite.container.Group(id='layer0')
for sk_num, stroke_points in enumerate(self.output_strokes):
lines = svgwrite.container.Group(id=f'sk_{sk_num}', stroke='black')
for i, (x, y, stroke_width) in enumerate(stroke_points):
if i != 0:
xp, yp, stroke_width_p = stroke_points[i - 1]
lines.add(
svg.line(
start=(xp * mm, yp * mm),
end=(x * mm, y * mm),
stroke_width=stroke_width,
style='fill:none'
)
)
g.add(lines)
svg.add(g)
svg.save()
class JsonPng(ImageExportBase):
_output_scaling_factor = 100
_base_pen_width = 3
_pen_pressure_width_factor = 1
def _convert(self):
width, height = self.output_dimensions
width, height = int(width), int(height)
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)
ctx = cairo.Context(surface)
# Paint a transparent background
ctx.set_source_rgba(0, 0, 0, 0)
ctx.paint()
ctx.set_antialias(cairo.Antialias.DEFAULT)
ctx.set_line_join(cairo.LINE_JOIN_ROUND)
ctx.set_source_rgb(0, 0, 0)
for sk_num, stroke_points in enumerate(self.output_strokes):
for i, (x, y, stroke_width) in enumerate(stroke_points):
ctx.set_line_width(stroke_width)
if i == 0:
ctx.move_to(x, y)
else:
ctx.line_to(x, y)
ctx.stroke()
surface.write_to_png(self.filename)

View File

@ -17,14 +17,16 @@ import xdg.BaseDirectory
import os import os
from pathlib import Path from pathlib import Path
from .config import Config from .config import Config
from tuhi.svg import JsonSvg from tuhi.export import JsonSvg, JsonPng
import gi import gi
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
from gi.repository import GObject, Gtk, GdkPixbuf, Gdk # NOQA from gi.repository import GObject, Gtk, GdkPixbuf, Gdk # NOQA
DATA_PATH = Path(xdg.BaseDirectory.xdg_cache_home, 'tuhi', 'svg') DATA_PATH = Path(xdg.BaseDirectory.xdg_cache_home, 'tuhi')
SVG_DATA_PATH = Path(DATA_PATH, 'svg')
PNG_DATA_PATH = Path(DATA_PATH, 'png')
@Gtk.Template(resource_path='/org/freedesktop/Tuhi/ui/Drawing.ui') @Gtk.Template(resource_path='/org/freedesktop/Tuhi/ui/Drawing.ui')
@ -40,7 +42,8 @@ class Drawing(Gtk.EventBox):
super().__init__() super().__init__()
self.orientation = Config().orientation self.orientation = Config().orientation
Config().connect('notify::orientation', self._on_orientation_changed) Config().connect('notify::orientation', self._on_orientation_changed)
DATA_PATH.mkdir(parents=True, exist_ok=True) SVG_DATA_PATH.mkdir(parents=True, exist_ok=True)
PNG_DATA_PATH.mkdir(parents=True, exist_ok=True)
self.json_data = json_data self.json_data = json_data
self._zoom = zoom self._zoom = zoom
@ -56,8 +59,12 @@ class Drawing(Gtk.EventBox):
self.redraw() self.redraw()
def process_svg(self): def process_svg(self):
path = os.fspath(Path(DATA_PATH, f'{self.json_data["timestamp"]}.svg')) path = os.fspath(Path(SVG_DATA_PATH, f'{self.json_data["timestamp"]}.svg'))
self.svg = JsonSvg(self.json_data, self.orientation, path) self.svg = JsonSvg(
self.json_data,
self.orientation,
path
)
width, height = -1, -1 width, height = -1, -1
if 'portrait' in self.orientation: if 'portrait' in self.orientation:
height = 1000 height = 1000
@ -68,6 +75,14 @@ class Drawing(Gtk.EventBox):
height=height, height=height,
preserve_aspect_ratio=True) preserve_aspect_ratio=True)
def process_png(self):
path = os.fspath(Path(PNG_DATA_PATH, f'{self.json_data["timestamp"]}.png'))
self.png = JsonPng(
self.json_data,
self.orientation,
path
)
def redraw(self): def redraw(self):
ratio = self.pixbuf.get_height() / self.pixbuf.get_width() ratio = self.pixbuf.get_height() / self.pixbuf.get_width()
base = 250 + self.zoom * 50 base = 250 + self.zoom * 50
@ -115,21 +130,33 @@ class Drawing(Gtk.EventBox):
# Translators: filter to show svg files only # Translators: filter to show svg files only
filter_svg.set_name(_('SVG files')) filter_svg.set_name(_('SVG files'))
filter_svg.add_pattern('*.svg') filter_svg.add_pattern('*.svg')
filter_png = Gtk.FileFilter()
# Translators: filter to show png files only
filter_png.set_name(_('PNG files'))
filter_png.add_pattern('*.png')
dialog.add_filter(filter_svg) dialog.add_filter(filter_svg)
dialog.add_filter(filter_png)
dialog.add_filter(filter_any) dialog.add_filter(filter_any)
response = dialog.run() response = dialog.run()
if response == Gtk.ResponseType.OK: if response == Gtk.ResponseType.OK:
import shutil import shutil
# regenerate the SVG based on the current rotation.
# where we used the orientation buttons, we haven't updated the
# file itself.
self.process_svg()
file = dialog.get_filename() file = dialog.get_filename()
shutil.copyfile(self.svg.filename, file)
# FIXME: error handling if file.lower().endswith('.png'):
# regenerate the PNG based on the current rotation.
# where we used the orientation buttons, we haven't updated the
# file itself.
self.process_png()
shutil.move(self.png.filename, file)
else:
# regenerate the SVG based on the current rotation.
# where we used the orientation buttons, we haven't updated the
# file itself.
self.process_svg()
shutil.copyfile(self.svg.filename, file)
# FIXME: error handling
dialog.destroy() dialog.destroy()