| /* |
| * Copyright (C) 2016 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.tools.pixelprobe.decoder.psd; |
| |
| import java.io.UnsupportedEncodingException; |
| import java.util.ArrayList; |
| import java.util.Deque; |
| import java.util.HashMap; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| /** |
| * The PSD format encodes all the text styling information in a structured |
| * text format. This format can describe: |
| * <ul> |
| * <li>Arrays</li> |
| * <li>Maps</li> |
| * <li>Named properties</li> |
| * <li>Integers</li> |
| * <li>Floats</li> |
| * <li>Booleans</li> |
| * <li>Strings</li> |
| * </ul> |
| * |
| * Here is an example: |
| * <pre> |
| * << |
| * /ParagraphSheet |
| * << |
| * /DefaultStyleSheet 0 |
| * /Properties |
| * << |
| * /WordSpacing [ .8 1.0 1.33 ] |
| * /EveryLineComposer false |
| * /PostHyphen 2 |
| * >> |
| * >> |
| * </pre> |
| * |
| * The << and >> markers define the beginning and the end of a map. |
| * A line starting with a / describes a named property (which can be of |
| * any type). Arrays are marked with [ and ] and can be multi-lines. |
| * |
| * To make things a bit tricky, strings are stored in UTF-16BE (big endian) |
| * while the rest of the structured document is in simple ASCII. To avoid |
| * encoding issues with the standard APIs, the structured text is parsed |
| * by hand. |
| * |
| * This format is both fairly easy to read and quirky. Please refer to the |
| * comments in the implementation below for more information. |
| */ |
| final class TextEngine { |
| /** |
| * A property has a type and a value. |
| */ |
| public interface Property<T> { |
| enum Type { |
| LIST, |
| MAP, |
| STRING, |
| INTEGER, |
| FLOAT, |
| BOOLEAN |
| } |
| |
| Property.Type getType(); |
| |
| T getValue(); |
| } |
| |
| /** |
| * A list of properties. |
| */ |
| public static final class ListProperty implements Property<List<Property<?>>> { |
| final List<Property<?>> list = new ArrayList<>(); |
| |
| @Override |
| public Type getType() { |
| return Type.LIST; |
| } |
| |
| @Override |
| public List<Property<?>> getValue() { |
| return list; |
| } |
| |
| public float[] toFloatArray() { |
| float[] a = new float[list.size()]; |
| for (int i = 0; i < a.length; i++) { |
| a[i] = ((FloatProperty) list.get(i)).getValue(); |
| } |
| return a; |
| } |
| |
| public int[] toIntArray() { |
| int[] a = new int[list.size()]; |
| for (int i = 0; i < a.length; i++) { |
| a[i] = ((IntProperty) list.get(i)).getValue(); |
| } |
| return a; |
| } |
| } |
| |
| /** |
| * A properties map. Each property stored in the map is named by the |
| * associated key. |
| */ |
| public static final class MapProperty implements Property<Map<String, Property<?>>> { |
| private static final Pattern PATH_PATTERN = Pattern.compile("([a-zA-Z0-9]+)\\[([0-9]+)\\]"); |
| |
| final Map<String, Property<?>> map = new HashMap<>(); |
| |
| @Override |
| public Type getType() { |
| return Type.MAP; |
| } |
| |
| @Override |
| public Map<String, Property<?>> getValue() { |
| return map; |
| } |
| |
| /** |
| * Finds a property in this map using a simple path expression language. |
| * Paths have the following form: |
| * |
| * PropertyName1.ArrayName[2].PropertyName2 |
| * |
| * This path will find the value of the property called PropertyName2 stored |
| * in the map as the third element of the array named ArrayName in the map |
| * called PropertyName1. |
| * |
| * @param path A path expression |
| * |
| * @return A property or null if no match is found |
| */ |
| public Property<?> get(String path) { |
| Property<?> property = null; |
| Map<String, Property<?>> currentMap = map; |
| // Simple regex to match indexing within arrays: name[INDEX] |
| Pattern pattern = PATH_PATTERN; |
| |
| String[] elements = path.split("\\."); |
| out: |
| for (String element : elements) { |
| int index = -1; |
| Matcher matcher = pattern.matcher(element); |
| if (matcher.matches()) { |
| element = matcher.group(1); |
| index = Integer.parseInt(matcher.group(2)); |
| } |
| |
| property = currentMap.get(element); |
| if (property == null) break; |
| |
| switch (property.getType()) { |
| case LIST: |
| if (index >= 0) { |
| Property<?> item = ((ListProperty) property).list.get(index); |
| if (item instanceof MapProperty) { |
| currentMap = ((MapProperty) item).map; |
| } else { |
| property = item; |
| break out; |
| } |
| } else { |
| break out; |
| } |
| break; |
| case MAP: |
| currentMap = ((MapProperty) property).map; |
| break; |
| case STRING: |
| case INTEGER: |
| case FLOAT: |
| case BOOLEAN: |
| break out; |
| } |
| } |
| |
| return property; |
| } |
| } |
| |
| public static final class StringProperty implements Property<String> { |
| final String text; |
| |
| StringProperty(String text) { |
| this.text = text; |
| } |
| |
| @Override |
| public Type getType() { |
| return Type.STRING; |
| } |
| |
| @Override |
| public String getValue() { |
| return text; |
| } |
| |
| @Override |
| public String toString() { |
| return text; |
| } |
| } |
| |
| public static final class BooleanProperty implements Property<Boolean> { |
| final Boolean value; |
| |
| BooleanProperty(Boolean value) { |
| this.value = value; |
| } |
| |
| @Override |
| public Type getType() { |
| return Type.BOOLEAN; |
| } |
| |
| @Override |
| public Boolean getValue() { |
| return value; |
| } |
| |
| @Override |
| public String toString() { |
| return String.valueOf(value); |
| } |
| } |
| |
| public static final class IntProperty implements Property<Integer> { |
| final Integer value; |
| |
| IntProperty(Integer value) { |
| this.value = value; |
| } |
| |
| @Override |
| public Type getType() { |
| return Type.INTEGER; |
| } |
| |
| @Override |
| public Integer getValue() { |
| return value; |
| } |
| |
| @Override |
| public String toString() { |
| return String.valueOf(value); |
| } |
| } |
| |
| public static final class FloatProperty implements Property<Float> { |
| final Float value; |
| |
| FloatProperty(Float value) { |
| this.value = value; |
| } |
| |
| @Override |
| public Type getType() { |
| return Type.FLOAT; |
| } |
| |
| @Override |
| public Float getValue() { |
| return value; |
| } |
| |
| @Override |
| public String toString() { |
| return String.valueOf(value); |
| } |
| } |
| |
| // Various tokens used to parse the structured data |
| private static final byte TOKEN_STRING_START = 0x28; // '(' |
| private static final byte TOKEN_STRING_END = 0x29; // ')' |
| private static final byte TOKEN_PROPERTY_START = 0x2F; // '/' |
| private static final byte TOKEN_ARRAY_START = 0x5B; // '[' |
| private static final byte TOKEN_ARRAY_END = 0x5D; // ']' |
| private static final byte TOKEN_SPACE = 0x20; // ' ' |
| private static final byte TOKEN_TAB = 0x09; // '\t' |
| private static final byte TOKEN_LINE_END = 0x0A; // '\n' |
| private static final byte TOKEN_FLOAT = 0x2E; // '.' |
| private static final byte TOKEN_BOOLEAN_TRUE = 0x74; // 't' |
| private static final byte TOKEN_BOOLEAN_FALSE = 0x66; // 'f' |
| private static final byte[] TOKEN_MAP_START = new byte[] { 0x3C, 0x3C }; // "<<" |
| private static final byte[] TOKEN_MAP_END = new byte[] { 0x3E, 0x3E }; // ">>" |
| private static final byte[] TOKEN_BIG_ENDIAN = new byte[] { (byte) 0xFE, (byte) 0xFF }; |
| |
| // Stores property names |
| private Deque<String> nameStack = new LinkedList<>(); |
| // Stores maps and arrays |
| private Deque<Property> stack = new LinkedList<>(); |
| |
| /** |
| * Checks whether all the bytes in the second array are present in the first |
| * array, starting at the "start" offset. |
| */ |
| private static boolean matches(byte[] l, byte[] r, int start) { |
| for (int i = 0; i < r.length; i++) { |
| if (l[start + i] != r[i]) return false; |
| } |
| return true; |
| } |
| |
| /** |
| * Decodes the specified structured text data as a MapProperty. |
| * Photoshop always uses a map as the root element of the structured text. |
| * |
| * @param data The structured text as raw bytes |
| * |
| * @return A MapProperty instance, never null |
| */ |
| public MapProperty parse(byte[] data) { |
| MapProperty root = null; |
| |
| nameStack.clear(); |
| stack.clear(); |
| |
| try { |
| int pos = 0; |
| while (pos < data.length) { |
| // The data is stored indented in the PSD file |
| // Photoshop always uses tabs as indents |
| while (pos < data.length && data[pos] == TOKEN_TAB) pos++; |
| int start = pos; |
| // An '\n' marks the end of a line in the structured text |
| // except when inside a String property |
| while (pos < data.length && data[pos] != TOKEN_LINE_END) pos++; |
| |
| // Skip empty lines |
| int length = pos - start; |
| if (length == 0) { |
| pos++; // don't forget to advance over the line end marker |
| continue; |
| } |
| |
| // Either << or >>, always marks a map |
| if (length == 2) { |
| // If we start a map, push a new MapProperty on the stack |
| if (matches(data, TOKEN_MAP_START, start)) { |
| stack.offerFirst(new MapProperty()); |
| } else if (matches(data, TOKEN_MAP_END, start)) { |
| // Found the end of a map, pop it off the stack and |
| // add it to wherever it belongs |
| root = (MapProperty) stack.pollFirst(); |
| addProperty(root); |
| } |
| } else if (length == 1 && data[start] == TOKEN_ARRAY_END) { |
| // Found the end of an array, pop it off the stack |
| addProperty(stack.pollFirst()); |
| } else if (data[start] == TOKEN_PROPERTY_START) { |
| // We found a named property, first read its name |
| // then figure out its type. The name is always followed by |
| // a space |
| int nameStart = start + 1; |
| while (start < pos && data[start] != TOKEN_SPACE) start++; |
| int nameEnd = start++; |
| |
| // Push the name on the stack in case this property is |
| // a map or an array |
| String name = new String(data, nameStart, nameEnd - nameStart); |
| nameStack.offerFirst(name); |
| |
| // If the last byte on this line is \n, then we have a map |
| // otherwise the property is a float, integer, boolean, |
| // array or string |
| if (data[nameEnd] != TOKEN_LINE_END) { |
| String value; |
| int valueLength = pos - start; |
| |
| switch (data[start]) { |
| // The property is an array (single or multi line) |
| case TOKEN_ARRAY_START: |
| stack.offerFirst(new ListProperty()); |
| // Single-line array |
| if (data[pos - 1] == TOKEN_ARRAY_END) { |
| // All the elements have the same type and as far |
| // as I can tell they are always numbers |
| value = new String(data, start + 1, valueLength - 2); |
| // Elements are space delimited |
| for (String element : value.trim().split("\\s+")) { |
| if (!element.isEmpty()) parseNumber(element); |
| } |
| addProperty(stack.pollFirst()); |
| } |
| break; |
| // The property is a string |
| case TOKEN_STRING_START: |
| // A String starts with FEFF, which wrecks havoc |
| // with Java's encodings, let's be careful |
| if (matches(data, TOKEN_BIG_ENDIAN, start + 1)) { |
| // Some strings can be multiline... |
| while (data[pos - 1] != TOKEN_STRING_END) { |
| pos++; // Advance over the last line end token |
| while (pos < data.length && data[pos] != TOKEN_LINE_END) pos++; |
| } |
| |
| // Decode the String as a big endian UTF-16 string |
| value = new String(data, start + 3, pos - start - 4, "UTF-16BE"); |
| value = value.replace('\r', '\n'); |
| addProperty(new StringProperty(value)); |
| } |
| break; |
| // The property is a float (starts with a '.') |
| case TOKEN_FLOAT: |
| value = new String(data, start, valueLength); |
| addProperty(new FloatProperty(Float.parseFloat(value))); |
| break; |
| // The property is a boolean |
| case TOKEN_BOOLEAN_TRUE: |
| case TOKEN_BOOLEAN_FALSE: |
| value = new String(data, start, valueLength); |
| addProperty(new BooleanProperty(Boolean.parseBoolean(value))); |
| break; |
| // The property is a number (floats start with either 0. or .) |
| default: |
| value = new String(data, start, valueLength); |
| parseNumber(value); |
| break; |
| } |
| } |
| } |
| pos++; |
| } |
| } catch (UnsupportedEncodingException e) { |
| throw new RuntimeException("Could not decode text engine data", e); |
| } |
| |
| |
| return root != null ? root : new MapProperty(); |
| } |
| |
| /** |
| * Parse the specified String as a number and adds the resulting property |
| * to the parent map or array. The number is either a float or an integer. |
| */ |
| private void parseNumber(String value) { |
| int decimalSeparator = value.indexOf('.'); |
| if (decimalSeparator == -1) { |
| addProperty(new IntProperty(Integer.parseInt(value))); |
| } else { |
| addProperty(new FloatProperty(Float.parseFloat(value))); |
| } |
| } |
| |
| /** |
| * Adds the specified property to the current parent map or array. |
| */ |
| private void addProperty(Property property) { |
| Property previous = stack.peekFirst(); |
| if (previous == null) return; |
| |
| switch (previous.getType()) { |
| case LIST: |
| ((ListProperty) previous).list.add(property); |
| break; |
| case MAP: |
| String name = nameStack.pollFirst(); |
| ((MapProperty) previous).map.put(name, property); |
| break; |
| case STRING: |
| break; |
| case INTEGER: |
| break; |
| case FLOAT: |
| break; |
| case BOOLEAN: |
| break; |
| } |
| } |
| } |