blob: ad11a4b68145be57e1df6af7226a12eb740d8307 [file] [log] [blame]
# -*- coding: utf-8 -*-
# Copyright 2010-2014, Google Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following disclaimer
# in the documentation and/or other materials provided with the
# distribution.
# * Neither the name of Google Inc. nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""Utilities to create Mozc's .pic file from .svg file.
This module provides some utilities to create a .pic file, which is interpreted
in MechaMozc, from .svg files.
.pic file is a Mozc original format to represent vector image data shared by
all android platforms.
Note: another idea was to use binary data which is saved via PictureDrawable
on Android, but it seems that there are some differencces between devices,
and it'd cause an crash error, unfortunately.
"""
__author__ = "hidehiko"
import cStringIO as StringIO
import logging
import optparse
import os
import re
import struct
import sys
from xml.etree import cElementTree as ElementTree
from build_tools import util
# State const values defined in android.R.attr.
STATE_PRESSED = 0x010100a7
STATE_SELECTED = 0x010100a1
STATE_CHECKED = 0x010100a0
DRAWABLE_PICTURE = 1
DRAWABLE_STATE_LIST = 2
CMD_PICTURE_EOP = 0
CMD_PICTURE_DRAW_PATH = 1
CMD_PICTURE_DRAW_POLYLINE = 2
CMD_PICTURE_DRAW_POLYGON = 3
CMD_PICTURE_DRAW_LINE = 4
CMD_PICTURE_DRAW_RECT = 5
CMD_PICTURE_DRAW_CIRCLE = 6
CMD_PICTURE_DRAW_ELLIPSE = 7
CMD_PICTURE_DRAW_GROUP_START = 8
CMD_PICTURE_DRAW_GROUP_END = 9
CMD_PICTURE_PATH_EOP = 0
CMD_PICTURE_PATH_MOVE = 1
CMD_PICTURE_PATH_LINE = 2
CMD_PICTURE_PATH_HORIZONTAL_LINE = 3
CMD_PICTURE_PATH_VERTICAL_LINE = 4
CMD_PICTURE_PATH_CURVE = 5
CMD_PICTURE_PATH_CONTINUED_CURVE = 6
CMD_PICTURE_PATH_CLOSE = 7
CMD_PICTURE_PAINT_EOP = 0
CMD_PICTURE_PAINT_STYLE = 1
CMD_PICTURE_PAINT_COLOR = 2
CMD_PICTURE_PAINT_SHADOW = 3
CMD_PICTURE_PAINT_STROKE_WIDTH = 4
CMD_PICTURE_PAINT_STROKE_CAP = 5
CMD_PICTURE_PAINT_STROKE_JOIN = 6
CMD_PICTURE_PAINT_SHADER = 7
CMD_PICTURE_SHADER_LINEAR_GRADIENT = 1
CMD_PICTURE_SHADER_RADIAL_GRADIENT = 2
STYLE_CATEGORY_TAG = 128
STYLE_CATEGORY_KEYICON_MAIN = 0
STYLE_CATEGORY_KEYICON_GUIDE = 1
STYLE_CATEGORY_KEYICON_GUIDE_LIGHT = 2
STYLE_CATEGORY_KEYICON_MAIN_HIGHLIGHT = 3
STYLE_CATEGORY_KEYICON_GUIDE_HIGHLIGHT = 4
STYLE_CATEGORY_KEYICON_BOUND = 5
STYLE_CATEGORY_KEYICON_FUNCTION = 6
STYLE_CATEGORY_KEYICON_FUNCTION_DARK = 7
STYLE_CATEGORY_KEYICON_QWERTY_SHIFT_ON_ARROW = 8
STYLE_CATEGORY_KEYICON_QWERTY_CAPS_ON_ARROW = 9
STYLE_CATEGORY_KEYPOPUP_HIGHLIGHT = 10
STYLE_CATEGORY_KEYICON_POPUP_FUNCTION = 11
STYLE_CATEGORY_KEYICON_POPUP_FUNCTION_DARK = 12
# We may be able to reuse same resources for symbol major/minor icons.
STYLE_CATEGORY_SYMBOL_MAJOR = 13
STYLE_CATEGORY_SYMBOL_MAJOR_SELECTED = 14
STYLE_CATEGORY_SYMBOL_MINOR = 15
STYLE_CATEGORY_SYMBOL_MINOR_SELECTED = 16
STYLE_CATEGORY_KEYBOARD_FOLDING_BUTTON_BACKGROUND = 17
# We'll check the category id by reverse sorted order, to resolve prefix match
# confliction.
STYLE_CATEGORY_MAP = sorted(
[
('style-keyicon-main', STYLE_CATEGORY_KEYICON_MAIN),
('style-keyicon-guide', STYLE_CATEGORY_KEYICON_GUIDE),
('style-keyicon-guide-light', STYLE_CATEGORY_KEYICON_GUIDE_LIGHT),
('style-keyicon-main-highlight', STYLE_CATEGORY_KEYICON_MAIN_HIGHLIGHT),
('style-keyicon-guide-highlight',
STYLE_CATEGORY_KEYICON_GUIDE_HIGHLIGHT),
('style-keyicon-bound', STYLE_CATEGORY_KEYICON_BOUND),
('style-keyicon-function', STYLE_CATEGORY_KEYICON_FUNCTION),
('style-keyicon-function-dark', STYLE_CATEGORY_KEYICON_FUNCTION_DARK),
('style-keyicon-qwerty-shift-on-arrow',
STYLE_CATEGORY_KEYICON_QWERTY_SHIFT_ON_ARROW),
('style-keyicon-qwerty-caps-on-arrow',
STYLE_CATEGORY_KEYICON_QWERTY_CAPS_ON_ARROW),
('style-keypopup-highlight', STYLE_CATEGORY_KEYPOPUP_HIGHLIGHT),
('style-keyicon-popup-function',
STYLE_CATEGORY_KEYICON_POPUP_FUNCTION),
('style-keyicon-popup-function-dark',
STYLE_CATEGORY_KEYICON_POPUP_FUNCTION_DARK),
('style-symbol-major', STYLE_CATEGORY_SYMBOL_MAJOR),
('style-symbol-major-selected', STYLE_CATEGORY_SYMBOL_MAJOR_SELECTED),
('style-symbol-minor', STYLE_CATEGORY_SYMBOL_MINOR),
('style-symbol-minor-selected', STYLE_CATEGORY_SYMBOL_MINOR_SELECTED),
('style-keyboard-folding-button-background',
STYLE_CATEGORY_KEYBOARD_FOLDING_BUTTON_BACKGROUND),
],
reverse=True)
# Format of PictureDrawable:
# 1, width, height, {CMD_PICTURE_XXX sequence}.
#
# Format of StateListDrawable:
# 2, [[state_list], drawable]
COLOR_PATTERN = re.compile(r'#([0-9A-Fa-f]{6})')
PIXEL_PATTERN = re.compile(r'(\d+)(?:px)?')
FLOAT_PATTERN = re.compile(r'^\s*,?\s*([+-]?\d+(?:\.\d*)?(:?e[+-]\d+)?)')
SHADER_PATTERN = re.compile(r'url\(#(.*)\)')
TRANSFORM_PATTERN = re.compile(r'(matrix|translate)\((.*)\)')
MATRIX_PATTERN = re.compile(r'matrix\((.*)\)')
STOP_COLOR_PATTERN = re.compile(r'stop-color:(.*)')
NO_SHADOW = 0
HAS_SHADOW = 1
HAS_BELOW_SHADOW = 2
class _OutputStream(object):
"""Simple wrapper of output stream by by packing value in big endian."""
def __init__(self, output):
self.output = output
def WriteByte(self, value):
if not (0 <= value <= 255):
logging.critical('overflow')
sys.exit(1)
self.output.write(struct.pack('>B', value & 0xFF))
def WriteInt16(self, value):
if not (0 <= value <= 65535):
logging.critical('overflow')
sys.exit(1)
self.output.write(struct.pack('>H', value & 0xFFFF))
def WriteInt32(self, value):
self.output.write(struct.pack('>I', value & 0xFFFFFFFF))
def WriteFloat(self, value):
# TODO(hidehiko): Because the precision of original values is thousandth,
# so we can compress the data by using fixed-precision values.
self.output.write(struct.pack('>f', value))
def __enter__(self):
self.output.__enter__()
def __exit__(self):
self.output.__exit__()
class MozcDrawableConverter(object):
"""Converter from .svg file to .pic file."""
def __init__(self):
pass
# Definitions of svg parsing utilities.
def _ParseColor(self, color):
"""Parses color attribute and returns int32 value or None."""
if color == 'none':
return None
m = COLOR_PATTERN.match(color)
if not m:
return None
c = int(m.group(1), 16)
if c < 0 or 0x1000000 <= c:
logging.critical('Out of color range: %s', color)
sys.exit(1)
# Set alpha.
return c | 0xFF000000
def _ParseShader(self, color, shader_map):
"""Parses shader attribute and returns a shader name from the given map."""
if color == 'none':
return None
m = SHADER_PATTERN.match(color)
if not m:
return None
return shader_map[m.group(1)]
def _ParsePixel(self, s):
"""Parses pixel size from the given attribute value."""
m = PIXEL_PATTERN.match(s)
if not m:
return None
return int(m.group(1))
def _ConsumeFloat(self, s):
"""Returns one floating value from string.
Args:
s: the target of parsing.
Return:
A tuple of the parsed floating value and the remaining string.
"""
m = FLOAT_PATTERN.search(s)
if not m:
logging.critical('failed to consume float: %s', s)
sys.exit(1)
return float(m.group(1)), s[m.end():]
def _ConsumeFloatList(self, s, num):
"""Parses num floating values from s."""
result = []
for _ in xrange(num):
value, s = self._ConsumeFloat(s)
result.append(value)
return result, s
def _ParseFloatList(self, s):
"""Parses floating list from string."""
s = s.strip()
result = []
while s:
value, s = self._ConsumeFloat(s)
result.append(value)
return result
def _ParseShaderMap(self, node):
if node.tag in ['{http://www.w3.org/2000/svg}g',
'{http://www.w3.org/2000/svg}svg']:
result = {}
for child in node:
result.update(self._ParseShaderMap(child))
return result
if node.tag == '{http://www.w3.org/2000/svg}linearGradient':
element_id = node.get('id')
x1 = float(node.get('x1'))
y1 = float(node.get('y1'))
x2 = float(node.get('x2'))
y2 = float(node.get('y2'))
gradientTransform = node.get('gradientTransform')
if gradientTransform:
m = MATRIX_PATTERN.match(gradientTransform)
(m11, m21, m12, m22, m13, m23) = self._ParseFloatList(m.group(1))
(x1, y1) = (m11 * x1 + m12 * y1 + m13, m21 * x1 + m22 * y1 + m23)
(x2, y2) = (m11 * x2 + m12 * y2 + m13, m21 * x2 + m22 * y2 + m23)
color_list = self._ParseStopList(node)
return { element_id: ('linear', x1, y1, x2, y2, color_list) }
if node.tag == '{http://www.w3.org/2000/svg}radialGradient':
element_id = node.get('id')
cx = float(node.get('cx'))
cy = float(node.get('cy'))
r = float(node.get('r'))
gradientTransform = node.get('gradientTransform')
if gradientTransform:
m = MATRIX_PATTERN.match(gradientTransform)
matrix = self._ParseFloatList(m.group(1))
else:
matrix = None
color_list = self._ParseStopList(node)
return { element_id: ('radial', cx, cy, r, matrix, color_list) }
return {}
def _ParseStyle(self, node, has_shadow, shader_map):
"""Parses style attribute of the given node."""
result = {}
for attr in node.get('style', '').split(';'):
attr = attr.strip()
if not attr:
continue
command, arg = attr.split(':')
if command == 'fill' or command == 'stroke':
shader = self._ParseShader(arg, shader_map)
color = self._ParseColor(arg)
if shader is None and color is None:
if arg != 'none':
logging.error('Unknown pattern: %s', arg)
continue
paint_map = {}
paint_map['style'] = command
if shader is not None:
paint_map['shader'] = shader
if color is not None:
paint_map['color'] = color
paint_map['shadow'] = has_shadow
result[command] = paint_map
continue
if command == 'stroke-width':
paint_map = result['stroke']
paint_map['stroke-width'] = float(arg)
continue
if command == 'stroke-linecap':
paint_map = result['stroke']
paint_map['stroke-linecap'] = arg
continue
if command == 'stroke-linejoin':
paint_map = result['stroke']
paint_map['stroke-linejoin'] = arg
continue
return sorted(result.values(), key=lambda e: e['style'])
def _ParseStopList(self, parent_node):
result = []
for stop in parent_node:
if stop.tag != '{http://www.w3.org/2000/svg}stop':
logging.critical('unknown elem: %s', stop.tag)
sys.exit(1)
offset = float(stop.get('offset'))
color = self._ParseColor(
STOP_COLOR_PATTERN.match(stop.get('style')).group(1))
result.append((offset, color))
return result
# Definition of conversion utilities.
def _ConvertSize(self, tree, output):
node = tree.getroot()
width = self._ParsePixel(node.get('width'))
height = self._ParsePixel(node.get('height'))
if width is None or height is None:
logging.critical('Unknown size')
sys.exit(1)
output.WriteInt16(width)
output.WriteInt16(height)
def _MaybeConvertShadow(self, has_shadow, output):
if has_shadow == HAS_SHADOW:
output.WriteByte(CMD_PICTURE_PAINT_SHADOW)
output.WriteFloat(2.)
output.WriteFloat(0.)
output.WriteFloat(-1.)
output.WriteInt32(0xFF404040)
return
if has_shadow == HAS_BELOW_SHADOW:
output.WriteByte(CMD_PICTURE_PAINT_SHADOW)
output.WriteFloat(4.)
output.WriteFloat(0.)
output.WriteFloat(2.)
output.WriteInt32(0xFF292929)
def _ConvertStyle(self, style, output):
if style == 'fill':
output.WriteByte(CMD_PICTURE_PAINT_STYLE)
output.WriteByte(0)
return
if style == 'stroke':
output.WriteByte(CMD_PICTURE_PAINT_STYLE)
output.WriteByte(1)
return
logging.critical('unknown style: %s', style)
sys.exit(1)
def _MaybeConvertColor(self, color, output):
if color is None:
return
output.WriteByte(CMD_PICTURE_PAINT_COLOR)
output.WriteInt32(color)
def _MaybeConvertShader(self, shader, output):
if shader is None:
return
output.WriteByte(CMD_PICTURE_PAINT_SHADER)
if shader[0] == 'linear':
output.WriteByte(CMD_PICTURE_SHADER_LINEAR_GRADIENT)
for coord in shader[1:5]:
output.WriteFloat(coord)
output.WriteByte(len(shader[5]))
for _, color in shader[5]:
output.WriteInt32(color)
for offset, _ in shader[5]:
output.WriteFloat(offset)
return
if shader[0] == 'radial':
output.WriteByte(CMD_PICTURE_SHADER_RADIAL_GRADIENT)
for coord in shader[1:4]:
output.WriteFloat(coord)
# Output matrix.
if shader[4] is None:
output.WriteByte(0)
else:
output.WriteByte(1)
for coord in shader[4]:
output.WriteFloat(coord)
output.WriteByte(len(shader[5]))
for _, color in shader[5]:
output.WriteInt32(color)
for offset, _ in shader[5]:
output.WriteFloat(offset)
return
logging.critical('unknown shader: %s', shader[0])
sys.exit(1)
def _MaybeConvertStrokeWidth(self, stroke_width, output):
if stroke_width is None:
return
output.WriteByte(CMD_PICTURE_PAINT_STROKE_WIDTH)
output.WriteFloat(stroke_width)
def _MaybeConvertStrokeLinecap(self, stroke_linecap, output):
if stroke_linecap is None:
return
if stroke_linecap == 'round':
output.WriteByte(CMD_PICTURE_PAINT_STROKE_CAP)
output.WriteByte(1)
return
if stroke_linecap == 'square':
output.WriteByte(CMD_PICTURE_PAINT_STROKE_CAP)
output.WriteByte(2)
return
logging.critical('unknown stroke-linecap: %s', stroke_linecap)
sys.exit(1)
def _MaybeConvertStrokeLinejoin(self, stroke_linejoin, output):
if stroke_linejoin is None:
return
if stroke_linejoin == 'round':
output.WriteByte(CMD_PICTURE_PAINT_STROKE_JOIN)
output.WriteByte(1)
return
if stroke_linejoin == 'bevel':
output.WriteByte(CMD_PICTURE_PAINT_STROKE_JOIN)
output.WriteByte(2)
return
logging.critical('unknown stroke-linejoin: %s', stroke_linejoin)
sys.exit(1)
def _ConvertStyleMap(self, style_map, output):
self._ConvertStyle(style_map['style'], output)
self._MaybeConvertColor(style_map.get('color'), output)
self._MaybeConvertShader(style_map.get('shader'), output)
self._MaybeConvertShadow(style_map['shadow'], output)
self._MaybeConvertStrokeWidth(style_map.get('stroke-width'), output)
self._MaybeConvertStrokeLinecap(style_map.get('stroke-linecap'), output)
self._MaybeConvertStrokeLinejoin(style_map.get('stroke-linejoin'), output)
output.WriteByte(CMD_PICTURE_PAINT_EOP)
def _ConvertStyleList(self, style_list, output):
output.WriteByte(len(style_list))
for style_map in style_list:
self._ConvertStyleMap(style_map, output)
def _ConvertStyleCategory(self, style_category, output):
output.WriteByte(1)
for id_prefix, category in STYLE_CATEGORY_MAP:
if style_category.startswith(id_prefix):
output.WriteByte(STYLE_CATEGORY_TAG + category)
return
logging.critical('unknown style_category: "%s"', style_category)
sys.exit(1)
def _ConvertPath(self, node, output):
path = node.get('d')
if path is None:
logging.critical('Unknown path')
sys.exit(1)
prev_control = None
prev = None
command_list = []
prev_command = None
while True:
path = path.strip()
if not path:
break
if path[0] in 'mMcCsShHvVlLzZ':
# If the head character of path is one of the command prefix
# characters, use the character as the command and consume it.
command = path[0]
path = path[1:]
else:
# If not, use prev_command as command
# in order to support continuous commands.
command = prev_command
if command == 'm' or command == 'M':
# Move command.
(x, y), path = self._ConsumeFloatList(path, 2)
if command == 'm' and prev is not None:
x += prev[0]
y += prev[1]
command_list.append((CMD_PICTURE_PATH_MOVE, x, y))
prev = (x, y)
prev_control = None
start = (x, y)
# Subsequent coordinates are treated as implicit
# lineto (l or L) commands.
prev_command = 'l' if command == 'm' else 'L'
continue
if command == 'c' or command == 'C':
# Cubic curve.
(x1, y1, x2, y2, x, y), path = self._ConsumeFloatList(path, 6)
if command == 'c':
x1 += prev[0]
y1 += prev[1]
x2 += prev[0]
y2 += prev[1]
x += prev[0]
y += prev[1]
command_list.append((CMD_PICTURE_PATH_CURVE, x1, y1, x2, y2, x, y))
prev = (x, y)
prev_control = (x2, y2)
prev_command = command
continue
if command == 's' or command == 'S':
# Continued cubic curve.
(x2, y2, x, y), path = self._ConsumeFloatList(path, 4)
if command == 's':
x2 += prev[0]
y2 += prev[1]
x += prev[0]
y += prev[1]
# if prev_control is not None:
# x1 = 2 * prev[0] - prev_control[0]
# y1 = 2 * prev[1] - prev_control[1]
# else:
# x1, y1 = prev
command_list.append((CMD_PICTURE_PATH_CONTINUED_CURVE, x2, y2, x, y))
prev = (x, y)
prev_control = (x2, y2)
prev_command = command
continue
if command == 'h' or command == 'H':
# Horizontal line.
x, path = self._ConsumeFloat(path)
if command == 'h':
x += prev[0]
y = prev[1]
command_list.append((CMD_PICTURE_PATH_HORIZONTAL_LINE, x))
prev = (x, y)
prev_control = None
prev_command = command
continue
if command == 'v' or command == 'V':
# Vertical line.
y, path = self._ConsumeFloat(path)
if command == 'v':
y += prev[1]
x = prev[0]
command_list.append((CMD_PICTURE_PATH_VERTICAL_LINE, y))
prev = (x, y)
prev_control = None
prev_command = command
continue
if command == 'l' or command == 'L':
# Line.
(x, y), path = self._ConsumeFloatList(path, 2)
if command == 'l':
x += prev[0]
y += prev[1]
command_list.append((CMD_PICTURE_PATH_LINE, x, y))
prev = (x, y)
prev_control = None
prev_command = command
continue
if command == 'z' or command == 'Z':
# Close the path.
command_list.append((CMD_PICTURE_PATH_CLOSE,))
path = path
prev = start
prev_control = None
prev_command = command
continue
logging.critical('Unknown command: %s', path)
sys.exit(1)
command_list.append((CMD_PICTURE_PATH_EOP,))
# Output.
output.WriteByte(CMD_PICTURE_DRAW_PATH)
for command in command_list:
output.WriteByte(command[0])
for coord in command[1:]:
output.WriteFloat(coord)
def _ConvertPathElement(
self, node, style_category, has_shadow, shader_map, output):
style_list = self._ParseStyle(node, has_shadow, shader_map)
self._ConvertPath(node, output)
if style_category is not None:
self._ConvertStyleCategory(style_category, output)
else:
self._ConvertStyleList(style_list, output)
def _ConvertPolylineElement(
self, node, style_category, has_shadow, shader_map, output):
point_list = self._ParseFloatList(node.get('points'))
if len(point_list) < 2:
logging.critical('Invalid point number.')
sys.exit(1)
style_list = self._ParseStyle(node, has_shadow, shader_map)
output.WriteByte(CMD_PICTURE_DRAW_POLYLINE)
output.WriteByte(len(point_list))
for coord in point_list:
output.WriteFloat(coord)
if style_category is not None:
self._ConvertStyleCategory(style_category, output)
else:
self._ConvertStyleList(style_list, output)
def _ConvertPolygonElement(
self, node, style_category, has_shadow, shader_map, output):
style_list = self._ParseStyle(node, has_shadow, shader_map)
point_list = self._ParseFloatList(node.get('points'))
output.WriteByte(CMD_PICTURE_DRAW_POLYGON)
output.WriteByte(len(point_list))
for coord in point_list:
output.WriteFloat(coord)
if style_category is not None:
self._ConvertStyleCategory(style_category, output)
else:
self._ConvertStyleList(style_list, output)
def _ConvertLineElement(
self, node, style_category, has_shadow, shader_map, output):
style_list = self._ParseStyle(node, has_shadow, shader_map)
x1 = float(node.get('x1'))
y1 = float(node.get('y1'))
x2 = float(node.get('x2'))
y2 = float(node.get('y2'))
output.WriteByte(CMD_PICTURE_DRAW_LINE)
output.WriteFloat(x1)
output.WriteFloat(y1)
output.WriteFloat(x2)
output.WriteFloat(y2)
if style_category is not None:
self._ConvertStyleCategory(style_category, output)
else:
self._ConvertStyleList(style_list, output)
def _ConvertCircleElement(
self, node, style_category, has_shadow, shader_map, output):
style_list = self._ParseStyle(node, has_shadow, shader_map)
cx = float(node.get('cx'))
cy = float(node.get('cy'))
r = float(node.get('r'))
output.WriteByte(CMD_PICTURE_DRAW_CIRCLE)
output.WriteFloat(cx)
output.WriteFloat(cy)
output.WriteFloat(r)
if style_category is not None:
self._ConvertStyleCategory(style_category, output)
else:
self._ConvertStyleList(style_list, output)
def _ConvertEllipseElement(
self, node, style_category, has_shadow, shader_map, output):
style_list = self._ParseStyle(node, has_shadow, shader_map)
cx = float(node.get('cx'))
cy = float(node.get('cy'))
rx = float(node.get('rx'))
ry = float(node.get('ry'))
output.WriteByte(CMD_PICTURE_DRAW_ELLIPSE)
output.WriteFloat(cx)
output.WriteFloat(cy)
output.WriteFloat(rx)
output.WriteFloat(ry)
if style_category is not None:
self._ConvertStyleCategory(style_category, output)
else:
self._ConvertStyleList(style_list, output)
def _ConvertRectElement(
self, node, style_category, has_shadow, shader_map, output):
style_list = self._ParseStyle(node, has_shadow, shader_map)
x = float(node.get('x', 0))
y = float(node.get('y', 0))
w = float(node.get('width'))
h = float(node.get('height'))
output.WriteByte(CMD_PICTURE_DRAW_RECT)
output.WriteFloat(x)
output.WriteFloat(y)
output.WriteFloat(w)
output.WriteFloat(h)
if style_category is not None:
self._ConvertStyleCategory(style_category, output)
else:
self._ConvertStyleList(style_list, output)
def _ConvertGroupElement(
self, node, style_category, has_shadow, shader_map, output):
transform = node.get('transform')
if transform:
# Output order of 3x3 matrix;
# [ 0 3 6
# 1 4 7
# 2 5 8 ]
# - Combined transformation is not supported yet.
# - Only 'matrix' and 'translate' transformation is supported.
transform_match = TRANSFORM_PATTERN.match(transform)
if transform_match is None:
logging.critical('Unsupported transform: %s', transform)
sys.exit(1)
transformation = transform_match.group(1)
coords = transform_match.group(2)
output.WriteByte(CMD_PICTURE_DRAW_GROUP_START)
if transformation == 'matrix':
(m11, m21, m12, m22, m13, m23) = tuple(self._ParseFloatList(coords))
output.WriteFloat(m11)
output.WriteFloat(m21)
output.WriteFloat(0)
output.WriteFloat(m12)
output.WriteFloat(m22)
output.WriteFloat(0)
output.WriteFloat(m13)
output.WriteFloat(m23)
output.WriteFloat(1)
elif transformation == 'translate':
(tx, ty) = tuple(self._ParseFloatList(coords))
output.WriteFloat(1)
output.WriteFloat(0)
output.WriteFloat(0)
output.WriteFloat(0)
output.WriteFloat(1)
output.WriteFloat(0)
output.WriteFloat(tx)
output.WriteFloat(ty)
output.WriteFloat(1)
else:
# Never reach here. Just in case.
logging.critical('Unsupported transform: %s', transform)
sys.exit(1)
for child in node:
self._ConvertPictureSequence(
child, style_category, has_shadow, shader_map, output)
if transform:
output.WriteByte(CMD_PICTURE_DRAW_GROUP_END)
def _ConvertPictureSequence(
self, node, style_category, has_shadow, shader_map, output):
# Hack. To support shadow, we use 'id' attribute.
# If the 'id' starts with 'shadow', it means the element and its children
# has shadow.
nodeid = node.get('id', '')
if nodeid.startswith('shadow'):
has_shadow = HAS_SHADOW
elif nodeid.startswith('below_x5F_shadow'):
has_shadow = HAS_BELOW_SHADOW
if nodeid.startswith('style-'):
style_category = nodeid
if node.tag == '{http://www.w3.org/2000/svg}path':
self._ConvertPathElement(
node, style_category, has_shadow, shader_map, output)
return
if node.tag == '{http://www.w3.org/2000/svg}polyline':
self._ConvertPolylineElement(
node, style_category, has_shadow, shader_map, output)
return
if node.tag == '{http://www.w3.org/2000/svg}polygon':
self._ConvertPolygonElement(
node, style_category, has_shadow, shader_map, output)
return
if node.tag == '{http://www.w3.org/2000/svg}line':
self._ConvertLineElement(
node, style_category, has_shadow, shader_map, output)
return
if node.tag == '{http://www.w3.org/2000/svg}circle':
self._ConvertCircleElement(
node, style_category, has_shadow, shader_map, output)
return
if node.tag == '{http://www.w3.org/2000/svg}ellipse':
self._ConvertEllipseElement(
node, style_category, has_shadow, shader_map, output)
return
if node.tag == '{http://www.w3.org/2000/svg}rect':
self._ConvertRectElement(
node, style_category, has_shadow, shader_map, output)
return
if node.tag in ['{http://www.w3.org/2000/svg}g',
'{http://www.w3.org/2000/svg}svg']:
self._ConvertGroupElement(
node, style_category, has_shadow, shader_map, output)
return
if node.tag in ['{http://www.w3.org/2000/svg}linearGradient',
'{http://www.w3.org/2000/svg}radialGradient']:
return
logging.warning('Unknown element: %s', node.tag)
def _OutputEOP(self, output):
output.WriteByte(CMD_PICTURE_EOP)
def _ConvertPictureDrawableInternal(self, tree, output):
output.WriteByte(DRAWABLE_PICTURE)
shader_map = self._ParseShaderMap(tree.getroot())
self._ConvertSize(tree, output)
self._ConvertPictureSequence(
tree.getroot(), None, NO_SHADOW, shader_map, output)
self._OutputEOP(output)
# Interface for drawable conversion.
def ConvertPictureDrawable(self, path):
output = _OutputStream(StringIO.StringIO())
self._ConvertPictureDrawableInternal(ElementTree.parse(path), output)
return output.output.getvalue()
def ConvertStateListDrawable(self, drawable_source_list):
output = _OutputStream(StringIO.StringIO())
output.WriteByte(DRAWABLE_STATE_LIST)
output.WriteByte(len(drawable_source_list))
for (state_list, path) in drawable_source_list:
# Output state.
output.WriteByte(len(state_list))
for state in state_list:
output.WriteInt32(state)
# Output drawable.
self._ConvertPictureDrawableInternal(ElementTree.parse(path), output)
return output.output.getvalue()
def ConvertFiles(svg_dir, output_dir):
"""Converts SVG files into MechaMozc specific *pic* files.
Args:
svg_dir: Path to a directory which has svg files (recursively).
output_dir: Path of the destination directory.
"""
logging.debug('Start SVG conversion. From:%s, To:%s', svg_dir, output_dir)
# Ensure that the output directory exists.
if not os.path.exists(output_dir):
os.makedirs(output_dir)
converter = MozcDrawableConverter()
number_of_conversion = 0
for dirpath, dirnames, filenames in os.walk(svg_dir):
for filename in filenames:
basename, ext = os.path.splitext(filename)
if ext != '.svg':
# Do nothing for files other than svg.
continue
# Filename hack to generate stateful .pic files.
if basename.endswith('_release') or basename.endswith('_selected'):
# 'XXX_release.svg' files will be processed with corresponding
# '_center.svg' files. So just skip them.
# As similar to it, '_selected.svg' files will be processed with
# corresponding non-selected .svg files.
continue
if basename == 'keyboard_fold_tab_up':
# 'keyboard_fold_tab_up.svg' file will be processed with
# 'keyboard_fold_tab_down.svg.' Just skip it, too.
continue
logging.debug('Converting %s...', filename)
if basename.endswith('_center'):
# Process '_center.svg' file with '_release.svg' file to make
# stateful drawable.
center_svg_file = os.path.join(dirpath, filename)
release_svg_file = os.path.join(
dirpath, basename[:-7] + '_release.svg')
pic_file = os.path.join(output_dir, basename + '.pic')
pic_data = converter.ConvertStateListDrawable(
[([STATE_PRESSED], center_svg_file), ([], release_svg_file)])
elif os.path.exists(os.path.join(dirpath, basename + '_selected.svg')):
# Process '_selected.svg' file at the same time if necessary.
unselected_svg_file = os.path.join(dirpath, filename)
selected_svg_file = os.path.join(dirpath, basename + '_selected.svg')
pic_file = os.path.join(output_dir, basename + '.pic')
pic_data = converter.ConvertStateListDrawable(
[([STATE_SELECTED], selected_svg_file), ([], unselected_svg_file)])
elif basename == 'keyboard_fold_tab_down':
# Special hack for keyboard__fold__tab.pic.
down_svg_file = os.path.join(dirpath, filename)
up_svg_file = os.path.join(dirpath, 'keyboard_fold_tab_up.svg')
pic_file = os.path.join(output_dir, 'keyboard__fold__tab.pic')
pic_data = converter.ConvertStateListDrawable(
[([STATE_CHECKED], up_svg_file), ([], down_svg_file)])
else:
# Normal .svg file.
svg_file = os.path.join(dirpath, filename)
pic_file = os.path.join(output_dir, basename + '.pic')
pic_data = converter.ConvertPictureDrawable(svg_file)
with open(pic_file, 'wb') as stream:
stream.write(pic_data)
number_of_conversion += 1
logging.debug('%d files are converted.', number_of_conversion)
def ParseOptions():
parser = optparse.OptionParser()
parser.add_option('--svg_dir', dest='svg_dir',
help='Path to a directory containing .svg files.')
parser.add_option('--output_dir', dest='output_dir',
help='Path to the output directory,')
parser.add_option('--verbose', '-v', dest='verbose',
action='store_true', default=False,
help='Shows verbose message.')
return parser.parse_args()[0]
def main():
logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')
logging.getLogger().addFilter(util.ColoredLoggingFilter())
options = ParseOptions()
if options.verbose:
logging.getLogger().setLevel(logging.DEBUG)
ConvertFiles(options.svg_dir, options.output_dir)
if __name__ == '__main__':
main()