/* * Copyright (C) 2010-2016 JPEXS, All rights reserved. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3.0 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. */ package com.jpexs.decompiler.flash.exporters.shape; import com.jpexs.decompiler.flash.SWF; import com.jpexs.decompiler.flash.exporters.commonshape.ExportRectangle; import com.jpexs.decompiler.flash.exporters.commonshape.Matrix; import com.jpexs.decompiler.flash.tags.base.ImageTag; import com.jpexs.decompiler.flash.types.ColorTransform; import com.jpexs.decompiler.flash.types.FILLSTYLE; import com.jpexs.decompiler.flash.types.GRADIENT; import com.jpexs.decompiler.flash.types.GRADRECORD; import com.jpexs.decompiler.flash.types.LINESTYLE2; import com.jpexs.decompiler.flash.types.RGB; import com.jpexs.decompiler.flash.types.SHAPE; import com.jpexs.helpers.SerializableImage; import java.awt.AlphaComposite; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Graphics2D; import java.awt.LinearGradientPaint; import java.awt.MultipleGradientPaint; import java.awt.Paint; import java.awt.Point; import java.awt.RadialGradientPaint; import java.awt.Rectangle; import java.awt.Shape; import java.awt.Stroke; import java.awt.TexturePaint; import java.awt.geom.AffineTransform; import java.awt.geom.GeneralPath; import java.awt.geom.NoninvertibleTransformException; import java.util.ArrayList; import java.util.List; /** * * @author JPEXS */ public class BitmapExporter extends ShapeExporterBase { private static final Point POINT_NEG16384_0 = new Point(-16384, 0); private static final Point POINT_16384_0 = new Point(16384, 0); private static final AffineTransform IDENTITY_TRANSFORM = new AffineTransform(); private SerializableImage image; private Graphics2D graphics; private final Color defaultColor; private final SWF swf; private final GeneralPath path = new GeneralPath(GeneralPath.WIND_EVEN_ODD); //For correct intersections display; private Paint fillPaint; private AffineTransform fillTransform; private Paint linePaint; private AffineTransform lineTransform; private Color lineColor; private Stroke lineStroke; private Stroke defaultStroke; private Matrix strokeTransformation; private class TransformedStroke implements Stroke { /** * To make this serializable without problems. */ private static final long serialVersionUID = 1; /** * the AffineTransform used to transform the shape before stroking. */ private final AffineTransform transform; /** * The inverse of {@link #transform}, used to transform back after * stroking. */ private final AffineTransform inverse; /** * Our base stroke. */ private final Stroke stroke; /** * Creates a TransformedStroke based on another Stroke and an * AffineTransform. */ public TransformedStroke(Stroke base, AffineTransform at) throws NoninvertibleTransformException { this.transform = new AffineTransform(at); this.inverse = transform.createInverse(); this.stroke = base; } /** * Strokes the given Shape with this stroke, creating an outline. * * This outline is distorted by our AffineTransform relative to the * outline which would be given by the base stroke, but only in terms of * scaling (i.e. thickness of the lines), as translation and rotation * are undone after the stroking. */ @Override public Shape createStrokedShape(Shape s) { Shape sTrans = transform.createTransformedShape(s); Shape sTransStroked = stroke.createStrokedShape(sTrans); Shape sStroked = inverse.createTransformedShape(sTransStroked); return sStroked; } } public static void export(SWF swf, SHAPE shape, Color defaultColor, SerializableImage image, Matrix transformation, Matrix strokeTransformation, ColorTransform colorTransform) { BitmapExporter exporter = new BitmapExporter(swf, shape, defaultColor, colorTransform); exporter.exportTo(image, transformation, strokeTransformation); } private BitmapExporter(SWF swf, SHAPE shape, Color defaultColor, ColorTransform colorTransform) { super(swf, shape, colorTransform); this.swf = swf; this.defaultColor = defaultColor; } private void exportTo(SerializableImage image, Matrix transformation, Matrix strokeTransformation) { this.image = image; this.strokeTransformation = strokeTransformation.clone(); this.strokeTransformation.scaleX /= SWF.unitDivisor; this.strokeTransformation.scaleY /= SWF.unitDivisor; graphics = (Graphics2D) image.getGraphics(); AffineTransform at = transformation.toTransform(); at.preConcatenate(AffineTransform.getScaleInstance(1 / SWF.unitDivisor, 1 / SWF.unitDivisor)); graphics.setTransform(at); defaultStroke = graphics.getStroke(); super.export(); } public SerializableImage getImage() { return image; } @Override public void beginShape() { } @Override public void endShape() { } @Override public void beginFills() { } @Override public void endFills() { } @Override public void beginLines() { } @Override public void endLines(boolean close) { if (close) { path.closePath(); } finalizePath(); } @Override public void beginFill(RGB color) { finalizePath(); if (color == null) { fillPaint = defaultColor; } else { fillPaint = color.toColor(); } } @Override public void beginGradientFill(int type, GRADRECORD[] gradientRecords, Matrix matrix, int spreadMethod, int interpolationMethod, float focalPointRatio) { finalizePath(); MultipleGradientPaint.ColorSpaceType cstype = MultipleGradientPaint.ColorSpaceType.SRGB; if (interpolationMethod == GRADIENT.INTERPOLATION_LINEAR_RGB_MODE) { cstype = MultipleGradientPaint.ColorSpaceType.LINEAR_RGB; } switch (type) { case FILLSTYLE.LINEAR_GRADIENT: { List<Color> colors = new ArrayList<>(); List<Float> ratios = new ArrayList<>(); for (int i = 0; i < gradientRecords.length; i++) { if ((i > 0) && (gradientRecords[i - 1].ratio == gradientRecords[i].ratio)) { continue; } ratios.add(gradientRecords[i].getRatioFloat()); colors.add(gradientRecords[i].color.toColor()); } float[] ratiosArr = new float[ratios.size()]; for (int i = 0; i < ratios.size(); i++) { ratiosArr[i] = ratios.get(i); } Color[] colorsArr = colors.toArray(new Color[colors.size()]); MultipleGradientPaint.CycleMethod cm = MultipleGradientPaint.CycleMethod.NO_CYCLE; if (spreadMethod == GRADIENT.SPREAD_PAD_MODE) { cm = MultipleGradientPaint.CycleMethod.NO_CYCLE; } else if (spreadMethod == GRADIENT.SPREAD_REFLECT_MODE) { cm = MultipleGradientPaint.CycleMethod.REFLECT; } else if (spreadMethod == GRADIENT.SPREAD_REPEAT_MODE) { cm = MultipleGradientPaint.CycleMethod.REPEAT; } fillPaint = new LinearGradientPaint(POINT_NEG16384_0, POINT_16384_0, ratiosArr, colorsArr, cm, cstype, IDENTITY_TRANSFORM); fillTransform = matrix.toTransform(); } break; case FILLSTYLE.RADIAL_GRADIENT: { List<Color> colors = new ArrayList<>(); List<Float> ratios = new ArrayList<>(); for (int i = 0; i < gradientRecords.length; i++) { if ((i > 0) && (gradientRecords[i - 1].ratio == gradientRecords[i].ratio)) { continue; } ratios.add(gradientRecords[i].getRatioFloat()); colors.add(gradientRecords[i].color.toColor()); } float[] ratiosArr = new float[ratios.size()]; for (int i = 0; i < ratios.size(); i++) { ratiosArr[i] = ratios.get(i); } Color[] colorsArr = colors.toArray(new Color[colors.size()]); MultipleGradientPaint.CycleMethod cm = MultipleGradientPaint.CycleMethod.NO_CYCLE; if (spreadMethod == GRADIENT.SPREAD_PAD_MODE) { cm = MultipleGradientPaint.CycleMethod.NO_CYCLE; } else if (spreadMethod == GRADIENT.SPREAD_REFLECT_MODE) { cm = MultipleGradientPaint.CycleMethod.REFLECT; } else if (spreadMethod == GRADIENT.SPREAD_REPEAT_MODE) { cm = MultipleGradientPaint.CycleMethod.REPEAT; } fillPaint = new RadialGradientPaint(new java.awt.Point(0, 0), 16384, ratiosArr, colorsArr, cm); fillTransform = matrix.toTransform(); } break; case FILLSTYLE.FOCAL_RADIAL_GRADIENT: { List<Color> colors = new ArrayList<>(); List<Float> ratios = new ArrayList<>(); for (int i = 0; i < gradientRecords.length; i++) { if ((i > 0) && (gradientRecords[i - 1].ratio == gradientRecords[i].ratio)) { continue; } ratios.add(gradientRecords[i].getRatioFloat()); colors.add(gradientRecords[i].color.toColor()); } float[] ratiosArr = new float[ratios.size()]; for (int i = 0; i < ratios.size(); i++) { ratiosArr[i] = ratios.get(i); } Color[] colorsArr = colors.toArray(new Color[colors.size()]); MultipleGradientPaint.CycleMethod cm = MultipleGradientPaint.CycleMethod.NO_CYCLE; if (spreadMethod == GRADIENT.SPREAD_PAD_MODE) { cm = MultipleGradientPaint.CycleMethod.NO_CYCLE; } else if (spreadMethod == GRADIENT.SPREAD_REFLECT_MODE) { cm = MultipleGradientPaint.CycleMethod.REFLECT; } else if (spreadMethod == GRADIENT.SPREAD_REPEAT_MODE) { cm = MultipleGradientPaint.CycleMethod.REPEAT; } fillPaint = new RadialGradientPaint(new java.awt.Point(0, 0), 16384, new java.awt.Point((int) (focalPointRatio * 16384), 0), ratiosArr, colorsArr, cm, cstype, AffineTransform.getTranslateInstance(0, 0)); fillTransform = matrix.toTransform(); } break; } } @Override public void beginBitmapFill(int bitmapId, Matrix matrix, boolean repeat, boolean smooth, ColorTransform colorTransform) { finalizePath(); ImageTag imageTag = swf.getImage(bitmapId); if (imageTag != null) { SerializableImage img = imageTag.getImageCached(); if (img != null) { if (colorTransform != null) { img = colorTransform.apply(img); } fillPaint = new TexturePaint(img.getBufferedImage(), new java.awt.Rectangle(img.getWidth(), img.getHeight())); fillTransform = matrix.toTransform(); return; } } // fill with red in case any error fillPaint = Color.RED; fillTransform = matrix.toTransform(); } @Override public void endFill() { finalizePath(); fillPaint = null; } @Override public void lineStyle(double thickness, RGB color, boolean pixelHinting, String scaleMode, int startCaps, int endCaps, int joints, float miterLimit) { finalizePath(); linePaint = null; lineTransform = null; lineColor = color == null ? null : color.toColor(); int capStyle = BasicStroke.CAP_ROUND; switch (startCaps) { case LINESTYLE2.NO_CAP: capStyle = BasicStroke.CAP_BUTT; break; case LINESTYLE2.ROUND_CAP: capStyle = BasicStroke.CAP_ROUND; break; case LINESTYLE2.SQUARE_CAP: capStyle = BasicStroke.CAP_SQUARE; break; } int joinStyle = BasicStroke.JOIN_ROUND; switch (joints) { case LINESTYLE2.BEVEL_JOIN: joinStyle = BasicStroke.JOIN_BEVEL; break; case LINESTYLE2.MITER_JOIN: joinStyle = BasicStroke.JOIN_MITER; break; case LINESTYLE2.ROUND_JOIN: joinStyle = BasicStroke.JOIN_ROUND; break; } switch (scaleMode) { case "VERTICAL": thickness *= strokeTransformation.scaleY; break; case "HORIZONTAL": thickness *= strokeTransformation.scaleX; break; case "NORMAL": thickness *= Math.max(strokeTransformation.scaleX, strokeTransformation.scaleY); break; } if (thickness < 0) { thickness = -thickness; } if (joinStyle == BasicStroke.JOIN_MITER) { lineStroke = new BasicStroke((float) thickness, capStyle, joinStyle, miterLimit); } else { lineStroke = new BasicStroke((float) thickness, capStyle, joinStyle); } // Do not scale strokes automatically: try { AffineTransform t = (AffineTransform) graphics.getTransform().clone(); t.translate(-0.5, -0.5); lineStroke = new TransformedStroke(lineStroke, t); } catch (NoninvertibleTransformException net) { // ignore } } @Override public void lineGradientStyle(int type, GRADRECORD[] gradientRecords, Matrix matrix, int spreadMethod, int interpolationMethod, float focalPointRatio) { MultipleGradientPaint.ColorSpaceType cstype = MultipleGradientPaint.ColorSpaceType.SRGB; if (interpolationMethod == GRADIENT.INTERPOLATION_LINEAR_RGB_MODE) { cstype = MultipleGradientPaint.ColorSpaceType.LINEAR_RGB; } switch (type) { case FILLSTYLE.LINEAR_GRADIENT: { List<Color> colors = new ArrayList<>(); List<Float> ratios = new ArrayList<>(); for (int i = 0; i < gradientRecords.length; i++) { if ((i > 0) && (gradientRecords[i - 1].ratio == gradientRecords[i].ratio)) { continue; } ratios.add(gradientRecords[i].getRatioFloat()); colors.add(gradientRecords[i].color.toColor()); } float[] ratiosArr = new float[ratios.size()]; for (int i = 0; i < ratios.size(); i++) { ratiosArr[i] = ratios.get(i); } Color[] colorsArr = colors.toArray(new Color[colors.size()]); MultipleGradientPaint.CycleMethod cm = MultipleGradientPaint.CycleMethod.NO_CYCLE; if (spreadMethod == GRADIENT.SPREAD_PAD_MODE) { cm = MultipleGradientPaint.CycleMethod.NO_CYCLE; } else if (spreadMethod == GRADIENT.SPREAD_REFLECT_MODE) { cm = MultipleGradientPaint.CycleMethod.REFLECT; } else if (spreadMethod == GRADIENT.SPREAD_REPEAT_MODE) { cm = MultipleGradientPaint.CycleMethod.REPEAT; } linePaint = new LinearGradientPaint(POINT_NEG16384_0, POINT_16384_0, ratiosArr, colorsArr, cm, cstype, IDENTITY_TRANSFORM); lineTransform = matrix.toTransform(); } break; case FILLSTYLE.RADIAL_GRADIENT: { List<Color> colors = new ArrayList<>(); List<Float> ratios = new ArrayList<>(); for (int i = 0; i < gradientRecords.length; i++) { if ((i > 0) && (gradientRecords[i - 1].ratio == gradientRecords[i].ratio)) { continue; } ratios.add(gradientRecords[i].getRatioFloat()); colors.add(gradientRecords[i].color.toColor()); } float[] ratiosArr = new float[ratios.size()]; for (int i = 0; i < ratios.size(); i++) { ratiosArr[i] = ratios.get(i); } Color[] colorsArr = colors.toArray(new Color[colors.size()]); MultipleGradientPaint.CycleMethod cm = MultipleGradientPaint.CycleMethod.NO_CYCLE; switch (spreadMethod) { case GRADIENT.SPREAD_PAD_MODE: cm = MultipleGradientPaint.CycleMethod.NO_CYCLE; break; case GRADIENT.SPREAD_REFLECT_MODE: cm = MultipleGradientPaint.CycleMethod.REFLECT; break; case GRADIENT.SPREAD_REPEAT_MODE: cm = MultipleGradientPaint.CycleMethod.REPEAT; break; } linePaint = new RadialGradientPaint(new java.awt.Point(0, 0), 16384, ratiosArr, colorsArr, cm); lineTransform = matrix.toTransform(); } break; case FILLSTYLE.FOCAL_RADIAL_GRADIENT: { List<Color> colors = new ArrayList<>(); List<Float> ratios = new ArrayList<>(); for (int i = 0; i < gradientRecords.length; i++) { if ((i > 0) && (gradientRecords[i - 1].ratio == gradientRecords[i].ratio)) { continue; } ratios.add(gradientRecords[i].getRatioFloat()); colors.add(gradientRecords[i].color.toColor()); } float[] ratiosArr = new float[ratios.size()]; for (int i = 0; i < ratios.size(); i++) { ratiosArr[i] = ratios.get(i); } Color[] colorsArr = colors.toArray(new Color[colors.size()]); MultipleGradientPaint.CycleMethod cm = MultipleGradientPaint.CycleMethod.NO_CYCLE; if (spreadMethod == GRADIENT.SPREAD_PAD_MODE) { cm = MultipleGradientPaint.CycleMethod.NO_CYCLE; } else if (spreadMethod == GRADIENT.SPREAD_REFLECT_MODE) { cm = MultipleGradientPaint.CycleMethod.REFLECT; } else if (spreadMethod == GRADIENT.SPREAD_REPEAT_MODE) { cm = MultipleGradientPaint.CycleMethod.REPEAT; } linePaint = new RadialGradientPaint(new java.awt.Point(0, 0), 16384, new java.awt.Point((int) (focalPointRatio * 16384), 0), ratiosArr, colorsArr, cm, cstype, AffineTransform.getTranslateInstance(0, 0)); lineTransform = matrix.toTransform(); } break; } } @Override public void moveTo(double x, double y) { path.moveTo(x, y); } @Override public void lineTo(double x, double y) { path.lineTo(x, y); } @Override public void curveTo(double controlX, double controlY, double anchorX, double anchorY) { path.quadTo(controlX, controlY, anchorX, anchorY); } protected void finalizePath() { if (fillPaint != null) { graphics.setComposite(AlphaComposite.SrcOver); if (fillPaint instanceof MultipleGradientPaint) { AffineTransform oldAf = graphics.getTransform(); graphics.setClip(path); Matrix inverse = null; try { double scx = fillTransform.getScaleX(); double scy = fillTransform.getScaleY(); double shx = fillTransform.getShearX(); double shy = fillTransform.getShearY(); double det = scx * scy - shx * shy; if (Math.abs(det) <= Double.MIN_VALUE) { // use only the translate values // todo: make it better fillTransform.setToTranslation(fillTransform.getTranslateX(), fillTransform.getTranslateY()); } inverse = new Matrix(new AffineTransform(fillTransform).createInverse()); } catch (NoninvertibleTransformException ex) { // it should never happen as we already checked the determinant of the matrix } fillTransform.preConcatenate(oldAf); graphics.setTransform(fillTransform); graphics.setPaint(fillPaint); if (inverse != null) { ExportRectangle rect = inverse.transform(new ExportRectangle(path.getBounds2D())); double minX = rect.xMin; double minY = rect.yMin; graphics.fill(new java.awt.Rectangle((int) minX, (int) minY, (int) (rect.xMax - minX), (int) (rect.yMax - minY))); } graphics.setTransform(oldAf); graphics.setClip(null); } else if (fillPaint instanceof TexturePaint) { AffineTransform oldAf = graphics.getTransform(); graphics.setClip(path); Matrix inverse = null; try { double scx = fillTransform.getScaleX(); double scy = fillTransform.getScaleY(); double shx = fillTransform.getShearX(); double shy = fillTransform.getShearY(); double det = scx * scy - shx * shy; if (Math.abs(det) <= Double.MIN_VALUE) { // use only the translate values // todo: make it better fillTransform.setToTranslation(fillTransform.getTranslateX(), fillTransform.getTranslateY()); } inverse = new Matrix(new AffineTransform(fillTransform).createInverse()); } catch (NoninvertibleTransformException ex) { // it should never happen as we already checked the determinant of the matrix } fillTransform.preConcatenate(oldAf); graphics.setTransform(fillTransform); graphics.setPaint(fillPaint); if (inverse != null) { ExportRectangle rect = inverse.transform(new ExportRectangle(path.getBounds2D())); double minX = rect.xMin; double minY = rect.yMin; graphics.fill(new Rectangle((int) minX, (int) minY, (int) (rect.xMax - minX), (int) (rect.yMax - minY))); } graphics.setTransform(oldAf); graphics.setClip(null); } else { graphics.setPaint(fillPaint); graphics.fill(path); } } if (linePaint != null && lineStroke != null) { Shape strokedShape = lineStroke.createStrokedShape(path); graphics.setComposite(AlphaComposite.SrcOver); if (linePaint instanceof MultipleGradientPaint) { AffineTransform oldAf = graphics.getTransform(); graphics.setClip(strokedShape); Matrix inverse = null; try { double scx = lineTransform.getScaleX(); double scy = lineTransform.getScaleY(); double shx = lineTransform.getShearX(); double shy = lineTransform.getShearY(); double det = scx * scy - shx * shy; if (Math.abs(det) <= Double.MIN_VALUE) { // use only the translate values // todo: make it better lineTransform.setToTranslation(lineTransform.getTranslateX(), lineTransform.getTranslateY()); } inverse = new Matrix(new AffineTransform(lineTransform).createInverse()); } catch (NoninvertibleTransformException ex) { // it should never happen as we already checked the determinant of the matrix } lineTransform.preConcatenate(oldAf); graphics.setTransform(lineTransform); graphics.setPaint(linePaint); if (inverse != null) { ExportRectangle rect = inverse.transform(new ExportRectangle(strokedShape.getBounds2D())); double minX = rect.xMin; double minY = rect.yMin; graphics.fill(new java.awt.Rectangle((int) minX, (int) minY, (int) (rect.xMax - minX), (int) (rect.yMax - minY))); } graphics.setTransform(oldAf); graphics.setClip(null); } else if (linePaint instanceof TexturePaint) { AffineTransform oldAf = graphics.getTransform(); graphics.setClip(strokedShape); Matrix inverse = null; try { double scx = lineTransform.getScaleX(); double scy = lineTransform.getScaleY(); double shx = lineTransform.getShearX(); double shy = lineTransform.getShearY(); double det = scx * scy - shx * shy; if (Math.abs(det) <= Double.MIN_VALUE) { // use only the translate values // todo: make it better lineTransform.setToTranslation(lineTransform.getTranslateX(), lineTransform.getTranslateY()); } inverse = new Matrix(new AffineTransform(lineTransform).createInverse()); } catch (NoninvertibleTransformException ex) { // it should never happen as we already checked the determinant of the matrix } lineTransform.preConcatenate(oldAf); graphics.setTransform(lineTransform); graphics.setPaint(linePaint); if (inverse != null) { ExportRectangle rect = inverse.transform(new ExportRectangle(path.getBounds2D())); double minX = rect.xMin; double minY = rect.yMin; graphics.fill(new Rectangle((int) minX, (int) minY, (int) (rect.xMax - minX), (int) (rect.yMax - minY))); } graphics.setTransform(oldAf); graphics.setClip(null); } else { graphics.setPaint(linePaint); graphics.fill(strokedShape); } } else if (lineColor != null) { graphics.setColor(lineColor); graphics.setStroke(lineStroke == null ? defaultStroke : lineStroke); graphics.draw(path); } path.reset(); lineStroke = null; lineColor = null; fillPaint = null; } }