mirror of
https://github.com/google/pebble.git
synced 2025-11-30 03:02:24 -05:00
Import of the watch repository from Pebble
This commit is contained in:
0
third_party/pbl/pblconvert/pblconvert/__init__.py
vendored
Normal file
0
third_party/pbl/pblconvert/pblconvert/__init__.py
vendored
Normal file
8
third_party/pbl/pblconvert/pblconvert/__main__.py
vendored
Normal file
8
third_party/pbl/pblconvert/pblconvert/__main__.py
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
"""executed when directory is called as script."""
|
||||
|
||||
|
||||
from pblconvert import main
|
||||
main()
|
||||
0
third_party/pbl/pblconvert/pblconvert/bin/__init__.py
vendored
Normal file
0
third_party/pbl/pblconvert/pblconvert/bin/__init__.py
vendored
Normal file
31
third_party/pbl/pblconvert/pblconvert/bin/gif2apng
vendored
Executable file
31
third_party/pbl/pblconvert/pblconvert/bin/gif2apng
vendored
Executable file
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from __future__ import print_function
|
||||
import os
|
||||
import platform
|
||||
import sys
|
||||
import subprocess
|
||||
|
||||
arguments = sys.argv[1:]
|
||||
|
||||
THIS_PATH = os.path.abspath(__file__)
|
||||
LINUX_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'gif2apng_noprev_linux')
|
||||
OSX_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'gif2apng_noprev_osx')
|
||||
|
||||
if platform.system() == 'Darwin':
|
||||
cmd = [OSX_PATH]
|
||||
elif platform.system() == 'Linux':
|
||||
cmd = [LINUX_PATH]
|
||||
else:
|
||||
raise Exception("Your operating system is not supported")
|
||||
|
||||
# Transform absolute paths into relative paths (they are not supported)
|
||||
arguments = [a if a[0] != '/' else os.path.relpath(a, THIS_PATH) for a in arguments]
|
||||
cmd += arguments
|
||||
|
||||
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
out, err = p.communicate()
|
||||
print(out, file=sys.stdout)
|
||||
print(err, file=sys.stderr)
|
||||
|
||||
sys.exit(p.returncode)
|
||||
BIN
third_party/pbl/pblconvert/pblconvert/bin/gif2apng_noprev_linux
vendored
Executable file
BIN
third_party/pbl/pblconvert/pblconvert/bin/gif2apng_noprev_linux
vendored
Executable file
Binary file not shown.
BIN
third_party/pbl/pblconvert/pblconvert/bin/gif2apng_noprev_osx
vendored
Executable file
BIN
third_party/pbl/pblconvert/pblconvert/bin/gif2apng_noprev_osx
vendored
Executable file
Binary file not shown.
145
third_party/pbl/pblconvert/pblconvert/bin/pbi2png.py
vendored
Normal file
145
third_party/pbl/pblconvert/pblconvert/bin/pbi2png.py
vendored
Normal file
@@ -0,0 +1,145 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import os, sys
|
||||
from ctypes import *
|
||||
from PIL import Image
|
||||
|
||||
format_dict = {
|
||||
'GBitmapFormat1Bit': 0,
|
||||
'GBitmapFormat8Bit': 1,
|
||||
'GBitmapFormat1BitPalette': 2,
|
||||
'GBitmapFormat2BitPalette': 3,
|
||||
'GBitmapFormat4BitPalette': 4
|
||||
}
|
||||
|
||||
|
||||
# NOTE: If this changes, please update the GBitmapDump gdb command.
|
||||
class pbi_struct(Structure):
|
||||
_fields_ = [
|
||||
("stride", c_uint16), ("info", c_uint16),
|
||||
("bounds_x", c_uint16), ("bounds_y", c_uint16),
|
||||
("bounds_w", c_uint16), ("bounds_h", c_uint16),
|
||||
]
|
||||
|
||||
def flip_byte(abyte):
|
||||
return int('{:08b}'.format(abyte)[::-1],2)
|
||||
|
||||
#converts from argb8 (2-bits per color channel) to RGBA32 (byte per channel)
|
||||
def argb8_to_rgba32(argb8):
|
||||
return (
|
||||
((argb8 >> 4) & 0x3) * 85, #R
|
||||
((argb8 >> 2) & 0x3) * 85, #G
|
||||
((argb8 ) & 0x3) * 85, #B
|
||||
((argb8 >> 6) & 0x3) * 85) #A
|
||||
|
||||
|
||||
def pbi_format(info):
|
||||
return (info & 0xe) >> 1
|
||||
|
||||
|
||||
def pbi_bitdepth(fmt):
|
||||
bitdepth_list = [1, 8, 1, 2, 4]
|
||||
return bitdepth_list[fmt]
|
||||
|
||||
|
||||
def pbi_is_palettized(fmt):
|
||||
return fmt >= format_dict['GBitmapFormat1BitPalette']
|
||||
|
||||
|
||||
def palette_size(fmt):
|
||||
return 2 ** pbi_bitdepth(fmt)
|
||||
|
||||
|
||||
def pbi_to_png(pbi, pixel_bytearray):
|
||||
gbitmap_version = (pbi.info >> 12) & 0x0F
|
||||
gbitmap_format = pbi_format(pbi.info)
|
||||
# if version is 2 and format is 0x01 (GBitmapFormat8Bit)
|
||||
if gbitmap_version == 1 and gbitmap_format == format_dict['GBitmapFormat8Bit']:
|
||||
print("8-bit ARGB color image")
|
||||
pixel_rgba_array = bytearray()
|
||||
for (idx, abyte) in enumerate(pixel_bytearray):
|
||||
argb8 = pixel_bytearray[idx]
|
||||
pixel_rgba_array.append(((argb8 >> 4) & 0x3) * 85) # r
|
||||
pixel_rgba_array.append(((argb8 >> 2) & 0x3) * 85) # g
|
||||
pixel_rgba_array.append(((argb8 >> 0) & 0x3) * 85) # b
|
||||
pixel_rgba_array.append(((argb8 >> 6) & 0x3) * 85) # a
|
||||
|
||||
png = Image.frombuffer('RGBA', (pbi.bounds_w, pbi.bounds_h),
|
||||
buffer(pixel_rgba_array), 'raw', 'RGBA', pbi.stride * 4, 1)
|
||||
|
||||
elif gbitmap_version == 1 and pbi_is_palettized(gbitmap_format):
|
||||
bitdepth = pbi_bitdepth(gbitmap_format)
|
||||
print("{}-bit palettized color image".format(bitdepth))
|
||||
|
||||
# Create palette colors in format R, G, B, A
|
||||
palette = []
|
||||
palette_offset = pbi.stride * pbi.bounds_h
|
||||
for argb8 in pixel_bytearray[palette_offset:]:
|
||||
palette.append((argb8_to_rgba32(argb8)))
|
||||
|
||||
pixels = []
|
||||
# go through the image data byte by byte, and handle
|
||||
# converting the depth-packed indexes for the palette to an unpacked list
|
||||
idx = 0 # index of actual packed values including padded values
|
||||
for pxl8 in pixel_bytearray[:palette_offset]:
|
||||
for i in xrange(0, 8 / bitdepth):
|
||||
# only append actual pixels, ignoring padding pixels
|
||||
# which is the difference between the width and the stride
|
||||
if (idx % (pbi.stride * (8 / bitdepth)) < pbi.bounds_w):
|
||||
pixels.append(
|
||||
((pxl8 >> (bitdepth * (8 / bitdepth - (i + 1)))) & ~(~0 << bitdepth)))
|
||||
idx = idx + 1
|
||||
|
||||
# Manually convert from paletted to RGBA
|
||||
# as PIL doesn't seem to handle palette with alpha
|
||||
rgba_pixels = []
|
||||
for pal_pxl in pixels:
|
||||
rgba_pixels.append(palette[pal_pxl])
|
||||
|
||||
png = Image.new('RGBA', (pbi.bounds_w, pbi.bounds_h))
|
||||
png.putdata(rgba_pixels)
|
||||
|
||||
# legacy 1-bit format
|
||||
elif gbitmap_version == 0 or \
|
||||
(gbitmap_version == 1 and gbitmap_format == format_dict['GBitmapFormat1Bit']):
|
||||
print("1-bit b&w image")
|
||||
# pbi has bits in bytes reversed, so flip here
|
||||
for (idx, abyte) in enumerate(pixel_bytearray):
|
||||
pixel_bytearray[idx] = flip_byte(pixel_bytearray[idx])
|
||||
|
||||
png = Image.frombuffer('1', (pbi.bounds_w, pbi.bounds_h),
|
||||
buffer(pixel_bytearray), 'raw', '1', pbi.stride, 1)
|
||||
else:
|
||||
print "Bad PBI"
|
||||
png = None
|
||||
|
||||
return png
|
||||
|
||||
|
||||
def main():
|
||||
# arguments, print an example of correct usage.
|
||||
if len(sys.argv) - 1 != 2:
|
||||
print("********************")
|
||||
print("Usage suggestion:")
|
||||
print("python " + sys.argv[0] + " <in_image.pbi> <out_image.png>")
|
||||
print("********************")
|
||||
exit()
|
||||
|
||||
input_filename = sys.argv[1]
|
||||
output_filename = sys.argv[2]
|
||||
|
||||
print("Converting PBI to PNG...")
|
||||
pbi = pbi_struct()
|
||||
pixel_bytearray = bytearray()
|
||||
with open(input_filename, 'rb') as afile:
|
||||
afile.readinto(pbi)
|
||||
print("x:%d y:%d" % (pbi.bounds_x, pbi.bounds_y))
|
||||
print("Width:%d Height:%d" % (pbi.bounds_w, pbi.bounds_h))
|
||||
print("row stride:%d" % (pbi.stride))
|
||||
pixel_bytearray = bytearray(afile.read())
|
||||
|
||||
png = pbi_to_png(pbi, pixel_bytearray)
|
||||
png.save(output_filename)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
27
third_party/pbl/pblconvert/pblconvert/bin/pdc2png
vendored
Executable file
27
third_party/pbl/pblconvert/pblconvert/bin/pdc2png
vendored
Executable file
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from __future__ import print_function
|
||||
import os
|
||||
import platform
|
||||
import sys
|
||||
import subprocess
|
||||
|
||||
arguments = sys.argv[1:]
|
||||
|
||||
LINUX_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'pdc2png_linux')
|
||||
OSX_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'pdc2png_osx')
|
||||
|
||||
if platform.system() == 'Darwin':
|
||||
cmd = [OSX_PATH]
|
||||
elif platform.system() == 'Linux':
|
||||
cmd = [LINUX_PATH]
|
||||
else:
|
||||
raise Exception("Your operating system is not supported")
|
||||
|
||||
cmd.extend(arguments)
|
||||
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
out, err = p.communicate()
|
||||
print(out, file=sys.stdout)
|
||||
print(err, file=sys.stderr)
|
||||
|
||||
sys.exit(p.returncode)
|
||||
BIN
third_party/pbl/pblconvert/pblconvert/bin/pdc2png_linux
vendored
Executable file
BIN
third_party/pbl/pblconvert/pblconvert/bin/pdc2png_linux
vendored
Executable file
Binary file not shown.
BIN
third_party/pbl/pblconvert/pblconvert/bin/pdc2png_osx
vendored
Executable file
BIN
third_party/pbl/pblconvert/pblconvert/bin/pdc2png_osx
vendored
Executable file
Binary file not shown.
6
third_party/pbl/pblconvert/pblconvert/exceptions.py
vendored
Normal file
6
third_party/pbl/pblconvert/pblconvert/exceptions.py
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
class PblConvertError(Exception):
|
||||
def __str__(self):
|
||||
return self.__class__.__name__ + ': ' + ' '.join(self.args)
|
||||
|
||||
class PblConvertFormatError(PblConvertError):
|
||||
pass
|
||||
0
third_party/pbl/pblconvert/pblconvert/gif2apng/__init__.py
vendored
Normal file
0
third_party/pbl/pblconvert/pblconvert/gif2apng/__init__.py
vendored
Normal file
68
third_party/pbl/pblconvert/pblconvert/gif2apng/colormap.txt
vendored
Normal file
68
third_party/pbl/pblconvert/pblconvert/gif2apng/colormap.txt
vendored
Normal file
@@ -0,0 +1,68 @@
|
||||
; The colormap for the Pebble Time family watches
|
||||
; in a format gifsicle understands.
|
||||
; See https://www.lcdf.org/gifsicle/man.html
|
||||
; for more details on the --user-colormap flag
|
||||
0 0 0
|
||||
0 0 85
|
||||
0 0 170
|
||||
0 0 255
|
||||
0 85 0
|
||||
0 85 85
|
||||
0 85 170
|
||||
0 85 255
|
||||
0 170 0
|
||||
0 170 85
|
||||
0 170 170
|
||||
0 170 255
|
||||
0 255 0
|
||||
0 255 85
|
||||
0 255 170
|
||||
0 255 255
|
||||
85 0 0
|
||||
85 0 85
|
||||
85 0 170
|
||||
85 0 255
|
||||
85 85 0
|
||||
85 85 85
|
||||
85 85 170
|
||||
85 85 255
|
||||
85 170 0
|
||||
85 170 85
|
||||
85 170 170
|
||||
85 170 255
|
||||
85 255 0
|
||||
85 255 85
|
||||
85 255 170
|
||||
85 255 255
|
||||
170 0 0
|
||||
170 0 85
|
||||
170 0 170
|
||||
170 0 255
|
||||
170 85 0
|
||||
170 85 85
|
||||
170 85 170
|
||||
170 85 255
|
||||
170 170 0
|
||||
170 170 85
|
||||
170 170 170
|
||||
170 170 255
|
||||
170 255 0
|
||||
170 255 85
|
||||
170 255 170
|
||||
170 255 255
|
||||
255 0 0
|
||||
255 0 85
|
||||
255 0 170
|
||||
255 0 255
|
||||
255 85 0
|
||||
255 85 85
|
||||
255 85 170
|
||||
255 85 255
|
||||
255 170 0
|
||||
255 170 85
|
||||
255 170 170
|
||||
255 170 255
|
||||
255 255 0
|
||||
255 255 85
|
||||
255 255 170
|
||||
255 255 255
|
||||
7
third_party/pbl/pblconvert/pblconvert/gif2apng/exceptions.py
vendored
Normal file
7
third_party/pbl/pblconvert/pblconvert/gif2apng/exceptions.py
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
class Gif2ApngError(Exception):
|
||||
def __str__(self):
|
||||
return self.__class__.__name__ + ': ' + ' '.join(self.args)
|
||||
|
||||
|
||||
class Gif2ApngFormatError(Gif2ApngError):
|
||||
pass
|
||||
69
third_party/pbl/pblconvert/pblconvert/gif2apng/gif.py
vendored
Normal file
69
third_party/pbl/pblconvert/pblconvert/gif2apng/gif.py
vendored
Normal file
@@ -0,0 +1,69 @@
|
||||
from __future__ import print_function
|
||||
import imghdr
|
||||
import io
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
# gif2apng
|
||||
from exceptions import *
|
||||
|
||||
GIF2APNG_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||||
'../bin/gif2apng')
|
||||
COLORMAP_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||||
'colormap.txt')
|
||||
|
||||
|
||||
def read_gif(obj):
|
||||
data = obj.read()
|
||||
if imghdr.what(None, data) != 'gif':
|
||||
raise Gif2ApngFormatError("{} is not a valid GIF data".format(path))
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def convert_to_apng(gif):
|
||||
# Write data to temporary file
|
||||
gif_file = tempfile.NamedTemporaryFile(delete=False)
|
||||
gif_file.write(gif)
|
||||
gif_file.close()
|
||||
|
||||
# Map onto Pebble colors
|
||||
mod_file = tempfile.NamedTemporaryFile(delete=False)
|
||||
mod_file.close()
|
||||
p = subprocess.Popen(['gifsicle',
|
||||
'--colors', '64',
|
||||
'--use-colormap', COLORMAP_PATH,
|
||||
'-O1',
|
||||
'-o', mod_file.name,
|
||||
gif_file.name],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
out, err = p.communicate()
|
||||
# Deal with https://github.com/kohler/gifsicle/issues/28
|
||||
# Which still exists in some of the packages out there
|
||||
if p.returncode not in [0, 1]:
|
||||
print(err, file=sys.stderr)
|
||||
raise Gif2ApngError(p.returncode)
|
||||
|
||||
# Convert to APNG
|
||||
apng_file = tempfile.NamedTemporaryFile(delete=False)
|
||||
apng_file.close()
|
||||
p = subprocess.Popen([GIF2APNG_PATH,
|
||||
'-z0',
|
||||
mod_file.name,
|
||||
apng_file.name],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
out, err = p.communicate()
|
||||
if p.returncode != 0:
|
||||
print(err, file=sys.stderr)
|
||||
raise Gif2ApngError(p.returncode)
|
||||
|
||||
with open(apng_file.name) as f:
|
||||
apng_data = f.read()
|
||||
|
||||
os.unlink(gif_file.name)
|
||||
os.unlink(mod_file.name)
|
||||
os.unlink(apng_file.name)
|
||||
|
||||
return apng_data
|
||||
112
third_party/pbl/pblconvert/pblconvert/handlers.py
vendored
Normal file
112
third_party/pbl/pblconvert/pblconvert/handlers.py
vendored
Normal file
@@ -0,0 +1,112 @@
|
||||
from abc import *
|
||||
|
||||
from exceptions import PblConvertFormatError
|
||||
|
||||
from gif2apng.gif import read_gif, convert_to_apng
|
||||
from gif2apng.exceptions import Gif2ApngFormatError
|
||||
|
||||
from svg2pdc.pdc import serialize_image, convert_to_png
|
||||
from svg2pdc.svg import surface_from_svg
|
||||
from svg2pdc.exceptions import Svg2PdcFormatError
|
||||
|
||||
|
||||
class Handler:
|
||||
__metaclass__ = ABCMeta
|
||||
|
||||
@classmethod
|
||||
def handler_for_format(cls, fmt):
|
||||
if cls is Handler:
|
||||
for C in cls.__subclasses__():
|
||||
if fmt == C.format():
|
||||
return C()
|
||||
return None
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def read(self, in_obj):
|
||||
return None
|
||||
|
||||
@abstractmethod
|
||||
def format(self):
|
||||
return ""
|
||||
|
||||
@abstractmethod
|
||||
def write_pdc(self, out_obj, data):
|
||||
return None
|
||||
|
||||
@abstractmethod
|
||||
def write_apng(self, out_obj, data):
|
||||
return None
|
||||
|
||||
@abstractmethod
|
||||
def write_png(self, out_obj, data):
|
||||
return None
|
||||
|
||||
@abstractmethod
|
||||
def write_svg(self, out_obj, data):
|
||||
return None
|
||||
|
||||
|
||||
class SvgHandler(Handler):
|
||||
@classmethod
|
||||
def format(cls):
|
||||
return "svg"
|
||||
|
||||
def read(self, in_obj):
|
||||
try:
|
||||
surface = surface_from_svg(bytestring=in_obj.read(),
|
||||
approximate_bezier=True)
|
||||
except Svg2PdcFormatError as e:
|
||||
raise PblConvertFormatError(e.args[0])
|
||||
return surface
|
||||
|
||||
def write_apng(self, out_obj, data):
|
||||
raise NotImplementedError
|
||||
|
||||
def write_pdc(self, out_obj, surface):
|
||||
commands = surface.pdc_commands
|
||||
pdci = serialize_image(commands, surface.size())
|
||||
with out_obj as o:
|
||||
o.write(pdci)
|
||||
|
||||
def write_png(self, out_obj, surface):
|
||||
commands = surface.pdc_commands
|
||||
pdci = serialize_image(commands, surface.size())
|
||||
with out_obj as o:
|
||||
o.write(convert_to_png(pdci))
|
||||
|
||||
def write_svg(self, out_obj, surface):
|
||||
with out_obj as o:
|
||||
et = surface.element_tree()
|
||||
et.write(o, pretty_print=True)
|
||||
|
||||
|
||||
class GifHandler(Handler):
|
||||
@classmethod
|
||||
def format(cls):
|
||||
return "gif"
|
||||
|
||||
def read(self, in_obj):
|
||||
try:
|
||||
gif = read_gif(in_obj)
|
||||
except Gif2ApngFormatError as e:
|
||||
raise PblConvertFormatError(e.args[0])
|
||||
return gif
|
||||
|
||||
def write_pdc(self, out_obj, data):
|
||||
raise NotImplementedError
|
||||
|
||||
def write_svg(self, out_obj, data):
|
||||
raise NotImplementedError
|
||||
|
||||
def write_png(self, out_obj, gif):
|
||||
raise NotImplementedError
|
||||
|
||||
def write_apng(self, out_obj, gif):
|
||||
apng_data = convert_to_apng(gif)
|
||||
with out_obj as o:
|
||||
o.write(apng_data)
|
||||
|
||||
|
||||
Handler.register(GifHandler)
|
||||
Handler.register(SvgHandler)
|
||||
90
third_party/pbl/pblconvert/pblconvert/pblconvert.py
vendored
Normal file
90
third_party/pbl/pblconvert/pblconvert/pblconvert.py
vendored
Normal file
@@ -0,0 +1,90 @@
|
||||
from handlers import *
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
|
||||
SUPPORTED_FORMATS_MAP = {
|
||||
"in": {
|
||||
"gif": ".gif",
|
||||
"svg": ".svg",
|
||||
},
|
||||
"out": {
|
||||
"pdc": ".pdc",
|
||||
"png": ".png",
|
||||
"svg": ".svg",
|
||||
"apng": ".apng",
|
||||
}
|
||||
}
|
||||
|
||||
FORMAT_TO_EXT = dict(SUPPORTED_FORMATS_MAP["in"],
|
||||
**SUPPORTED_FORMATS_MAP["out"])
|
||||
EXT_TO_FORMAT = {v: k for k, v in FORMAT_TO_EXT.items()}
|
||||
|
||||
OUT_FORMATS = SUPPORTED_FORMATS_MAP["out"].keys()
|
||||
IN_FORMATS = SUPPORTED_FORMATS_MAP["in"].keys()
|
||||
|
||||
LIMIT_WHEN_AVOIDING_OVERRIDE = 100
|
||||
|
||||
|
||||
def parse_args(args):
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('-i', '--infile',
|
||||
type=argparse.FileType('r'), required=True)
|
||||
parser.add_argument('-if', '--informat',
|
||||
type=str, choices=IN_FORMATS)
|
||||
parser.add_argument('-o', '--outfile',
|
||||
type=argparse.FileType('w'))
|
||||
parser.add_argument('-of', '--outformat',
|
||||
type=str, choices=OUT_FORMATS)
|
||||
|
||||
parsed = parser.parse_args(args)
|
||||
|
||||
assert parsed.infile is not None
|
||||
assert parsed.outformat is not None or parsed.outfile is not None
|
||||
|
||||
if parsed.informat is None:
|
||||
parsed.informat = EXT_TO_FORMAT.get(
|
||||
os.path.splitext(parsed.infile.name)[1], "svg")
|
||||
|
||||
if parsed.outformat is None:
|
||||
parsed.outformat = "pdc" if parsed.outfile is None else \
|
||||
EXT_TO_FORMAT.get(os.path.splitext(parsed.outfile.name)[1], "pdc")
|
||||
|
||||
if parsed.outfile is None:
|
||||
if parsed.infile == sys.stdin:
|
||||
parsed.outfile = sys.stdout
|
||||
else:
|
||||
# look at format
|
||||
outfile_path = os.path.splitext(parsed.infile.name)[0] + \
|
||||
FORMAT_TO_EXT[parsed.outformat]
|
||||
if os.path.exists(outfile_path):
|
||||
avoiding_path = None
|
||||
# avoid accidental overrides
|
||||
for i in range(2, LIMIT_WHEN_AVOIDING_OVERRIDE):
|
||||
split = os.path.splitext(outfile_path)
|
||||
avoiding_path = "%s_%d%s" % (split[0], i, split[1])
|
||||
if not os.path.exists(avoiding_path):
|
||||
outfile_path = avoiding_path
|
||||
break
|
||||
|
||||
if outfile_path != avoiding_path:
|
||||
raise IOError("File %s and (%d similar alternatives) "
|
||||
"already exists" %
|
||||
(outfile_path, LIMIT_WHEN_AVOIDING_OVERRIDE))
|
||||
|
||||
parsed.outfile = open(outfile_path, "w")
|
||||
|
||||
return parsed
|
||||
|
||||
|
||||
def logic(handler, parsed):
|
||||
data = handler.read(parsed.infile)
|
||||
method = getattr(handler, "write_" + parsed.outformat)
|
||||
method(parsed.outfile, data)
|
||||
|
||||
|
||||
def main():
|
||||
parsed = parse_args(sys.argv[1:])
|
||||
handler = Handler.handler_for_format(parsed.informat)
|
||||
logic(handler, parsed)
|
||||
1
third_party/pbl/pblconvert/pblconvert/svg2pdc/__init__.py
vendored
Normal file
1
third_party/pbl/pblconvert/pblconvert/svg2pdc/__init__.py
vendored
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
65
third_party/pbl/pblconvert/pblconvert/svg2pdc/annotation.py
vendored
Normal file
65
third_party/pbl/pblconvert/pblconvert/svg2pdc/annotation.py
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
# coding=utf-8
|
||||
from xml.etree import ElementTree
|
||||
from pdc import extend_bounding_box, bounding_box_around_points
|
||||
|
||||
NS_ANNOTATION = "http://www.pebble.com/2015/pdc"
|
||||
PREFIX_ANNOTATION = "pdc"
|
||||
|
||||
TAG_HIGHLIGHT = "{%s}highlight" % NS_ANNOTATION
|
||||
TAG_ANNOTATION = "{%s}annotation" % NS_ANNOTATION
|
||||
|
||||
def to_str(value):
|
||||
return "%.2f" % float(value)
|
||||
|
||||
class Annotation():
|
||||
def __init__(self, node, text, transformed=False, link=None):
|
||||
self.node = node
|
||||
self.text = text
|
||||
self.href = link
|
||||
self.element = ElementTree.SubElement(self.node.node, TAG_ANNOTATION)
|
||||
if text is not None:
|
||||
self.element.set("description", text)
|
||||
if link is not None:
|
||||
self.element.set("href", link)
|
||||
self.highlights = []
|
||||
self.transformed = transformed
|
||||
self.node.annotations.append(self)
|
||||
|
||||
def add_highlight(self, x, y, width=None, height=None, details=None):
|
||||
highlight = ElementTree.SubElement(self.element, TAG_HIGHLIGHT)
|
||||
self.set_highlight(highlight, x=x, y=y, width=width, height=height, details=details)
|
||||
self.highlights.append(highlight)
|
||||
return highlight
|
||||
|
||||
def set_highlight(self, highlight, **kwargs):
|
||||
for k, v in kwargs.iteritems():
|
||||
if v is None:
|
||||
if k in highlight:
|
||||
highlight.pop(k)
|
||||
else:
|
||||
value = to_str(v) if k in ["x", "y", "width", "height"] else v
|
||||
highlight.set(k, value)
|
||||
|
||||
def transform(self, transformer):
|
||||
if self.transformed:
|
||||
return
|
||||
self.transformed = True
|
||||
|
||||
for highlight in self.highlights:
|
||||
top_left = (float(highlight.get("x")), float(highlight.get("y")))
|
||||
size = (float(highlight.get("width")), float(highlight.get("height")))
|
||||
top_right = (top_left[0] + size[0], top_left[1])
|
||||
bottom_right = (top_left[0] + size[0], top_left[1] + size[1])
|
||||
bottom_left = (top_left[0], top_left[1] + size[1])
|
||||
|
||||
# we needs to transform all four points instead of origin + diagonal
|
||||
# e.g. 45° on a square would otherwise become a line
|
||||
top_left = transformer.transform_point(top_left)
|
||||
top_right = transformer.transform_point(top_right)
|
||||
bottom_right = transformer.transform_point(bottom_right)
|
||||
bottom_left = transformer.transform_point(bottom_left)
|
||||
|
||||
box = bounding_box_around_points([top_left, top_right, bottom_right, bottom_left])
|
||||
self.set_highlight(highlight, x=box[0], y=box[1], width=box[2], height=box[3])
|
||||
|
||||
|
||||
6
third_party/pbl/pblconvert/pblconvert/svg2pdc/exceptions.py
vendored
Normal file
6
third_party/pbl/pblconvert/pblconvert/svg2pdc/exceptions.py
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
class Svg2PdcError(Exception):
|
||||
def __str__(self):
|
||||
return self.__class__.__name__ + ': ' + ' '.join(self.args)
|
||||
|
||||
class Svg2PdcFormatError(Svg2PdcError):
|
||||
pass
|
||||
303
third_party/pbl/pblconvert/pblconvert/svg2pdc/pdc.py
vendored
Normal file
303
third_party/pbl/pblconvert/pblconvert/svg2pdc/pdc.py
vendored
Normal file
@@ -0,0 +1,303 @@
|
||||
from StringIO import StringIO
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
from struct import pack
|
||||
import sys
|
||||
from subprocess import Popen, PIPE
|
||||
from pebble_image_routines import truncate_color_to_pebble64_palette, nearest_color_to_pebble64_palette, \
|
||||
rgba32_triplet_to_argb8
|
||||
|
||||
DRAW_COMMAND_VERSION = 1
|
||||
DRAW_COMMAND_TYPE_PATH = 1
|
||||
DRAW_COMMAND_TYPE_CIRCLE = 2
|
||||
DRAW_COMMAND_TYPE_PRECISE_PATH = 3
|
||||
|
||||
epsilon = sys.float_info.epsilon
|
||||
|
||||
def valid_color(r, g, b, a):
|
||||
return (r <= 0xFF) and (g <= 0xFF) and (b <= 0xFF) and (a <= 0xFF) and \
|
||||
(r >= 0x00) and (g >= 0x00) and (b >= 0x00) and (a >= 0x00)
|
||||
|
||||
|
||||
def convert_color(r, g, b, a, truncate=True):
|
||||
|
||||
valid = valid_color(r, g, b, a)
|
||||
if not valid:
|
||||
print "Invalid color: ({}, {}, {}, {})".format(r, g, b, a)
|
||||
return 0
|
||||
|
||||
if truncate:
|
||||
(r, g, b, a) = truncate_color_to_pebble64_palette(r, g, b, a)
|
||||
else:
|
||||
(r, g, b, a) = nearest_color_to_pebble64_palette(r, g, b, a)
|
||||
|
||||
return rgba32_triplet_to_argb8(r, g, b, a)
|
||||
|
||||
def sum_points(p1, p2):
|
||||
return p1[0] + p2[0], p1[1] + p2[1]
|
||||
|
||||
|
||||
def subtract_points(p1, p2):
|
||||
return p1[0] - p2[0], p1[1] - p2[1]
|
||||
|
||||
|
||||
def round_point(p):
|
||||
return round(p[0] + epsilon), round(p[1] + epsilon) # hack to get around the fact that python rounds negative
|
||||
# numbers downwards
|
||||
|
||||
|
||||
def scale_point(p, factor):
|
||||
return p[0] * factor, p[1] * factor
|
||||
|
||||
|
||||
def find_nearest_valid_point(p):
|
||||
return (round(p[0] * 2.0) / 2.0), (round(p[1] * 2.0) / 2.0)
|
||||
|
||||
|
||||
def find_nearest_valid_precise_point(p):
|
||||
return (round(p[0] * 8.0) / 8.0), (round(p[1] * 8.0) / 8.0)
|
||||
|
||||
|
||||
def convert_to_pebble_coordinates(point, precise=False):
|
||||
# convert from graphic tool coordinate system to pebble coordinate system so that they render the same on
|
||||
# both
|
||||
|
||||
if not precise:
|
||||
nearest = find_nearest_valid_point(point) # used to give feedback to user if the point shifts considerably
|
||||
else:
|
||||
nearest = find_nearest_valid_precise_point(point)
|
||||
|
||||
problem = None if compare_points(point, nearest) else "Invalid point: ({:.2f}, {:.2f}). Used closest supported coordinate: ({}, {})".format(
|
||||
point[0], point[1], nearest[0], nearest[1])
|
||||
|
||||
translated = sum_points(point, (-0.5, -0.5)) # translate point by (-0.5, -0.5)
|
||||
if precise:
|
||||
translated = scale_point(translated, 8) # scale point for precise coordinates
|
||||
rounded = round_point(translated)
|
||||
|
||||
return rounded, problem
|
||||
|
||||
|
||||
def compare_points(p1, p2):
|
||||
return p1[0] == p2[0] and p1[1] == p2[1]
|
||||
|
||||
|
||||
class InvalidPointException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def bounding_box_around_points(points):
|
||||
result = None
|
||||
for p in points:
|
||||
result = extend_bounding_box(result, p)
|
||||
return result
|
||||
|
||||
|
||||
def extend_bounding_box(rect, point=None, rect2=None):
|
||||
if rect is None:
|
||||
return rect2 if rect2 is not None else (point[0], point[1], 0, 0)
|
||||
|
||||
if rect2 is not None:
|
||||
top_left = (rect2[0], rect2[1])
|
||||
bottom_right = (rect2[0] + rect2[2], rect2[1] + rect2[3])
|
||||
rect = extend_bounding_box(rect, point=top_left)
|
||||
rect = extend_bounding_box(rect, point=bottom_right)
|
||||
return rect
|
||||
|
||||
assert point is not None
|
||||
min_x = min(rect[0], point[0])
|
||||
min_y = min(rect[1], point[1])
|
||||
max_x = max(rect[0] + rect[2], point[0])
|
||||
max_y = max(rect[1] + rect[3], point[1])
|
||||
return (min_x, min_y, max_x - min_x, max_y - min_y)
|
||||
|
||||
|
||||
PDC2PNG = os.path.join(os.path.dirname(os.path.realpath(__file__)), "../bin/pdc2png")
|
||||
|
||||
|
||||
def convert_to_png(pdc_data):
|
||||
tmp_dir = tempfile.mkdtemp()
|
||||
try:
|
||||
pdc_path = os.path.join(tmp_dir, "image.pdc")
|
||||
with open(pdc_path, "wb") as pdc_file:
|
||||
pdc_file.write(pdc_data)
|
||||
|
||||
cmd = '%s %s' % (PDC2PNG, pdc_path)
|
||||
p = Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE)
|
||||
stdout, stderr = p.communicate()
|
||||
if p.returncode != 0:
|
||||
raise IOError(stderr)
|
||||
|
||||
png_path = os.path.join(tmp_dir, "image.png")
|
||||
with open(png_path, "rb") as png_file:
|
||||
return png_file.read()
|
||||
finally:
|
||||
shutil.rmtree(tmp_dir)
|
||||
|
||||
|
||||
class Command:
|
||||
'''
|
||||
Draw command serialized structure:
|
||||
| Bytes | Field
|
||||
| 1 | Draw command type
|
||||
| 1 | Reserved byte
|
||||
| 1 | Stroke color
|
||||
| 1 | Stroke width
|
||||
| 1 | Fill color
|
||||
For Paths:
|
||||
| 1 | Open path
|
||||
| 1 | Unused/Reserved
|
||||
For Circles:
|
||||
| 2 | Radius
|
||||
Common:
|
||||
| 2 | Number of points (should always be 1 for circles)
|
||||
| n * 4 | Array of n points in the format below:
|
||||
Point:
|
||||
| 2 | x
|
||||
| 2 | y
|
||||
'''
|
||||
|
||||
def __init__(self, points, stroke_width=0, stroke_color=0, fill_color=0,
|
||||
raise_error=False):
|
||||
# for i in range(len(points)):
|
||||
# points[i], valid = convert_to_pebble_coordinates(points[i], precise)
|
||||
# if not valid and raise_error:
|
||||
# raise InvalidPointException("Invalid point in command")
|
||||
|
||||
self.points = list(points)
|
||||
self.stroke_width = stroke_width
|
||||
self.stroke_color = stroke_color
|
||||
self.fill_color = fill_color
|
||||
|
||||
def is_precise(self):
|
||||
return False
|
||||
|
||||
def transform(self, transformer):
|
||||
self.points = list([transformer.transform_point(p) for p in self.points])
|
||||
|
||||
def finalize(self, annotator):
|
||||
grid_annotation = None
|
||||
for p in self.points:
|
||||
converted, problem = convert_to_pebble_coordinates(p, self.is_precise())
|
||||
|
||||
if problem is not None:
|
||||
if grid_annotation is None:
|
||||
link = "https://pebbletechnology.atlassian.net/wiki/display/DEV/Pebble+Draw+Commands#PebbleDrawCommands-issue-pixelgrid"
|
||||
grid_annotation = annotator.add_annotation("Element is expressed with unsupported coordinate(s).", link=link)
|
||||
grid_annotation.add_highlight(p[0], p[1], details=problem)
|
||||
|
||||
|
||||
pass
|
||||
|
||||
def bounding_box(self):
|
||||
result = None
|
||||
for p in self.points:
|
||||
result = extend_bounding_box(result, point=p)
|
||||
return result
|
||||
|
||||
def serialize_common(self):
|
||||
return pack('<BBBB',
|
||||
0, #reserved byte
|
||||
self.stroke_color,
|
||||
self.stroke_width,
|
||||
self.fill_color)
|
||||
|
||||
def serialize_points(self):
|
||||
s = pack('H', len(self.points)) # number of points (16-bit)
|
||||
for p in self.points:
|
||||
converted, _ = convert_to_pebble_coordinates(p, self.is_precise())
|
||||
s += pack('<hh',
|
||||
int(converted[0]), # x (16-bit)
|
||||
int(converted[1])) # y (16-bit)
|
||||
return s
|
||||
|
||||
|
||||
class PathCommand(Command):
|
||||
def __init__(self, points, path_open, stroke_width=0, stroke_color=0, fill_color=0, precise=False,
|
||||
raise_error=False):
|
||||
self.open = path_open
|
||||
self.type = DRAW_COMMAND_TYPE_PATH if not precise else DRAW_COMMAND_TYPE_PRECISE_PATH
|
||||
Command.__init__(self, points, stroke_width, stroke_color, fill_color, raise_error)
|
||||
|
||||
def is_precise(self):
|
||||
return self.type == DRAW_COMMAND_TYPE_PRECISE_PATH
|
||||
|
||||
def serialize(self):
|
||||
s = pack('B', self.type) # command type
|
||||
s += self.serialize_common()
|
||||
s += pack('<BB',
|
||||
int(self.open), # open path boolean
|
||||
0) # unused byte in path
|
||||
s += self.serialize_points()
|
||||
return s
|
||||
|
||||
def __str__(self):
|
||||
points = self.points[:]
|
||||
if self.type == DRAW_COMMAND_TYPE_PRECISE_PATH:
|
||||
type = 'P'
|
||||
for i in range(len(points)):
|
||||
points[i] = scale_point(points[i], 0.125)
|
||||
else:
|
||||
type = ''
|
||||
return "Path: [fill color:{}; stroke color:{}; stroke width:{}] {} {} {}".format(self.fill_color,
|
||||
self.stroke_color,
|
||||
self.stroke_width,
|
||||
points,
|
||||
self.open,
|
||||
type)
|
||||
|
||||
|
||||
class CircleCommand(object, Command):
|
||||
def __init__(self, center, radius, stroke_width=0, stroke_color=0, fill_color=0):
|
||||
points = [(center[0], center[1])]
|
||||
Command.__init__(self, points, stroke_width, stroke_color, fill_color)
|
||||
self.radius = radius
|
||||
|
||||
def transform(self, transformer):
|
||||
super(CircleCommand, self).transform(transformer)
|
||||
|
||||
(dx, dy) = transformer.transform_distance(self.radius, self.radius)
|
||||
self.radius = min(dx, dy)
|
||||
if dx != dy:
|
||||
annotation = transformer.add_annotation("Only rigid transformations for circles are supported.",
|
||||
transformed=True)
|
||||
center = self.points[0]
|
||||
annotation.add_highlight(center[0] - dx, center[1] - dy, dx * 2, dy * 2)
|
||||
|
||||
|
||||
|
||||
def serialize(self):
|
||||
s = pack('B', DRAW_COMMAND_TYPE_CIRCLE) # command type
|
||||
s += self.serialize_common()
|
||||
s += pack('H', self.radius) # circle radius (16-bit)
|
||||
s += self.serialize_points()
|
||||
return s
|
||||
|
||||
def __str__(self):
|
||||
return "Circle: [fill color:{}; stroke color:{}; stroke width:{}] {} {}".format(self.fill_color,
|
||||
self.stroke_color,
|
||||
self.stroke_width,
|
||||
self.points[0],
|
||||
self.radius)
|
||||
|
||||
|
||||
def serialize_header(size):
|
||||
return pack('<BBhh', DRAW_COMMAND_VERSION, 0, int(round(size[0])), int(round(size[1])))
|
||||
|
||||
|
||||
def serialize(commands):
|
||||
output = pack('H', len(commands)) # number of commands in list
|
||||
for c in commands:
|
||||
output += c.serialize()
|
||||
|
||||
return output
|
||||
|
||||
def serialize_image(commands, size):
|
||||
s = serialize_header(size)
|
||||
s += serialize(commands)
|
||||
|
||||
output = "PDCI"
|
||||
output += pack('I', len(s))
|
||||
output += s
|
||||
return output
|
||||
123
third_party/pbl/pblconvert/pblconvert/svg2pdc/pebble_image_routines.py
vendored
Normal file
123
third_party/pbl/pblconvert/pblconvert/svg2pdc/pebble_image_routines.py
vendored
Normal file
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import math
|
||||
|
||||
# This module contains common image and color routines used to convert images
|
||||
# for use with Pebble.
|
||||
#
|
||||
# pebble64 refers to the color palette that is available in color products,
|
||||
# pebble2 refers to the palette available in b&w products
|
||||
|
||||
|
||||
# Create pebble 64 colors-table (r, g, b - 2 bits per channel)
|
||||
def _get_pebble64_palette():
|
||||
pebble_palette = []
|
||||
for i in xrange(0, 64):
|
||||
pebble_palette.append((
|
||||
((i >> 4) & 0x3) * 85, # R
|
||||
((i >> 2) & 0x3) * 85, # G
|
||||
((i ) & 0x3) * 85)) # B
|
||||
return pebble_palette
|
||||
|
||||
|
||||
def nearest_color_to_pebble64_palette(r, g, b, a):
|
||||
"""
|
||||
match each rgba32 pixel to the nearest color in the 8 bit pebble palette
|
||||
returns closest rgba32 color triplet (r, g, b, a)
|
||||
"""
|
||||
|
||||
a = ((a + 42) / 85) * 85 # fast nearest alpha for 2bit color range
|
||||
# clear transparent pixels (makes image more compress-able)
|
||||
# and required for greyscale tests
|
||||
if a == 0:
|
||||
r, g, b = (0, 0, 0)
|
||||
else:
|
||||
r = ((r + 42) / 85) * 85 # nearest for 2bit color range
|
||||
g = ((g + 42) / 85) * 85 # nearest for 2bit color range
|
||||
b = ((b + 42) / 85) * 85 # nearest for 2bit color range
|
||||
|
||||
return r, g, b, a
|
||||
|
||||
|
||||
def nearest_color_to_pebble2_palette(r, g, b, a):
|
||||
"""
|
||||
match each rgba32 pixel to the nearest color in 2 bit pebble palette
|
||||
returns closest rgba32 color triplet (r, g, b, a)
|
||||
"""
|
||||
|
||||
# these constants come from ITU-R recommendation BT.709
|
||||
luma = (r * 0.2126 + g * 0.7152 + b * 0.11)
|
||||
|
||||
def round_to_1_bit(value):
|
||||
""" Round a [0-255] value to either 0 or 255 """
|
||||
if value > (255 / 2):
|
||||
return 255
|
||||
return 0
|
||||
|
||||
rounded_luma = round_to_1_bit(luma)
|
||||
return (rounded_luma, rounded_luma, rounded_luma, round_to_1_bit(a))
|
||||
|
||||
|
||||
def truncate_color_to_pebble64_palette(r, g, b, a):
|
||||
"""
|
||||
converts each rgba32 pixel to the next lower matching color (truncate method)
|
||||
in the pebble palette
|
||||
returns the truncated color as a rgba32 color triplet (r, g, b, a)
|
||||
"""
|
||||
|
||||
a = (a / 85) * 85 # truncate alpha for 2bit color range
|
||||
# clear transparent pixels (makes image more compress-able)
|
||||
# and required for greyscale tests
|
||||
if a == 0:
|
||||
r, g, b = (0, 0, 0)
|
||||
else:
|
||||
r = (r / 85) * 85 # truncate for 2bit color range
|
||||
g = (g / 85) * 85 # truncate for 2bit color range
|
||||
b = (b / 85) * 85 # truncate for 2bit color range
|
||||
|
||||
return r, g, b, a
|
||||
|
||||
|
||||
def truncate_color_to_pebble2_palette(r, g, b, a):
|
||||
"""
|
||||
converts each rgba32 pixel to the next lower matching color (truncate method)
|
||||
returns closest rgba32 color triplet (r, g, b, a)
|
||||
"""
|
||||
|
||||
if a != 255:
|
||||
a = 0
|
||||
|
||||
if r == 255 and g == 255 and b == 255:
|
||||
return r, g, b, a
|
||||
else:
|
||||
return 0, 0, 0, a
|
||||
|
||||
|
||||
def rgba32_triplet_to_argb8(r, g, b, a):
|
||||
"""
|
||||
converts a 32-bit RGBA color by channel to an ARGB8 (1 byte containing all 4 channels)
|
||||
"""
|
||||
a, r, g, b = (a >> 6, r >> 6, g >> 6, b >> 6)
|
||||
argb8 = (a << 6) | (r << 4) | (g << 2) | b
|
||||
return argb8
|
||||
|
||||
|
||||
# convert 32-bit color (r, g, b, a) to 32-bit RGBA word
|
||||
def rgba32_triplet_to_rgba32(r, g, b, a):
|
||||
return (((r & 0xFF) << 24) | ((g & 0xFF) << 16) | ((b & 0xFF) << 8) | (a & 0xFF))
|
||||
|
||||
|
||||
# takes number of colors and outputs PNG & PBI compatible bit depths for paletted images
|
||||
def num_colors_to_bitdepth(num_colors):
|
||||
bitdepth = int(math.ceil(math.log(num_colors, 2)))
|
||||
|
||||
# only bitdepth 1,2,4 and 8 supported by PBI and PNG
|
||||
if bitdepth == 0:
|
||||
# caused when palette has only 1 color
|
||||
bitdepth = 1
|
||||
elif bitdepth == 3:
|
||||
bitdepth = 4
|
||||
elif bitdepth > 4:
|
||||
bitdepth = 8
|
||||
|
||||
return bitdepth
|
||||
485
third_party/pbl/pblconvert/pblconvert/svg2pdc/svg.py
vendored
Normal file
485
third_party/pbl/pblconvert/pblconvert/svg2pdc/svg.py
vendored
Normal file
@@ -0,0 +1,485 @@
|
||||
# coding=utf-8
|
||||
from exceptions import *
|
||||
from StringIO import StringIO
|
||||
from functools import partial
|
||||
from lxml import etree
|
||||
import cairosvg
|
||||
from cairosvg.parser import Tree
|
||||
from cairosvg.surface import size, node_format, normalize, gradient_or_pattern, color
|
||||
from cairosvg.surface.helpers import point, paint
|
||||
import io
|
||||
from pdc import PathCommand, CircleCommand, extend_bounding_box, bounding_box_around_points
|
||||
from annotation import Annotation, NS_ANNOTATION, PREFIX_ANNOTATION, TAG_HIGHLIGHT
|
||||
from pebble_image_routines import truncate_color_to_pebble64_palette, rgba32_triplet_to_argb8
|
||||
|
||||
try:
|
||||
import cairocffi as cairo
|
||||
# OSError means cairocffi is installed,
|
||||
# but could not load a cairo dynamic library.
|
||||
# pycairo may still be available with a statically-linked cairo.
|
||||
except (ImportError, OSError):
|
||||
import cairo # pycairo
|
||||
|
||||
|
||||
def cairo_from_png(path):
|
||||
surface = cairo.ImageSurface.create_from_png(path)
|
||||
return surface, cairo.Context(surface)
|
||||
|
||||
|
||||
class PDCContext(cairo.Context):
|
||||
def line_to(self, x, y):
|
||||
super(PDCContext, self).line_to(x, y)
|
||||
|
||||
|
||||
# http://effbot.org/zone/element-lib.htm#prettyprint
|
||||
def indent(elem, level=0):
|
||||
i = "\n" + level*" "
|
||||
if len(elem):
|
||||
if not elem.text or not elem.text.strip():
|
||||
elem.text = i + " "
|
||||
if not elem.tail or not elem.tail.strip():
|
||||
elem.tail = i
|
||||
for elem in elem:
|
||||
indent(elem, level+1)
|
||||
if not elem.tail or not elem.tail.strip():
|
||||
elem.tail = i
|
||||
else:
|
||||
if level and (not elem.tail or not elem.tail.strip()):
|
||||
elem.tail = i
|
||||
|
||||
|
||||
class PDCSurface(cairosvg.surface.PNGSurface):
|
||||
# noinspection PyMissingConstructor
|
||||
def __init__(self, tree, output, dpi, parent_surface=None, approximate_bezier=False):
|
||||
self.svg_tree = tree
|
||||
self.cairo = None
|
||||
self.cairosvg_tags = []
|
||||
self.pdc_commands = []
|
||||
self.approximate_bezier = approximate_bezier
|
||||
self.stored_size = None
|
||||
self.context_width, self.context_height = None, None
|
||||
self.cursor_position = [0, 0]
|
||||
self.cursor_d_position = [0, 0]
|
||||
self.text_path_width = 0
|
||||
self.tree_cache = {(tree.url, tree["id"]): tree}
|
||||
if parent_surface:
|
||||
self.markers = parent_surface.markers
|
||||
self.gradients = parent_surface.gradients
|
||||
self.patterns = parent_surface.patterns
|
||||
self.masks = parent_surface.masks
|
||||
self.paths = parent_surface.paths
|
||||
self.filters = parent_surface.filters
|
||||
else:
|
||||
self.markers = {}
|
||||
self.gradients = {}
|
||||
self.patterns = {}
|
||||
self.masks = {}
|
||||
self.paths = {}
|
||||
self.filters = {}
|
||||
self.page_sizes = []
|
||||
self._old_parent_node = self.parent_node = None
|
||||
self.output = output
|
||||
self.dpi = dpi
|
||||
self.font_size = size(self, "12pt")
|
||||
self.stroke_and_fill = True
|
||||
# we only support px
|
||||
for unit in ["mm", "cm", "in", "pt", "pc"]:
|
||||
for attr in ["width", "height"]:
|
||||
value = tree.get(attr)
|
||||
if value is not None and unit in value:
|
||||
tree.pop(attr)
|
||||
value = None
|
||||
|
||||
width, height, viewbox = node_format(self, tree)
|
||||
# Actual surface dimensions: may be rounded on raster surfaces types
|
||||
self.cairo, self.width, self.height = self._create_surface(
|
||||
width * self.device_units_per_user_units,
|
||||
height * self.device_units_per_user_units)
|
||||
self.page_sizes.append((self.width, self.height))
|
||||
self.context = PDCContext(self.cairo)
|
||||
# We must scale the context as the surface size is using physical units
|
||||
self.context.scale(
|
||||
self.device_units_per_user_units, self.device_units_per_user_units)
|
||||
|
||||
# SVG spec says "viewbox of size 0 means 'don't render'"
|
||||
if viewbox is not None and viewbox[2] <= 0 and viewbox[3] <= 0:
|
||||
return
|
||||
|
||||
# Initial, non-rounded dimensions
|
||||
self.set_context_size(width, height, viewbox)
|
||||
self.context.move_to(0, 0)
|
||||
|
||||
# register PDC namespace and add temporary fake attribute to propagate prefix
|
||||
etree.register_namespace(PREFIX_ANNOTATION, NS_ANNOTATION)
|
||||
ns_fake_attr = "{%s}foo" % NS_ANNOTATION
|
||||
tree.node.set(ns_fake_attr, "bar")
|
||||
|
||||
# remove all PDC elements (annotations in case we're processing an annotated SVG)
|
||||
def remove_pdc_elements(elem):
|
||||
for child in elem:
|
||||
if isinstance(child.tag, str) and child.tag.startswith("{%s}" % NS_ANNOTATION):
|
||||
elem.remove(child)
|
||||
else:
|
||||
remove_pdc_elements(child)
|
||||
|
||||
remove_pdc_elements(tree.node)
|
||||
self.draw_root(tree)
|
||||
tree.node.attrib.pop(ns_fake_attr)
|
||||
|
||||
def size(self):
|
||||
if self.stored_size is not None:
|
||||
return self.stored_size
|
||||
|
||||
result = None
|
||||
for command in self.pdc_commands:
|
||||
result = extend_bounding_box(result, rect2=command.bounding_box())
|
||||
|
||||
if result is None:
|
||||
return (0, 0)
|
||||
|
||||
# returned size is diagonal from (0, 0) to max_x/max_y
|
||||
return (max(0, result[0] + result[2]), max(0, result[1] + result[3]))
|
||||
|
||||
def cairo_tag_func(self, tag):
|
||||
return self.cairosvg_tags[0][tag]
|
||||
|
||||
def cairo_tags_push_and_wrap(self):
|
||||
self.cairosvg_tags.append(cairosvg.surface.TAGS.copy())
|
||||
custom_impl = {"polyline": polyline, "polygon": polygon, "line": line, "rect": rect, "circle": circle,
|
||||
"path": path, "svg": svg}
|
||||
for k,v in custom_impl.iteritems():
|
||||
original = self.cairo_tag_func(k)
|
||||
cairosvg.surface.TAGS[k] = partial(custom_impl[k], original=original)
|
||||
|
||||
def draw_root(self, node):
|
||||
if node.get("display", "").upper() == 'NONE':
|
||||
node.annotations = []
|
||||
Annotation(node, 'Attribute display="none" for root element will be ignored.')
|
||||
node.pop("display")
|
||||
|
||||
super(PDCSurface, self).draw_root(node)
|
||||
|
||||
def draw(self, node):
|
||||
self.cairo_tags_push_and_wrap()
|
||||
if not hasattr(node, "annotations"):
|
||||
node.annotations = []
|
||||
super(PDCSurface, self).draw(node)
|
||||
cairosvg.surface.TAGS = self.cairosvg_tags.pop()
|
||||
|
||||
def element_tree(self):
|
||||
indent(self.svg_tree.node)
|
||||
# ensure that viewbox is always set
|
||||
view_box = self.svg_tree.node.get("viewBox", "0 0 %d %d" % self.size())
|
||||
self.svg_tree.node.set("viewBox", view_box)
|
||||
|
||||
return etree.ElementTree(self.svg_tree.node)
|
||||
|
||||
def render_annoations_on_top(self, png_path):
|
||||
surface, ctx = cairo_from_png(png_path)
|
||||
|
||||
def iterate(elem):
|
||||
if TAG_HIGHLIGHT == elem.tag:
|
||||
args = [float(elem.get(k, "1")) for k in ["x", "y", "width", "height"]]
|
||||
ctx.rectangle(*args)
|
||||
ctx.set_source_rgb(1, 0, 0)
|
||||
ctx.stroke()
|
||||
|
||||
for child in elem:
|
||||
iterate(child)
|
||||
|
||||
iterate(self.svg_tree.node)
|
||||
result = StringIO()
|
||||
surface.write_to_png(result)
|
||||
return result.getvalue()
|
||||
|
||||
|
||||
def gcolor8_is_visible(color):
|
||||
return (color & 0b11000000) != 0
|
||||
|
||||
|
||||
def svg_color(surface, node, opacity, attribute, default=None):
|
||||
paint_source, paint_color = paint(node.get(attribute, default))
|
||||
if gradient_or_pattern(surface, node, paint_source):
|
||||
return 0
|
||||
(r, g, b, a) = color(paint_color, opacity)
|
||||
color256 = truncate_color_to_pebble64_palette(int(r*255), int(g*255), int(b*255), int(a*255))
|
||||
gcolor8 = rgba32_triplet_to_argb8(*color256)
|
||||
return gcolor8 if gcolor8_is_visible(gcolor8) else 0
|
||||
|
||||
|
||||
def svg(surface, node, original=None):
|
||||
original(surface, node)
|
||||
width, height, viewbox = node_format(surface, node)
|
||||
if not "width" in node:
|
||||
width = None if viewbox is None else viewbox[2]
|
||||
if not "height" in node:
|
||||
height = None if viewbox is None else viewbox[3]
|
||||
if (width is not None) and (height is not None):
|
||||
surface.stored_size = (width, height)
|
||||
|
||||
|
||||
def line(surface, node, original=None):
|
||||
x1, y1, x2, y2 = tuple(
|
||||
size(surface, node.get(position), position[0])
|
||||
for position in ("x1", "y1", "x2", "y2"))
|
||||
points = [(x1, y1), (x2, y2)]
|
||||
command = PathCommand(points, path_open=True, precise=True)
|
||||
handle_command(surface, node, command)
|
||||
|
||||
return original(surface, node)
|
||||
|
||||
def polygon(surface, node, original=None):
|
||||
return poly_element(surface, node, open=False, original=original)
|
||||
|
||||
|
||||
def polyline(surface, node, original=None):
|
||||
return poly_element(surface, node, open=True, original=original)
|
||||
|
||||
|
||||
def poly_element(surface, node, open, original=None):
|
||||
points_attr = normalize(node.get("points"))
|
||||
points = []
|
||||
while points_attr:
|
||||
x, y, points_attr = point(surface, points_attr)
|
||||
points.append((x, y))
|
||||
command = PathCommand(points, path_open=True, precise=True)
|
||||
command.open = open
|
||||
handle_command(surface, node, command)
|
||||
|
||||
return original(surface, node)
|
||||
|
||||
|
||||
def rect(surface, node, original=None):
|
||||
x, y = size(surface, node.get("x"), "x"), size(surface, node.get("y"), "y")
|
||||
width = size(surface, node.get("width"), "x")
|
||||
height = size(surface, node.get("height"), "y")
|
||||
|
||||
rx = node.get("rx")
|
||||
ry = node.get("ry")
|
||||
if rx and (ry is None):
|
||||
ry = rx
|
||||
elif ry and (rx is None):
|
||||
rx = ry
|
||||
rx = size(surface, rx, "x")
|
||||
ry = size(surface, ry, "y")
|
||||
if not (rx == 0 and ry == 0):
|
||||
annotation = Annotation(node, "Rounded rectangles are not supported.")
|
||||
right = x + width - rx
|
||||
bottom = y + height - ry
|
||||
annotation.add_highlight(x, y, rx, ry)
|
||||
annotation.add_highlight(right, y, rx, ry)
|
||||
annotation.add_highlight(right, bottom, rx, ry)
|
||||
annotation.add_highlight(x, bottom, rx, ry)
|
||||
|
||||
points = [(x, y), (x + width, y), (x + width, y + height), (x, y + height)]
|
||||
command = PathCommand(points, path_open=False, precise=True)
|
||||
handle_command(surface, node, command)
|
||||
|
||||
return original(surface, node)
|
||||
|
||||
|
||||
def circle(surface, node, original=None):
|
||||
r = size(surface, node.get("r"))
|
||||
cx = size(surface, node.get("cx"), "x")
|
||||
cy = size(surface, node.get("cy"), "y")
|
||||
|
||||
if r and cx is not None and cy is not None:
|
||||
command = CircleCommand((cx, cy), r)
|
||||
handle_command(surface, node, command)
|
||||
|
||||
return original(surface, node)
|
||||
|
||||
|
||||
def cubicbezier_mid(p0, p1, p2, p3, min_dist, l, r):
|
||||
t = (r[0] + l[0]) / 2
|
||||
a = (1. - t)**3
|
||||
b = 3. * t * (1. - t)**2
|
||||
c = 3.0 * t**2 * (1.0 - t)
|
||||
d = t**3
|
||||
|
||||
p = (a * p0[0] + b * p1[0] + c * p2[0] + d * p3[0],
|
||||
a * p0[1] + b * p1[1] + c * p2[1] + d * p3[1])
|
||||
|
||||
def pt_dist(p1, p2):
|
||||
return ((p1[0]-p2[0])**2 + (p1[1]-p2[1])**2)**0.5
|
||||
|
||||
if pt_dist(l[1], p) <= min_dist or pt_dist(r[1], p) <= min_dist:
|
||||
return []
|
||||
|
||||
left = cubicbezier_mid(p0, p1, p2, p3, min_dist=min_dist, l=l, r=(t, p))
|
||||
right = cubicbezier_mid(p0, p1, p2, p3, min_dist=min_dist, l=(t, p), r=r)
|
||||
|
||||
return left + [p] + right
|
||||
|
||||
def cubicbezier(p0, p1, p2, p3, min_dist):
|
||||
return [p0] + cubicbezier_mid(p0, p1, p2, p3, min_dist, (0.0, p0), (1.0, p3)) + [p3]
|
||||
|
||||
|
||||
class PathSurfaceContext(cairo.Context):
|
||||
def __init__(self, target, node, approximate_bezier):
|
||||
super(PathSurfaceContext, self).__init__(target)
|
||||
self.points = []
|
||||
self.path_open = True
|
||||
self.node = node
|
||||
self.approximate_bezier = approximate_bezier
|
||||
self.grouped_annotations = {}
|
||||
|
||||
def get_grouped_description(self, description, *args, **kwargs):
|
||||
if not description in self.grouped_annotations:
|
||||
self.grouped_annotations[description] = self.add_annotation(description, *args, **kwargs)
|
||||
|
||||
return self.grouped_annotations[description]
|
||||
|
||||
def add_annotation(self, *args, **kwargs):
|
||||
return Annotation(self.node, *args, **kwargs)
|
||||
|
||||
def add_current_point(self):
|
||||
self.points.append(self.get_current_point())
|
||||
|
||||
def create_command(self):
|
||||
return PathCommand(self.points, self.path_open, precise=True)
|
||||
|
||||
def move_to(self, x, y):
|
||||
super(PathSurfaceContext, self).move_to(x, y)
|
||||
self.add_current_point()
|
||||
|
||||
def line_to(self, x, y):
|
||||
super(PathSurfaceContext, self).line_to(x, y)
|
||||
self.add_current_point()
|
||||
|
||||
def rel_line_to(self, dx, dy):
|
||||
super(PathSurfaceContext, self).rel_line_to(dx, dy)
|
||||
self.add_current_point()
|
||||
|
||||
def curve_to(self, x1, y1, x2, y2, x3, y3):
|
||||
first = self.get_current_point()
|
||||
super(PathSurfaceContext, self).curve_to(x1, y1, x2, y2, x3, y3)
|
||||
last = self.get_current_point()
|
||||
|
||||
# approximate bezier curve
|
||||
points = cubicbezier(first, (x1, y1), (x2, y2), last, 5)
|
||||
box = bounding_box_around_points(points)
|
||||
|
||||
if self.approximate_bezier:
|
||||
description = "Curved command(s) were approximated."
|
||||
self.points.extend(points[1:])
|
||||
else:
|
||||
description = "Element contains unsupported curved command(s)."
|
||||
self.add_current_point()
|
||||
|
||||
link = "https://pebbletechnology.atlassian.net/wiki/display/DEV/Pebble+Draw+Commands#PebbleDrawCommands-issue-bezier"
|
||||
self.get_grouped_description("Element contains unsupported curved command(s).", link=link).add_highlight(*box)
|
||||
|
||||
|
||||
def rel_curve_to(self, dx1, dy1, dx2, dy2, dx3, dy3):
|
||||
cur = self.get_current_point()
|
||||
x1 = dx1 + cur[0]
|
||||
y1 = dy1 + cur[1]
|
||||
x2 = dx2 + cur[0]
|
||||
y2 = dy2 + cur[1]
|
||||
x3 = dx3 + cur[0]
|
||||
y3 = dy3 + cur[1]
|
||||
self.curve_to(x1, y1, x2, y2, x3, y3)
|
||||
|
||||
def arc(self, xc, yc, radius, angle1, angle2):
|
||||
self.add_annotation_arc_unsupported(xc, yc, radius)
|
||||
super(PathSurfaceContext, self).arc(xc, yc, radius, angle1, angle2)
|
||||
|
||||
def arc_negative(self, xc, yc, radius, angle1, angle2):
|
||||
self.add_annotation_arc_unsupported(xc, yc, radius)
|
||||
super(PathSurfaceContext, self).arc_negative(xc, yc, radius, angle1, angle2)
|
||||
|
||||
def close_path(self):
|
||||
super(PathSurfaceContext, self).close_path()
|
||||
self.path_open = False
|
||||
|
||||
def add_annotation_arc_unsupported(self, xc, yc, radius):
|
||||
# caller uses context transforms to express arcs
|
||||
points = [
|
||||
(xc-radius, yc-radius), # top-left
|
||||
(xc+radius, yc-radius), # top-right
|
||||
(xc+radius, yc+radius), # bottom-right
|
||||
(xc-radius, yc+radius), # bottom-left
|
||||
]
|
||||
box = bounding_box_around_points([self.user_to_device(*p) for p in points])
|
||||
self.get_grouped_description("Element contains unsupported arc command(s).").add_highlight(*box)
|
||||
|
||||
|
||||
class PathSurface:
|
||||
def __init__(self, target, node, approximate_bezier):
|
||||
self.context = PathSurfaceContext(target, node, approximate_bezier)
|
||||
|
||||
|
||||
def path(surface, node, original=None):
|
||||
# unfortunately, the original implementation has a side-effect on node
|
||||
# but as it's rather complex, we'd rather reuse it and as it doesn't change the surface
|
||||
# it's ok to call it with the fake surface we create here
|
||||
collecting_surface = PathSurface(surface.cairo, node, surface.approximate_bezier)
|
||||
result = original(collecting_surface, node)
|
||||
command = collecting_surface.context.create_command()
|
||||
if len(command.points) > 1:
|
||||
handle_command(surface, node, command)
|
||||
else:
|
||||
Annotation(node, "Path needs at least two points.")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class Transformer:
|
||||
def __init__(self, cairo, node):
|
||||
self.context = cairo
|
||||
self.node = node
|
||||
|
||||
def transform_point(self, pt):
|
||||
return self.context.user_to_device(pt[0], pt[1])
|
||||
|
||||
def transform_distance(self, dx, dy):
|
||||
return self.context.user_to_device_distance(dx, dy)
|
||||
|
||||
def add_annotation(self, *args, **kwargs):
|
||||
return Annotation(self.node, *args, **kwargs)
|
||||
|
||||
def handle_command(surface, node, command):
|
||||
opacity = float(node.get("opacity", 1))
|
||||
# Get stroke and fill opacity
|
||||
stroke_opacity = float(node.get("stroke-opacity", 1))
|
||||
fill_opacity = float(node.get("fill-opacity", 1))
|
||||
if opacity < 1:
|
||||
stroke_opacity *= opacity
|
||||
fill_opacity *= opacity
|
||||
default_fill = "black" if node.get("fill-rule") == "evenodd" else None
|
||||
command.fill_color = svg_color(surface, node, fill_opacity, "fill", default_fill)
|
||||
command.stroke_color = svg_color(surface, node, stroke_opacity, "stroke")
|
||||
if gcolor8_is_visible(command.stroke_color):
|
||||
command.stroke_width = int(size(surface, node.get("stroke-width")))
|
||||
if command.stroke_width == 0:
|
||||
command.stroke_color = 0
|
||||
|
||||
# transform
|
||||
transformer = Transformer(surface.context, node)
|
||||
command.transform(transformer)
|
||||
if command.stroke_width and node.get("vector-effect") != "non-scaling-stroke":
|
||||
transformed_stroke = transformer.transform_distance(command.stroke_width, 0)
|
||||
transformed_stroke_width = (transformed_stroke[0]**2 + transformed_stroke[1]**2)**0.5
|
||||
command.stroke_width = transformed_stroke_width
|
||||
for annotation in node.annotations:
|
||||
annotation.transform(transformer)
|
||||
|
||||
command.finalize(transformer)
|
||||
|
||||
# Manage display and visibility
|
||||
display = node.get("display", "inline") != "none"
|
||||
visible = display and (node.get("visibility", "visible") != "hidden") and \
|
||||
(gcolor8_is_visible(command.fill_color) or gcolor8_is_visible(command.stroke_color))
|
||||
|
||||
if visible:
|
||||
surface.pdc_commands.append(command)
|
||||
|
||||
|
||||
def surface_from_svg(url=None, bytestring=None, approximate_bezier=False):
|
||||
try:
|
||||
tree = Tree(url=url, bytestring=bytestring)
|
||||
except etree.XMLSyntaxError as e:
|
||||
raise Svg2PdcFormatError(e.args[0])
|
||||
output = io.BytesIO()
|
||||
return PDCSurface(tree, output, 96, approximate_bezier=approximate_bezier)
|
||||
2
third_party/pbl/pblconvert/pblconvert/version.py
vendored
Normal file
2
third_party/pbl/pblconvert/pblconvert/version.py
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
__version_info__ = (0, 0, 2)
|
||||
__version__ = '.'.join(map(str, __version_info__))
|
||||
Reference in New Issue
Block a user