| /* |
| * |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You 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.adobe.internal.fxg.swf; |
| |
| import java.awt.Image; |
| import java.awt.image.PixelGrabber; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| import com.adobe.fxg.FXGVersion; |
| import com.adobe.internal.fxg.dom.AbstractFXGNode; |
| import com.adobe.internal.fxg.dom.BitmapGraphicNode; |
| import com.adobe.internal.fxg.dom.FillNode; |
| import com.adobe.internal.fxg.dom.fills.BitmapFillNode; |
| import com.adobe.internal.fxg.dom.types.FillMode; |
| |
| import flash.graphics.images.ImageUtil; |
| import flash.swf.SwfConstants; |
| import flash.swf.Tag; |
| import flash.swf.TagValues; |
| import flash.swf.tags.DefineBits; |
| import flash.swf.tags.DefineBitsLossless; |
| import flash.swf.tags.DefineShape; |
| import flash.swf.types.FillStyle; |
| import flash.swf.types.Matrix; |
| import flash.swf.types.Rect; |
| import flash.swf.types.ShapeRecord; |
| import flash.swf.types.ShapeWithStyle; |
| import flash.swf.types.StraightEdgeRecord; |
| import flash.swf.types.StyleChangeRecord; |
| import flash.util.FileUtils; |
| |
| /** |
| * Utilities to help create SWF DefineBits and DefineBitsLossess image tags. |
| * |
| * @author Peter Farland |
| * @author Sujata Das |
| */ |
| public class ImageHelper |
| { |
| |
| /** The Constant MIME_GIF. */ |
| public static final String MIME_GIF = "image/gif"; |
| |
| /** The Constant MIME_JPEG. */ |
| public static final String MIME_JPEG = "image/jpeg"; |
| |
| /** The Constant MIME_JPG. */ |
| public static final String MIME_JPG = "image/jpg"; |
| |
| /** The Constant MIME_PNG. */ |
| public static final String MIME_PNG = "image/png"; |
| |
| /** |
| * Creates a rectangle for the given width and height as a DefineShape. The |
| * shape is painted with a bitmap FillStyle with the given DefineBits |
| * tag. |
| * |
| * @param tag The DefineBits tag encoding the image. |
| * @param node The BitmapGraphicNode. |
| * @return A rectangle of given width and height as a DefineShape with a |
| * bitmap fill. |
| */ |
| public static DefineShape createShapeForImage(DefineBits tag, BitmapGraphicNode node) |
| { |
| double width = node.width; |
| double height = node.height; |
| boolean repeat = node.repeat; |
| FillMode fillMode = node.fillMode; |
| FXGVersion fileVersion = node.getFileVersion(); |
| |
| // Use default width/height information if none specified |
| if (Double.isNaN(width)) |
| width = tag.width; |
| |
| if (Double.isNaN(height)) |
| height = tag.height; |
| |
| // Create Fill Style |
| Matrix matrix = new Matrix(); |
| matrix.scaleX = (int)(SwfConstants.TWIPS_PER_PIXEL * SwfConstants.FIXED_POINT_MULTIPLE); |
| matrix.scaleY = (int)(SwfConstants.TWIPS_PER_PIXEL * SwfConstants.FIXED_POINT_MULTIPLE); |
| matrix.hasScale = true; // Apply runtime scale of 20x for twips |
| |
| FillStyle fs = null; |
| if (fileVersion.equalTo(FXGVersion.v1_0)) |
| { |
| if (repeat) |
| fs = new FillStyle(FillStyle.FILL_BITS, matrix, tag); |
| else |
| fs = new FillStyle(FillStyle.FILL_BITS | FillStyle.FILL_BITS_CLIP, matrix, tag); |
| } |
| else |
| { |
| if (fillMode.equals(FillMode.REPEAT)) |
| { |
| fs = new FillStyle(FillStyle.FILL_BITS, matrix, tag); |
| } |
| else if (fillMode.equals(FillMode.CLIP)) |
| { |
| fs = new FillStyle(FillStyle.FILL_BITS | FillStyle.FILL_BITS_CLIP, matrix, tag); |
| } |
| else if (fillMode.equals(FillMode.SCALE)) |
| { |
| //override the scale for matrix |
| matrix.scaleX = (int)StrictMath.rint((width*SwfConstants.TWIPS_PER_PIXEL * SwfConstants.FIXED_POINT_MULTIPLE)/(double)tag.width); |
| matrix.scaleY = (int)StrictMath.rint((height*SwfConstants.TWIPS_PER_PIXEL * SwfConstants.FIXED_POINT_MULTIPLE)/(double)tag.height); |
| |
| //fill style does not matter much since the entire area is filled with bitmap |
| fs = new FillStyle(FillStyle.FILL_BITS | FillStyle.FILL_BITS_CLIP, matrix, tag); |
| } |
| } |
| |
| // Apply Fill Styles |
| ShapeWithStyle sws = new ShapeWithStyle(); |
| sws.fillstyles = new ArrayList<FillStyle>(1); |
| sws.fillstyles.add(fs); |
| |
| // Build Raw SWF Shape |
| List<ShapeRecord> shapeRecords = ShapeHelper.rectangle(width, height); |
| ShapeHelper.setStyles(shapeRecords, 0, 1, 0); |
| sws.shapeRecords = shapeRecords; |
| |
| |
| // Wrap up into a SWF DefineShape Tag |
| DefineShape defineShape = new DefineShape(Tag.stagDefineShape4); |
| defineShape.bounds = TypeHelper.rect(width, height); |
| defineShape.edgeBounds = defineShape.bounds; |
| defineShape.shapeWithStyle = sws; |
| |
| |
| return defineShape; |
| } |
| |
| /** |
| * Determines whether the bitmap image should be clipped. |
| * |
| * @param node The BitmapGraphicNode. |
| * @param imageTag the image tag |
| * |
| * @return boolean if bitmap should be clipped. |
| */ |
| public static boolean bitmapImageNeedsClipping(DefineBits imageTag, BitmapGraphicNode node) |
| { |
| if (((node.getFileVersion().equalTo(FXGVersion.v1_0)) && !node.repeat) || |
| (node.fillMode.equals(FillMode.CLIP))) |
| { |
| if ((imageTag.width < node.width) || (imageTag.height < node.height)) |
| return true; |
| } |
| |
| return false; |
| |
| } |
| |
| /** |
| * Determines whether the bitmap fill mode is repeat. |
| * |
| * @param node The BitmapFillNode. |
| * @return boolean if bitmap should repeat. |
| */ |
| public static boolean bitmapFillModeIsRepeat(BitmapFillNode node) |
| { |
| if (((node.getFileVersion().equalTo(FXGVersion.v1_0)) && node.repeat) || |
| (node.fillMode.equals(FillMode.REPEAT))) |
| { |
| return true; |
| } |
| |
| return false; |
| |
| } |
| |
| /** |
| * Checks if bitmap fill is clipped. |
| * |
| * @param fill the fill |
| * |
| * @return true, if bitmap fill is clipped |
| */ |
| public static boolean isBitmapFillWithClip(FillNode fill) |
| { |
| if (fill == null) |
| return false; |
| |
| if (fill instanceof BitmapFillNode) |
| { |
| BitmapFillNode bFill = (BitmapFillNode) fill; |
| if (ImageHelper.bitmapFillModeIsRepeat(bFill)) |
| { |
| return false; |
| } |
| else |
| { |
| if ((bFill.getFileVersion().equalTo(FXGVersion.v2_0)) && (bFill.fillMode == FillMode.SCALE)) |
| { |
| if (Double.isNaN(bFill.scaleX) && Double.isNaN(bFill.scaleY) && |
| Double.isNaN(bFill.x) && Double.isNaN(bFill.y) && |
| (Double.isNaN(bFill.rotation) || Math.abs(bFill.rotation) < AbstractFXGNode.EPSILON) && |
| bFill.matrix == null) |
| return false; |
| else |
| return true; |
| } |
| else |
| { |
| return true; |
| } |
| } |
| |
| } |
| return false; |
| |
| } |
| |
| /** |
| * Creates the define bits. |
| * |
| * @param in the in |
| * @param mimeType the mime type |
| * |
| * @return the define bits |
| * |
| * @throws IOException Signals that an I/O exception has occurred. |
| */ |
| public static DefineBits createDefineBits(InputStream in, String mimeType) throws IOException |
| { |
| // TODO: Investigate faster mechanisms of getting image info and pixels |
| byte[] bytes = FileUtils.toByteArray(in); |
| Image image = ImageUtil.getImage(bytes); |
| if (mimeType == null) |
| { |
| throw new IOException("Unsupported MIME type"); |
| } |
| |
| PixelGrabber pixelGrabber = null; |
| try |
| { |
| pixelGrabber = ImageUtil.getPixelGrabber(image, null); |
| } |
| catch (Exception e) |
| { |
| throw new IOException("Error reading image"); |
| } |
| |
| int width = pixelGrabber.getWidth(); |
| int height = pixelGrabber.getHeight(); |
| |
| // JPEG |
| if (MIME_JPG.equals(mimeType) || MIME_JPEG.equals(mimeType)) |
| { |
| DefineBits imageTag = new DefineBits(Tag.stagDefineBitsJPEG2); |
| imageTag.data = bytes; |
| imageTag.width = width; |
| imageTag.height = height; |
| return imageTag; |
| } |
| // PNG or GIF |
| else if (MIME_PNG.equals(mimeType) || MIME_GIF.equals(mimeType)) |
| { |
| int[] pixels = (int[])pixelGrabber.getPixels(); |
| DefineBitsLossless imageTag = createDefineBitsLossless(pixels, width, height); |
| return imageTag; |
| } |
| else |
| { |
| throw new IOException("Unsupported MIME type: " + mimeType); |
| } |
| } |
| |
| /** |
| * Creates the define bits lossless. |
| * |
| * @param pixels the pixels |
| * @param width the width |
| * @param height the height |
| * |
| * @return the define bits lossless |
| */ |
| public static DefineBitsLossless createDefineBitsLossless(int[] pixels, int width, int height) |
| { |
| DefineBitsLossless defineBitsLossless = new DefineBitsLossless(TagValues.stagDefineBitsLossless2); |
| defineBitsLossless.format = DefineBitsLossless.FORMAT_24_BIT_RGB; |
| defineBitsLossless.width = width; |
| defineBitsLossless.height = height; |
| defineBitsLossless.data = new byte[pixels.length * 4]; |
| |
| for (int i = 0; i < pixels.length; i++) |
| { |
| int offset = i * 4; |
| int alpha = (pixels[i] >> 24) & 0xFF; |
| defineBitsLossless.data[offset] = (byte)alpha; |
| |
| // Premultiply the alpha channel |
| if (defineBitsLossless.data[offset] != 0) |
| { |
| int red = (pixels[i] >> 16) & 0xFF; |
| defineBitsLossless.data[offset + 1] = (byte)((red * alpha) / 255); |
| int green = (pixels[i] >> 8) & 0xFF; |
| defineBitsLossless.data[offset + 2] = (byte)((green * alpha) / 255); |
| int blue = pixels[i] & 0xFF; |
| defineBitsLossless.data[offset + 3] = (byte)((blue * alpha) / 255); |
| } |
| } |
| |
| return defineBitsLossless; |
| } |
| |
| /** |
| * Create 9 sliced shape. |
| * |
| * @param bitmap the bitmap |
| * @param r the r |
| * @param width the width |
| * @param height the height |
| * |
| * @return the define shape |
| */ |
| public static DefineShape create9SlicedShape(DefineBits bitmap, Rect r, double width, double height) |
| { |
| // Use default width/height information if none specified |
| if (Double.isNaN(width)) |
| width = bitmap.width; |
| |
| if (Double.isNaN(height)) |
| height = bitmap.height; |
| |
| int slt = r.xMin; |
| int srt = r.xMax; |
| int stt = r.yMin; |
| int sbt = r.yMax; |
| |
| List<ShapeRecord> shapeRecords = new ArrayList<ShapeRecord>(50); |
| ShapeWithStyle sws = new ShapeWithStyle(); |
| sws.fillstyles = new ArrayList<FillStyle>(9); |
| sws.shapeRecords = shapeRecords; |
| |
| // Apply runtime scale of 20x for twips |
| Matrix matrix = new Matrix(); |
| matrix.scaleX = (int)(SwfConstants.TWIPS_PER_PIXEL * SwfConstants.FIXED_POINT_MULTIPLE); |
| matrix.scaleY = (int)(SwfConstants.TWIPS_PER_PIXEL * SwfConstants.FIXED_POINT_MULTIPLE); |
| matrix.hasScale = true; |
| |
| // Create 9 identical fillstyles as a work around |
| for (int i = 0; i < 9; i++) |
| { |
| FillStyle fs = new FillStyle(FillStyle.FILL_BITS | FillStyle.FILL_BITS_NOSMOOTH, matrix, bitmap); |
| sws.fillstyles.add(fs); |
| } |
| |
| int dxa = slt; |
| int dxb = srt - slt; |
| int dxc = (int)(bitmap.width * SwfConstants.TWIPS_PER_PIXEL) - srt; |
| |
| int dya = stt; |
| int dyb = sbt - stt; |
| int dyc = (int)(bitmap.height * SwfConstants.TWIPS_PER_PIXEL) - sbt; |
| |
| // border |
| shapeRecords.add(new StyleChangeRecord(0, dya, 0, 0, 1)); |
| shapeRecords.add(new StraightEdgeRecord(0, -dya)); |
| shapeRecords.add(new StraightEdgeRecord(dxa, 0)); |
| shapeRecords.add(new StyleChangeRecord(0, 0, 2)); |
| shapeRecords.add(new StraightEdgeRecord(dxb, 0)); |
| shapeRecords.add(new StyleChangeRecord(0, 0, 3)); |
| shapeRecords.add(new StraightEdgeRecord(dxc, 0)); |
| shapeRecords.add(new StraightEdgeRecord(0, dya)); |
| shapeRecords.add(new StyleChangeRecord(0, 0, 6)); |
| shapeRecords.add(new StraightEdgeRecord(0, dyb)); |
| shapeRecords.add(new StyleChangeRecord(0, 0 ,9)); |
| shapeRecords.add(new StraightEdgeRecord(0, dyc)); |
| shapeRecords.add(new StraightEdgeRecord(-dxc, 0)); |
| shapeRecords.add(new StyleChangeRecord(0, 0, 8)); |
| shapeRecords.add(new StraightEdgeRecord(-dxb, 0)); |
| shapeRecords.add(new StyleChangeRecord(0, 0, 7)); |
| shapeRecords.add(new StraightEdgeRecord(-dxa, 0)); |
| shapeRecords.add(new StraightEdgeRecord(0, -dyc)); |
| shapeRecords.add(new StyleChangeRecord(0, 0, 4)); |
| shapeRecords.add(new StraightEdgeRecord(0, -dyb)); |
| |
| // down 1 |
| shapeRecords.add(new StyleChangeRecord(dxa, 0, 0, 2, 1)); |
| shapeRecords.add(new StraightEdgeRecord(0, dya)); |
| shapeRecords.add(new StyleChangeRecord(0, 5, 4)); |
| shapeRecords.add(new StraightEdgeRecord(0, dyb)); |
| shapeRecords.add(new StyleChangeRecord(0, 8, 7)); |
| shapeRecords.add(new StraightEdgeRecord(0, dyc)); |
| |
| // down 2 |
| shapeRecords.add(new StyleChangeRecord(dxa + dxb, 0, 0, 3, 2)); |
| shapeRecords.add(new StraightEdgeRecord(0, dya)); |
| shapeRecords.add(new StyleChangeRecord(0, 6, 5)); |
| shapeRecords.add(new StraightEdgeRecord(0, dyb)); |
| shapeRecords.add(new StyleChangeRecord(0, 9, 8)); |
| shapeRecords.add(new StraightEdgeRecord(0, dyc)); |
| |
| // right 1 |
| shapeRecords.add(new StyleChangeRecord(0, dya, 0, 1, 4)); |
| shapeRecords.add(new StraightEdgeRecord(dxa, 0)); |
| shapeRecords.add(new StyleChangeRecord(0, 2, 5)); |
| shapeRecords.add(new StraightEdgeRecord(dxb, 0)); |
| shapeRecords.add(new StyleChangeRecord(0, 3, 6)); |
| shapeRecords.add(new StraightEdgeRecord(dxc, 0)); |
| |
| // right 2 |
| shapeRecords.add(new StyleChangeRecord(0, dya + dyb, 0, 4, 7)); |
| shapeRecords.add(new StraightEdgeRecord(dxa, 0)); |
| shapeRecords.add(new StyleChangeRecord(0, 5, 8)); |
| shapeRecords.add(new StraightEdgeRecord(dxb, 0)); |
| shapeRecords.add(new StyleChangeRecord(0, 6, 9)); |
| shapeRecords.add(new StraightEdgeRecord(dxc, 0)); |
| |
| DefineShape shape = new DefineShape(TagValues.stagDefineShape4); |
| shape.bounds = TypeHelper.rect(width, height); |
| shape.edgeBounds = shape.bounds; |
| shape.shapeWithStyle = sws; |
| return shape; |
| } |
| |
| /** |
| * Guess mime type. |
| * |
| * @param path the path |
| * |
| * @return the string |
| */ |
| public static String guessMimeType(String path) |
| { |
| if (path != null) |
| { |
| path = path.toLowerCase(); |
| if (path.endsWith(".png")) |
| return MIME_PNG; |
| if (path.endsWith(".gif")) |
| return MIME_GIF; |
| if (path.endsWith(".jpg")) |
| return MIME_JPG; |
| if (path.endsWith(".jpeg")) |
| return MIME_JPEG; |
| } |
| |
| return null; |
| } |
| } |