blob: 32b78be6bfe3e58b782ef06236a36e3709f3a4f6 [file] [log] [blame]
/*
* 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 com.android.tools.pixelprobe.BlendMode;
import com.android.tools.pixelprobe.ShapeInfo;
import com.android.tools.pixelprobe.ShapeInfo.Path;
import com.android.tools.pixelprobe.ShapeInfo.PathType;
import com.android.tools.pixelprobe.color.Colors;
import com.android.tools.pixelprobe.decoder.psd.PsdFile.ColorProfileBlock;
import com.android.tools.pixelprobe.decoder.psd.PsdFile.Descriptor;
import com.android.tools.pixelprobe.decoder.psd.PsdFile.DescriptorItem.UnitDouble;
import com.android.tools.pixelprobe.decoder.psd.PsdFile.PathRecord;
import com.android.tools.pixelprobe.decoder.psd.PsdFile.ShapeMask;
import com.android.tools.pixelprobe.util.Bytes;
import java.awt.*;
import java.awt.color.ColorSpace;
import java.awt.color.ICC_ColorSpace;
import java.awt.color.ICC_Profile;
import java.awt.geom.AffineTransform;
import java.awt.geom.Path2D;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static com.android.tools.pixelprobe.decoder.psd.PsdFile.PathRecord.*;
/**
* Various utilities to decode PSD file data chunks.
*/
@SuppressWarnings("UseJBColor")
final class PsdUtils {
/**
* Constant to convert centimeters to inches.
*/
static final float CENTIMETER_TO_INCH = 1.0f / 2.54f;
/**
* Constant to convert millimeters to inches.
*/
static final float MILLIMETER_TO_INCH = 1.0f / 25.4f;
// Used to parse descriptor paths
private static final Pattern PATH_PATTERN = Pattern.compile("([a-zA-Z0-9]+)\\[([0-9]+)\\]");
// List of all the blending modes supported by Photoshop. They are all identified
// by a 4 character key. The BlendMode enum uses Photoshop's naming conventions
// (linear dodge is for instance commonly known as "add").
private static final Map<String, BlendMode> blendModes = new HashMap<>();
static {
blendModes.put("pass", BlendMode.PASS_THROUGH);
blendModes.put("norm", BlendMode.NORMAL);
blendModes.put("diss", BlendMode.DISSOLVE);
blendModes.put("dark", BlendMode.DARKEN);
blendModes.put("mul ", BlendMode.MULTIPLY);
blendModes.put("idiv", BlendMode.COLOR_BURN);
blendModes.put("lbrn", BlendMode.LINEAR_BURN);
blendModes.put("dkCl", BlendMode.DARKER_COLOR);
blendModes.put("lite", BlendMode.LIGHTEN);
blendModes.put("scrn", BlendMode.SCREEN);
blendModes.put("div ", BlendMode.COLOR_DODGE);
blendModes.put("lddg", BlendMode.LINEAR_DODGE);
blendModes.put("lgCl", BlendMode.LIGHTER_COLOR);
blendModes.put("over", BlendMode.OVERLAY);
blendModes.put("sLit", BlendMode.SOFT_LIGHT);
blendModes.put("hLit", BlendMode.HARD_LIGHT);
blendModes.put("vLit", BlendMode.VIVID_LIGHT);
blendModes.put("lLit", BlendMode.LINEAR_LIGHT);
blendModes.put("pLit", BlendMode.PIN_LIGHT);
blendModes.put("hMix", BlendMode.HARD_MIX);
blendModes.put("diff", BlendMode.DIFFERENCE);
blendModes.put("smud", BlendMode.EXCLUSION);
blendModes.put("fsub", BlendMode.SUBTRACT);
blendModes.put("fdiv", BlendMode.DIVIDE);
blendModes.put("hue ", BlendMode.HUE);
blendModes.put("sat ", BlendMode.SATURATION);
blendModes.put("colr", BlendMode.COLOR);
blendModes.put("lum ", BlendMode.LUMINOSITY);
// blend modes stored in descriptors have different keys
blendModes.put("Nrml", BlendMode.NORMAL);
blendModes.put("Dslv", BlendMode.DISSOLVE);
blendModes.put("Drkn", BlendMode.DARKEN);
blendModes.put("Mltp", BlendMode.MULTIPLY);
blendModes.put("CBrn", BlendMode.COLOR_BURN);
blendModes.put("linearBurn", BlendMode.LINEAR_BURN);
blendModes.put("darkerColor", BlendMode.DARKER_COLOR);
blendModes.put("Lghn", BlendMode.LIGHTEN);
blendModes.put("Scrn", BlendMode.SCREEN);
blendModes.put("CDdg", BlendMode.COLOR_DODGE);
blendModes.put("linearDodge", BlendMode.LINEAR_DODGE);
blendModes.put("lighterColor", BlendMode.LIGHTER_COLOR);
blendModes.put("Ovrl", BlendMode.OVERLAY);
blendModes.put("SftL", BlendMode.SOFT_LIGHT);
blendModes.put("HrdL", BlendMode.HARD_LIGHT);
blendModes.put("vividLight", BlendMode.VIVID_LIGHT);
blendModes.put("linearLight", BlendMode.LINEAR_LIGHT);
blendModes.put("pinLight", BlendMode.PIN_LIGHT);
blendModes.put("hardMix", BlendMode.HARD_MIX);
blendModes.put("Dfrn", BlendMode.DIFFERENCE);
blendModes.put("Xclu", BlendMode.EXCLUSION);
blendModes.put("blendSubtraction", BlendMode.SUBTRACT);
blendModes.put("blendDivide", BlendMode.DIVIDE);
blendModes.put("H ", BlendMode.HUE);
blendModes.put("Strt", BlendMode.SATURATION);
blendModes.put("Clr ", BlendMode.COLOR);
blendModes.put("Lmns", BlendMode.LUMINOSITY);
}
private static final Set<String> adjustmentLayers = new HashSet<>(20);
static {
adjustmentLayers.add("SoCo");
adjustmentLayers.add("GdFl");
adjustmentLayers.add("PtFl");
adjustmentLayers.add("brit");
adjustmentLayers.add("levl");
adjustmentLayers.add("curv");
adjustmentLayers.add("expA");
adjustmentLayers.add("vibA");
adjustmentLayers.add("hue ");
adjustmentLayers.add("hue2");
adjustmentLayers.add("blnc");
adjustmentLayers.add("blwh");
adjustmentLayers.add("phfl");
adjustmentLayers.add("mixr");
adjustmentLayers.add("clrL");
adjustmentLayers.add("nvrt");
adjustmentLayers.add("post");
adjustmentLayers.add("thrs");
adjustmentLayers.add("grdm");
adjustmentLayers.add("selc");
}
private PsdUtils() {
}
/**
* Finds a descriptor value in the specified descriptor.
* The path must be of the form <pre>child.value</pre>, with an
* optional index for descriptor arrays. For instance:
*
* <pre>
* children.properties[2].key
* </pre>
*
* @param descriptor The descriptor on which to apply the path
* @param path The path of the descriptor value
*
* @return The value pointed to by the specified path or null
* if the path is invalid
*/
@SuppressWarnings("unchecked")
static <T> T get(Descriptor descriptor, String path) {
Object result = null;
Descriptor currentDescriptor = descriptor;
String[] elements = path.split("\\.");
for (String element : elements) {
int index = -1;
Matcher matcher = PATH_PATTERN.matcher(element);
if (matcher.matches()) {
element = matcher.group(1);
index = Integer.parseInt(matcher.group(2));
}
PsdFile.DescriptorItem item = currentDescriptor.items.get(element);
if (item == null) break;
Object data = item.value.data;
if (data == null) break;
if (data instanceof PsdFile.DescriptorItem.ValueList) {
if (index >= 0) {
data = ((PsdFile.DescriptorItem.ValueList) data).items.get(index).data;
}
} else if (data instanceof PsdFile.DescriptorItem.Reference) {
if (index >= 0) {
data = ((PsdFile.DescriptorItem.Reference) data).items.get(index).data;
}
}
if (data instanceof Descriptor) {
result = currentDescriptor = (Descriptor) data;
} else if (data instanceof PsdFile.FixedString ||
data instanceof PsdFile.UnicodeString ||
data instanceof PsdFile.DescriptorItem.Enumerated) {
result = data.toString();
} else if (data instanceof PsdFile.FixedByteArray) {
result = ((PsdFile.FixedByteArray) data).value;
} else {
result = data;
}
}
return (T) result;
}
static float getFloat(Descriptor descriptor, String path) {
return ((Double) get(descriptor, path)).floatValue();
}
static float getFloat(Descriptor descriptor, String path, float defaultValue) {
Double v = get(descriptor, path);
return v == null ? defaultValue : v.floatValue();
}
static float getUnitFloat(Descriptor descriptor, String path, float resolution) {
return (float) resolveUnit(get(descriptor, path), resolution);
}
static double resolveUnit(UnitDouble unitDouble, float resolution) {
if (UnitDouble.PIXELS.equals(unitDouble.unit)) {
return unitDouble.value;
} else if (UnitDouble.POINTS.equals(unitDouble.unit)) {
return unitDouble.value * resolution / 72.0;
} else if (UnitDouble.INCHES.equals(unitDouble.unit)) {
return unitDouble.value * resolution;
} else if (UnitDouble.MILLIMETERS.equals(unitDouble.unit)) {
return unitDouble.value * MILLIMETER_TO_INCH * resolution;
} else if (UnitDouble.CENTIMETERS.equals(unitDouble.unit)) {
return unitDouble.value * CENTIMETER_TO_INCH * resolution;
} else if (UnitDouble.PERCENT.equals(unitDouble.unit)) {
return unitDouble.value / 100.0;
} else if (UnitDouble.ANGLE_DEGREES.equals(unitDouble.unit)) {
return unitDouble.value / 360.0;
}
return unitDouble.value;
}
static boolean getBoolean(Descriptor descriptor, String path) {
Object value = get(descriptor, path);
if (value instanceof Boolean) {
return (Boolean) value;
} else if (value == null) {
return true;
}
return "true".equalsIgnoreCase(String.valueOf(value));
}
/**
* Returns the blend mode associated with the key found
* in a PSD file.
*
* @param mode Blending mode key as found in a PSD file
*
* @return The blend mode matching the specified key, or
* {@link BlendMode#NORMAL} if the key is unknown
*/
static BlendMode getBlendMode(String mode) {
BlendMode blendMode = blendModes.get(mode);
return blendMode != null ? blendMode : BlendMode.NORMAL;
}
/**
* Returns the known list of adjustment layer keys.
*/
static Set<String> getAdjustmentLayerKeys() {
return adjustmentLayers;
}
/**
* Creates a color space from a color profile block. Can return null
* if the specified block is null.
*/
static ColorSpace createColorSpace(ColorProfileBlock block) {
if (block == null) return null;
ICC_Profile iccProfile = ICC_Profile.getInstance(block.icc);
return new ICC_ColorSpace(iccProfile);
}
/**
* Returns the image resource block that matches the specified id.
* Can return null if the block cannot be found.
*/
@SuppressWarnings("unchecked")
static <T> T get(PsdFile.ImageResources resources, int id) {
PsdFile.ImageResourceBlock block = resources.blocks.get(id);
return block == null ? null : (T) block.data;
}
/**
* Returns the color present in the specified descriptor.
* The returned color always has an alpha of 1.0f.
* If no color can be found, this method returns black.
*/
static Color getColor(Descriptor descriptor) {
return getColor(descriptor, 1.0f);
}
/**
* Returns the color present in the specified descriptor.
* If no color can be found, this method returns black.
*/
static Color getColor(Descriptor descriptor, float alpha) {
Descriptor color = get(descriptor, "Clr ");
if (color == null) return Color.BLACK;
String colorType = color.classId.toString();
switch (colorType) {
case Descriptor.CLASS_ID_COLOR_RGB:
return colorFromRgb(color, alpha);
case Descriptor.CLASS_ID_COLOR_HSB:
return colorFromHsb(color, alpha);
case Descriptor.CLASS_ID_COLOR_CMYK:
return colorFromCmyk(color, alpha);
case Descriptor.CLASS_ID_COLOR_LAB:
return colorFromLab(color, alpha);
case Descriptor.CLASS_ID_COLOR_GRAY:
return colorFromGray(color, alpha);
}
if (alpha == 1.0f) {
return Color.BLACK;
}
return new Color(0.0f, 0.0f, 0.0f, alpha);
}
private static Color colorFromRgb(Descriptor color, float alpha) {
return new Color(
getFloat(color, "Rd ") / 255.0f,
getFloat(color, "Grn ") / 255.0f,
getFloat(color, "Bl ") / 255.0f,
alpha);
}
private static Color colorFromHsb(Descriptor color, float alpha) {
float[] rgb = Colors.hsbToRgb(
getUnitFloat(color, "H ", 0.0f),
getFloat(color, "Strt") / 100.0f,
getFloat(color, "Brgh") / 100.0f);
return new Color(rgb[0], rgb[1], rgb[2], alpha);
}
private static Color colorFromCmyk(Descriptor color, float alpha) {
float[] rgb = Colors.getCmykColorSpace().toRGB(new float[] {
getFloat(color, "Cyn "),
getFloat(color, "Mgnt"),
getFloat(color, "Ylw "),
getFloat(color, "Blck")
});
return new Color(rgb[0], rgb[1], rgb[2], alpha);
}
private static Color colorFromLab(Descriptor color, float alpha) {
float[] rgb = Colors.getLabColorSpace().toRGB(new float[] {
getFloat(color, "Lmnc"),
getFloat(color, "A "),
getFloat(color, "B ")
});
return new Color(rgb[0], rgb[1], rgb[2], alpha);
}
private static Color colorFromGray(Descriptor color, float alpha) {
float gray = Colors.linearRgbToRgb(getFloat(color, "Gry ") / 255.0f);
return new Color(gray, gray, gray, alpha);
}
/**
* Creates one or more path from a {@link PsdFile.ShapeMask} and add them to the
* specified shape info.
*/
static void createPaths(ShapeMask mask, ShapeInfo.Builder shapeInfo, AffineTransform transform) {
Path2D.Float path = null;
ShapeInfo.PathOp op = ShapeInfo.PathOp.ADD;
// Each Bézier knot in a PSD is made of three points:
// - the anchor (the knot or point itself)
// - the control point before the anchor
// - the control point after the anchor
//
// PSD Bézier knots must be converted to moveTo/curveTo commands.
// A curveTo() describes a cubic curve. To generate a curveTo() we
// need three points:
// - the next anchor (the destination point of the curveTo())
// - the control point after the previous anchor
// - the control point before the next anchor
PathType type = PathType.UNKNOWN;
BezierKnot firstKnot = null;
BezierKnot lastKnot = null;
int moveCount = 0;
for (PathRecord record : mask.pathRecords) {
switch (record.selector) {
// A "LENGTH" record marks the beginning of a new sub-path
// Closed subpath needs special handling at the end
case CLOSED_SUBPATH_LENGTH:
case OPEN_SUBPATH_LENGTH:
if (type == PathType.CLOSED) {
// If the previous subpath is of the closed type, close it now
addToPath(path, firstKnot, lastKnot);
path.closePath();
}
SubPath subPath = (SubPath) record.data;
if (subPath.tag != SubPath.NO_OP) {
if (path != null) {
path.transform(transform);
if (moveCount > 1) type = PathType.UNKNOWN;
shapeInfo.addPath(new Path.Builder().path(path).op(op).type(type).build());
}
op = getPathOp(subPath.op);
path = new Path2D.Float(Path2D.WIND_EVEN_ODD, subPath.knotCount);
moveCount = 0;
} else if (path == null) {
op = ShapeInfo.PathOp.ADD;
path = new Path2D.Float(Path2D.WIND_EVEN_ODD, subPath.knotCount);
moveCount = 0;
}
type = record.selector == OPEN_SUBPATH_LENGTH ? PathType.OPEN : PathType.CLOSED;
firstKnot = lastKnot = null;
break;
// Open and closed subpath knots can be handled the same way
// The linked/unlinked characteristic only matters to interactive
// editors and we happily throw away that information
case CLOSED_SUBPATH_KNOT_LINKED:
case CLOSED_SUBPATH_KNOT_UNLINKED:
case OPEN_SUBPATH_KNOT_LINKED:
case OPEN_SUBPATH_KNOT_UNLINKED:
if (path == null) continue;
BezierKnot knot = (BezierKnot) record.data;
if (lastKnot == null) {
// If we just started a subpath we need to insert a moveTo()
// using the new anchor
path.moveTo(
Bytes.fixed8_24ToFloat(knot.anchorX),
Bytes.fixed8_24ToFloat(knot.anchorY));
firstKnot = knot;
moveCount++;
} else {
// Otherwise let's curve to the new anchor
addToPath(path, knot, lastKnot);
}
lastKnot = knot;
break;
}
}
// Close the subpath if needed
if (type == PathType.CLOSED) {
addToPath(path, firstKnot, lastKnot);
path.closePath();
}
if (path != null) {
path.transform(transform);
if (moveCount > 1) type = PathType.UNKNOWN;
shapeInfo.addPath(new Path.Builder().path(path).op(op).type(type).build());
}
}
private static ShapeInfo.PathOp getPathOp(int op) {
switch (op) {
case SubPath.OP_XOR:
return ShapeInfo.PathOp.EXCLUSIVE_OR;
case SubPath.OP_MERGE:
return ShapeInfo.PathOp.ADD;
case SubPath.OP_SUBTRACT:
return ShapeInfo.PathOp.SUBTRACT;
case SubPath.OP_INTERSECT:
return ShapeInfo.PathOp.INTERSECT;
}
return ShapeInfo.PathOp.ADD;
}
private static void addToPath(Path2D.Float path, BezierKnot toKnot, BezierKnot lastKnot) {
float fromX = Bytes.fixed8_24ToFloat(lastKnot.anchorX);
float fromY = Bytes.fixed8_24ToFloat(lastKnot.anchorY);
float exitX = Bytes.fixed8_24ToFloat(lastKnot.controlExitX);
float exitY = Bytes.fixed8_24ToFloat(lastKnot.controlExitY);
float enterX = Bytes.fixed8_24ToFloat(toKnot.controlEnterX);
float enterY = Bytes.fixed8_24ToFloat(toKnot.controlEnterY);
float toX = Bytes.fixed8_24ToFloat(toKnot.anchorX);
float toY = Bytes.fixed8_24ToFloat(toKnot.anchorY);
if (exitX == fromX && exitY == fromY && enterX == toX && enterY == toY) {
path.lineTo(toX, toY);
} else {
path.curveTo(exitX, exitY, enterX, enterY, toX, toY);
}
}
}