blob: 4027a53a9bd45ee0321216cca61f1598a4e0e594 [file] [log] [blame]
# -*- coding: utf-8 -*-
# Copyright 2010-2015, 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_DRAW_TEXT = 10
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_PAINT_FONT_SIZE = 8
CMD_PICTURE_PAINT_TEXT_ANCHOR = 9
CMD_PICTURE_PAINT_DOMINANT_BASELINE = 10
CMD_PICTURE_PAINT_FONT_WEIGHT = 11
CMD_PICTURE_PAINT_TEXT_ANCHOR_START = 0
CMD_PICTURE_PAINT_TEXT_ANCHOR_MIDDLE = 1
CMD_PICTURE_PAINT_TEXT_ANCHOR_END = 2
CMD_PICTURE_PAINT_DOMINANTE_BASELINE_AUTO = 0
CMD_PICTURE_PAINT_DOMINANTE_BASELINE_CENTRAL = 1
CMD_PICTURE_PAINT_FONT_WEIGHT_NORMAL = 0
CMD_PICTURE_PAINT_FONT_WEIGHT_BOLD = 1
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_TWELVEKEYS_FUNCTION = 6
STYLE_CATEGORY_KEYICON_TWELVEKEYS_GLOBE = 7
STYLE_CATEGORY_KEYICON_QWERTY_FUNCTION = 8
STYLE_CATEGORY_KEYICON_FUNCTION_DARK = 9
STYLE_CATEGORY_KEYICON_ENTER = 10
STYLE_CATEGORY_KEYICON_ENTER_CIRCLE = 11
STYLE_CATEGORY_KEYICON_QWERTY_SHIFT_ON_ARROW = 12
STYLE_CATEGORY_KEYPOPUP_HIGHLIGHT = 13
# We may be able to reuse same resources for symbol major/minor icons.
STYLE_CATEGORY_SYMBOL_MAJOR = 14
STYLE_CATEGORY_SYMBOL_MAJOR_SELECTED = 15
STYLE_CATEGORY_SYMBOL_MAJOR_EMOJI_DISABLE_CIRCLE = 16
STYLE_CATEGORY_SYMBOL_MINOR = 17
STYLE_CATEGORY_SYMBOL_MINOR_SELECTED = 18
STYLE_CATEGORY_KEYBOARD_FOLDING_BUTTON_BACKGROUND_DEFAULT = 19
STYLE_CATEGORY_KEYBOARD_FOLDING_BUTTON_BACKGROUND_SCROLLED = 20
# 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-twelvekeys-function',
STYLE_CATEGORY_KEYICON_TWELVEKEYS_FUNCTION),
('style-keyicon-twelvekeys-globe',
STYLE_CATEGORY_KEYICON_TWELVEKEYS_GLOBE),
('style-keyicon-qwerty-function',
STYLE_CATEGORY_KEYICON_QWERTY_FUNCTION),
('style-keyicon-function-dark', STYLE_CATEGORY_KEYICON_FUNCTION_DARK),
('style-keyicon-enter', STYLE_CATEGORY_KEYICON_ENTER),
('style-keyicon-enter-circle', STYLE_CATEGORY_KEYICON_ENTER_CIRCLE),
('style-keyicon-qwerty-shift-on-arrow',
STYLE_CATEGORY_KEYICON_QWERTY_SHIFT_ON_ARROW),
('style-keypopup-highlight', STYLE_CATEGORY_KEYPOPUP_HIGHLIGHT),
('style-symbol-major', STYLE_CATEGORY_SYMBOL_MAJOR),
('style-symbol-major-selected', STYLE_CATEGORY_SYMBOL_MAJOR_SELECTED),
('style-symbol-major-emoji-disable-circle',
STYLE_CATEGORY_SYMBOL_MAJOR_EMOJI_DISABLE_CIRCLE),
('style-symbol-minor', STYLE_CATEGORY_SYMBOL_MINOR),
('style-symbol-minor-selected', STYLE_CATEGORY_SYMBOL_MINOR_SELECTED),
('style-keyboard-folding-button-background-default',
STYLE_CATEGORY_KEYBOARD_FOLDING_BUTTON_BACKGROUND_DEFAULT),
('style-keyboard-folding-button-background-scrolled',
STYLE_CATEGORY_KEYBOARD_FOLDING_BUTTON_BACKGROUND_SCROLLED),
],
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]{3,8}(?![0-9A-Fa-f]))')
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|scale)\((.*)\)')
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 byte')
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 WriteString(self, value):
utf8_string = value.encode('utf-8')
self.WriteInt16(len(utf8_string))
self.output.write(utf8_string)
def __enter__(self):
self.output.__enter__()
def __exit__(self, exec_type, exec_value, traceback):
self.output.__exit__(exec_type, exec_value, traceback)
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_str = m.group(1)
if 3 <= len(c_str) <= 4:
expanded_c_str = ''
for ch in c_str:
expanded_c_str = expanded_c_str + ch + ch
c_str = expanded_c_str
if len(c_str) == 6:
c_str = 'FF' + c_str
if len(c_str) != 8:
logging.critical('Invalid color format.')
sys.exit(1)
c = int(c_str, 16)
if c < 0 or 0x100000000 <= c:
logging.critical('Out of color range: %s', color)
sys.exit(1)
return c
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'))
gradient_transform = node.get('gradientTransform')
if gradient_transform:
m = MATRIX_PATTERN.match(gradient_transform)
(m11, m21, m12, m22, m13, m23) = tuple(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'))
gradient_transform = node.get('gradientTransform')
if gradient_transform:
m = MATRIX_PATTERN.match(gradient_transform)
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."""
common_map = {}
# Default fill color is black (SVG's spec)
fill_map = {'style': 'fill', 'color': 0xFF000000, 'shadow': has_shadow}
# Default stroke color is none (SVG's spec)
stroke_map = {'style': 'stroke', 'shadow': has_shadow}
# Special warning for font-size.
# Inkscape often unexpectedly converts from sytle to font-size attribute.
if node.get('font-size', ''):
logging.warning('font-size attribute is not supported.')
for attr in node.get('style', '').split(';'):
attr = attr.strip()
if not attr:
continue
command, arg = attr.split(':')
if command == 'fill' or command == 'stroke':
paint_map = fill_map if command == 'fill' else stroke_map
if arg == 'none':
paint_map.pop('color', None)
paint_map.pop('shader', None)
continue
shader = self._ParseShader(arg, shader_map)
color = self._ParseColor(arg)
if shader is None and color is None:
if arg != 'none':
logging.critical('Unknown pattern: %s', arg)
sys.exit(1)
continue
paint_map['style'] = command
if shader is not None:
paint_map['shader'] = shader
if color is not None:
paint_map['color'] = color
continue
if command == 'stroke-width':
stroke_map['stroke-width'] = float(arg)
continue
if command == 'stroke-linecap':
stroke_map['stroke-linecap'] = arg
continue
if command == 'stroke-linejoin':
stroke_map['stroke-linejoin'] = arg
continue
# font relating attributes are common to all commands.
if command == 'font-size':
common_map['font-size'] = self._ParsePixel(arg)
if command == 'text-anchor':
common_map['text-anchor'] = arg
if command == 'dominant-baseline':
common_map['dominant-baseline'] = arg
if command == 'font-weight':
common_map['font-weight'] = arg
# 'fill' comes first in order to draw 'fill' first (SVG specification).
result = []
if 'color' in fill_map or 'shader' in fill_map:
fill_map.update(common_map)
result.append(fill_map)
if 'color' in stroke_map or 'shader' in stroke_map:
stroke_map.update(common_map)
result.append(stroke_map)
return result
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 _ConvertStyleCommand(self, style, output):
"""Converts style attribute."""
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 _MaybeConvertFontSize(self, font_size, output):
if font_size is None:
return
output.WriteByte(CMD_PICTURE_PAINT_FONT_SIZE)
output.WriteFloat(font_size)
return
def _MaybeConvertTextAnchor(self, text_anchor, output):
if text_anchor is None:
return
output.WriteByte(CMD_PICTURE_PAINT_TEXT_ANCHOR)
if text_anchor == 'start':
output.WriteByte(CMD_PICTURE_PAINT_TEXT_ANCHOR_START)
elif text_anchor == 'middle':
output.WriteByte(CMD_PICTURE_PAINT_TEXT_ANCHOR_MIDDLE)
elif text_anchor == 'end':
output.WriteByte(CMD_PICTURE_PAINT_TEXT_ANCHOR_END)
else:
logging.critical('text-anchor is invalid (%s)', text_anchor)
sys.exit(1)
return
def _MaybeConvertDominantBaseline(self, baseline, output):
if baseline is None:
return
output.WriteByte(CMD_PICTURE_PAINT_DOMINANT_BASELINE)
if baseline == 'auto':
output.WriteByte(CMD_PICTURE_PAINT_DOMINANTE_BASELINE_AUTO)
elif baseline == 'central':
output.WriteByte(CMD_PICTURE_PAINT_DOMINANTE_BASELINE_CENTRAL)
else:
logging.critical('dominant-baseline is invalid (%s)', baseline)
sys.exit(1)
return
def _MaybeConvertFontWeight(self, weight, output):
if weight is None:
return
output.WriteByte(CMD_PICTURE_PAINT_FONT_WEIGHT)
if weight == 'normal':
output.WriteByte(CMD_PICTURE_PAINT_FONT_WEIGHT_NORMAL)
elif weight == 'bold':
output.WriteByte(CMD_PICTURE_PAINT_FONT_WEIGHT_BOLD)
else:
logging.critical('font-weight is invalid (%s)', weight)
sys.exit(1)
return
def _MaybeConvertStyleCategory(self, style_category, output):
if style_category is None:
return
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 _ConvertStyle(self, style_category, style_list, output):
style_len = len(style_list)
output.WriteByte(style_len)
for style_map in style_list:
self._ConvertStyleCommand(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)
self._MaybeConvertFontSize(style_map.get('font-size'), output)
self._MaybeConvertTextAnchor(style_map.get('text-anchor'), output)
self._MaybeConvertDominantBaseline(style_map.get('dominant-baseline'),
output)
self._MaybeConvertFontWeight(style_map.get('font-weight'), output)
self._MaybeConvertStyleCategory(style_category, output)
output.WriteByte(CMD_PICTURE_PAINT_EOP)
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)
self._ConvertStyle(style_category, 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)
self._ConvertStyle(style_category, 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)
self._ConvertStyle(style_category, 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)
self._ConvertStyle(style_category, 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)
self._ConvertStyle(style_category, 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)
self._ConvertStyle(style_category, 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)
self._ConvertStyle(style_category, style_list, output)
def _ConvertTextElement(
self, node, style_category, has_shadow, shader_map, output):
"""Converts text element.
The text element must not have any children.
Args:
node: node
style_category: style_category(optional)
has_shadow: shadow value
shader_map: shader map
output: output stream
"""
if not node.text:
# Ignore empty text node
return
style_list = self._ParseStyle(node, has_shadow, shader_map)
x = float(node.get('x', 0))
y = float(node.get('y', 0))
output.WriteByte(CMD_PICTURE_DRAW_TEXT)
output.WriteFloat(x)
output.WriteFloat(y)
if node:
logging.critical('<text> with children is not supported.')
sys.exit(1)
output.WriteString(node.text)
self._ConvertStyle(style_category, 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)
elif transformation == 'scale':
parsed_coords = tuple(self._ParseFloatList(coords))
if len(parsed_coords) != 1 and len(parsed_coords) != 2:
logging.critical('Invalid argument for scale: %s', coords)
sys.exit(1)
sx = parsed_coords[0]
if len(parsed_coords) == 1:
sy = sx
else:
sy = parsed_coords[1]
output.WriteFloat(sx)
output.WriteFloat(0)
output.WriteFloat(0)
output.WriteFloat(0)
output.WriteFloat(sy)
output.WriteFloat(0)
output.WriteFloat(0)
output.WriteFloat(0)
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 == '{http://www.w3.org/2000/svg}text':
self._ConvertTextElement(
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
# Ignore following tags.
if node.tag in ['{http://www.w3.org/2000/svg}linearGradient',
'{http://www.w3.org/2000/svg}radialGradient',
'{http://www.w3.org/2000/svg}metadata',
'{http://www.w3.org/2000/svg}defs',]:
return
logging.critical('Unknown element: %s', node.tag)
sys.exit(1)
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_paths, output_dir):
"""Converts SVG files into MechaMozc specific *pic* files.
Args:
svg_paths: Comma separated paths to a directory/zip which has svg files
(recursively).
output_dir: Path of the destination directory.
"""
logging.debug('Start SVG conversion. From:%s, To:%s', svg_paths, 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, _, filenames in util.WalkFileContainers(svg_paths):
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():
"""Parses options."""
parser = optparse.OptionParser()
parser.add_option('--svg_paths', dest='svg_paths',
help='Comma separated paths to a directory or'
' a .zip file containing .svg files.')
parser.add_option('--output_dir', dest='output_dir',
help='Path to the output directory,')
parser.add_option('--build_log', dest='build_log',
help='(Optional) Path to build log to generate.'
' If set, nothing will be sent to stderr.')
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.build_log:
logging.getLogger().handlers = []
logging.getLogger().addHandler(logging.FileHandler(options.build_log, 'w'))
if options.verbose or options.build_log:
logging.getLogger().setLevel(logging.DEBUG)
ConvertFiles(options.svg_paths, options.output_dir)
if __name__ == '__main__':
main()