/* * 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.timeline; import com.jpexs.decompiler.flash.SWF; import com.jpexs.decompiler.flash.exporters.FrameExporter; import com.jpexs.decompiler.flash.exporters.commonshape.ExportRectangle; import com.jpexs.decompiler.flash.exporters.commonshape.Matrix; import com.jpexs.decompiler.flash.exporters.commonshape.SVGExporter; import com.jpexs.decompiler.flash.tags.DefineScalingGridTag; import com.jpexs.decompiler.flash.tags.DefineSpriteTag; import com.jpexs.decompiler.flash.tags.DoActionTag; import com.jpexs.decompiler.flash.tags.DoInitActionTag; import com.jpexs.decompiler.flash.tags.FrameLabelTag; import com.jpexs.decompiler.flash.tags.SetBackgroundColorTag; import com.jpexs.decompiler.flash.tags.ShowFrameTag; import com.jpexs.decompiler.flash.tags.SoundStreamBlockTag; import com.jpexs.decompiler.flash.tags.StartSound2Tag; import com.jpexs.decompiler.flash.tags.StartSoundTag; import com.jpexs.decompiler.flash.tags.Tag; import com.jpexs.decompiler.flash.tags.base.ASMSource; import com.jpexs.decompiler.flash.tags.base.ASMSourceContainer; import com.jpexs.decompiler.flash.tags.base.BoundedTag; import com.jpexs.decompiler.flash.tags.base.ButtonTag; import com.jpexs.decompiler.flash.tags.base.CharacterIdTag; import com.jpexs.decompiler.flash.tags.base.CharacterTag; import com.jpexs.decompiler.flash.tags.base.DrawableTag; import com.jpexs.decompiler.flash.tags.base.MorphShapeTag; import com.jpexs.decompiler.flash.tags.base.PlaceObjectTypeTag; import com.jpexs.decompiler.flash.tags.base.RemoveTag; import com.jpexs.decompiler.flash.tags.base.RenderContext; import com.jpexs.decompiler.flash.tags.base.ShapeTag; import com.jpexs.decompiler.flash.tags.base.SoundStreamHeadTypeTag; import com.jpexs.decompiler.flash.tags.base.TextTag; import com.jpexs.decompiler.flash.types.CLIPACTIONS; import com.jpexs.decompiler.flash.types.ColorTransform; import com.jpexs.decompiler.flash.types.MATRIX; import com.jpexs.decompiler.flash.types.RECT; import com.jpexs.decompiler.flash.types.filters.BlendComposite; import com.jpexs.decompiler.flash.types.filters.FILTER; import com.jpexs.helpers.Helper; import com.jpexs.helpers.SerializableImage; import java.awt.AlphaComposite; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Graphics2D; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.Shape; import java.awt.geom.AffineTransform; import java.awt.geom.Area; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.Stack; import org.w3c.dom.Element; /** * * @author JPEXS */ public class Timeline { public int id; public SWF swf; public RECT displayRect; public float frameRate; public Timelined timelined; public int maxDepth; public int fontFrameNum = -1; private final List<Frame> frames = new ArrayList<>(); private final Map<Integer, Integer> depthMaxFrame = new HashMap<>(); private final List<ASMSource> asmSources = new ArrayList<>(); private final List<ASMSourceContainer> asmSourceContainers = new ArrayList<>(); private final Map<ASMSource, Integer> actionFrames = new HashMap<>(); private final Map<SoundStreamHeadTypeTag, List<SoundStreamBlockTag>> soundStramBlocks = new HashMap<>(); private AS2Package as2RootPackage; public final List<Tag> otherTags = new ArrayList<>(); private boolean initialized = false; private Map<String, Integer> labelToFrame = new HashMap<>(); private void ensureInitialized() { if (!initialized) { initialize(); initialized = true; } } public List<Frame> getFrames() { ensureInitialized(); return frames; } public Frame getFrame(int index) { ensureInitialized(); if (index >= frames.size()) { return null; } return frames.get(index); } public void addFrame(Frame frame) { ensureInitialized(); frames.add(frame); maxDepth = getMaxDepthInternal(); calculateMaxDepthFrames(); } public AS2Package getAS2RootPackage() { ensureInitialized(); return as2RootPackage; } public Map<Integer, Integer> getDepthMaxFrame() { ensureInitialized(); return depthMaxFrame; } public List<SoundStreamBlockTag> getSoundStreamBlocks(SoundStreamHeadTypeTag head) { ensureInitialized(); return soundStramBlocks.get(head); } public Tag getParentTag() { return timelined instanceof Tag ? (Tag) timelined : null; } public void reset(SWF swf) { reset(swf, swf, 0, swf.displayRect); } public void reset(SWF swf, Timelined timelined, int id, RECT displayRect) { initialized = false; frames.clear(); depthMaxFrame.clear(); asmSources.clear(); asmSourceContainers.clear(); actionFrames.clear(); soundStramBlocks.clear(); otherTags.clear(); this.id = id; this.swf = swf; this.displayRect = displayRect; this.frameRate = swf.frameRate; this.timelined = timelined; as2RootPackage = new AS2Package(null, null, swf); } public final int getMaxDepth() { ensureInitialized(); return maxDepth; } private int getMaxDepthInternal() { int max_depth = 0; for (Frame f : frames) { for (int depth : f.layers.keySet()) { if (depth > max_depth) { max_depth = depth; } int clipDepth = f.layers.get(depth).clipDepth; if (clipDepth > max_depth) { max_depth = clipDepth; } } } return max_depth; } public int getFrameCount() { ensureInitialized(); return frames.size(); } public int getRealFrameCount() { ensureInitialized(); int cnt = 1; for (int i = 1; i < frames.size(); i++) { if (!frames.get(i).actions.isEmpty()) { cnt++; continue; } if (frames.get(i).layersChanged) { cnt++; } } return cnt; } public int getFrameForAction(ASMSource asm) { Integer frame = actionFrames.get(asm); if (frame == null) { return -1; } return frame; } public Timeline(SWF swf) { this(swf, swf, 0, swf.displayRect); } public Timeline(SWF swf, Timelined timelined, int id, RECT displayRect) { this.id = id; this.swf = swf; this.displayRect = displayRect; this.frameRate = swf.frameRate; this.timelined = timelined; as2RootPackage = new AS2Package(null, null, swf); } public int getFrameWithLabel(String label) { if (labelToFrame.containsKey(label)) { return labelToFrame.get(label); } return -1; } private void initialize() { int frameIdx = 0; Frame frame = new Frame(this, frameIdx++); frame.layersChanged = true; boolean newFrameNeeded = false; for (Tag t : timelined.getTags()) { boolean isNested = ShowFrameTag.isNestedTagType(t.getId()); if (isNested) { newFrameNeeded = true; frame.innerTags.add(t); } if (t instanceof ASMSourceContainer) { ASMSourceContainer asmSourceContainer = (ASMSourceContainer) t; if (!asmSourceContainer.getSubItems().isEmpty()) { if (isNested) { frame.actionContainers.add(asmSourceContainer); } else { asmSourceContainers.add(asmSourceContainer); } } } if (t instanceof FrameLabelTag) { newFrameNeeded = true; frame.label = ((FrameLabelTag) t).getLabelName(); frame.namedAnchor = ((FrameLabelTag) t).isNamedAnchor(); labelToFrame.put(frame.label, frames.size()); } else if (t instanceof StartSoundTag) { newFrameNeeded = true; frame.sounds.add(((StartSoundTag) t).soundId); } else if (t instanceof StartSound2Tag) { newFrameNeeded = true; frame.soundClasses.add(((StartSound2Tag) t).soundClassName); } else if (t instanceof SetBackgroundColorTag) { newFrameNeeded = true; frame.backgroundColor = ((SetBackgroundColorTag) t).backgroundColor; } else if (t instanceof PlaceObjectTypeTag) { newFrameNeeded = true; PlaceObjectTypeTag po = (PlaceObjectTypeTag) t; int depth = po.getDepth(); DepthState fl = frame.layers.get(depth); if (fl == null) { frame.layers.put(depth, fl = new DepthState(swf, frame)); } frame.layersChanged = true; fl.placeObjectTag = po; fl.minPlaceObjectNum = Math.max(fl.minPlaceObjectNum, po.getPlaceObjectNum()); int characterId = po.getCharacterId(); if (characterId != -1) { fl.characterId = characterId; } if (po.flagMove()) { MATRIX matrix2 = po.getMatrix(); if (matrix2 != null) { fl.matrix = matrix2; } String instanceName2 = po.getInstanceName(); if (instanceName2 != null) { fl.instanceName = instanceName2; } ColorTransform colorTransForm2 = po.getColorTransform(); if (colorTransForm2 != null) { fl.colorTransForm = colorTransForm2; } CLIPACTIONS clipActions2 = po.getClipActions(); if (clipActions2 != null) { fl.clipActions = clipActions2; } if (po.cacheAsBitmap()) { fl.cacheAsBitmap = true; } int blendMode2 = po.getBlendMode(); if (blendMode2 > 0) { fl.blendMode = blendMode2; } List<FILTER> filters2 = po.getFilters(); if (filters2 != null) { fl.filters = filters2; } int ratio2 = po.getRatio(); if (ratio2 > -1) { fl.ratio = ratio2; } int clipDepth2 = po.getClipDepth(); if (clipDepth2 > -1) { fl.clipDepth = clipDepth2; } } else { fl.matrix = po.getMatrix(); fl.instanceName = po.getInstanceName(); fl.colorTransForm = po.getColorTransform(); fl.cacheAsBitmap = po.cacheAsBitmap(); fl.blendMode = po.getBlendMode(); fl.filters = po.getFilters(); fl.ratio = po.getRatio(); fl.clipActions = po.getClipActions(); fl.clipDepth = po.getClipDepth(); } fl.key = characterId != -1; } else if (t instanceof RemoveTag) { newFrameNeeded = true; RemoveTag r = (RemoveTag) t; int depth = r.getDepth(); frame.layers.remove(depth); frame.layersChanged = true; } else if (t instanceof DoActionTag) { newFrameNeeded = true; frame.actions.add((DoActionTag) t); actionFrames.put((DoActionTag) t, frame.frame); } else if (t instanceof ShowFrameTag) { frame.showFrameTag = (ShowFrameTag) t; frames.add(frame); frame = new Frame(frame, frameIdx++); newFrameNeeded = false; } else if (t instanceof ASMSource) { asmSources.add((ASMSource) t); } else { otherTags.add(t); } } if (newFrameNeeded) { frames.add(frame); } maxDepth = getMaxDepthInternal(); detectTweens(); calculateMaxDepthFrames(); createASPackages(); if (timelined instanceof SWF) { // popuplate only for main timeline populateSoundStreamBlocks(0, timelined.getTags()); } initialized = true; } private void detectTweens() { for (int d = 1; d <= maxDepth; d++) { int characterId = -1; int len = 0; for (int f = 0; f <= frames.size(); f++) { DepthState ds = f >= frames.size() ? null : frames.get(f).layers.get(d); if (ds != null && characterId != -1 && ds.characterId == characterId) { len++; } else { if (characterId != -1) { int startPos = f - len; List<DepthState> matrices = new ArrayList<>(len); for (int k = 0; k < len; k++) { matrices.add(frames.get(startPos + k).layers.get(d)); } List<TweenRange> ranges = TweenDetector.detectRanges(matrices); for (TweenRange r : ranges) { for (int t = r.startPosition; t <= r.endPosition; t++) { DepthState layer = frames.get(startPos + t).layers.get(d); layer.motionTween = true; layer.key = false; } frames.get(startPos + r.startPosition).layers.get(d).key = true; } } len = 1; } characterId = ds == null ? -1 : ds.characterId; } } } private void calculateMaxDepthFrames() { depthMaxFrame.clear(); for (int d = 1; d <= maxDepth; d++) { for (int f = frames.size() - 1; f >= 0; f--) { if (frames.get(f).layers.get(d) != null) { depthMaxFrame.put(d, f); break; } } } } private void createASPackages() { for (ASMSource asm : asmSources) { if (asm instanceof DoInitActionTag) { DoInitActionTag initAction = (DoInitActionTag) asm; String path = swf.getExportName(initAction.spriteId); path = path != null ? path : "_unk_"; if (path.isEmpty()) { path = initAction.getExportFileName(); } String[] pathParts = path.contains(".") ? path.split("\\.") : new String[]{path}; AS2Package pkg = as2RootPackage; for (int pos = 0; pos < pathParts.length - 1; pos++) { String pathPart = pathParts[pos]; AS2Package subPkg = pkg.subPackages.get(pathPart); if (subPkg == null) { subPkg = new AS2Package(pathPart, pkg, swf); pkg.subPackages.put(pathPart, subPkg); } pkg = subPkg; } pkg.scripts.put(pathParts[pathParts.length - 1], asm); } } } private void populateSoundStreamBlocks(int containerId, Iterable<Tag> tags) { List<SoundStreamBlockTag> blocks = null; for (Tag t : tags) { if (t instanceof SoundStreamHeadTypeTag) { SoundStreamHeadTypeTag head = (SoundStreamHeadTypeTag) t; head.setVirtualCharacterId(containerId); blocks = new ArrayList<>(); soundStramBlocks.put(head, blocks); continue; } if (t instanceof DefineSpriteTag) { DefineSpriteTag sprite = (DefineSpriteTag) t; populateSoundStreamBlocks(sprite.getCharacterId(), sprite.getTags()); } if (blocks == null) { continue; } if (t instanceof SoundStreamBlockTag) { blocks.add((SoundStreamBlockTag) t); } } } public void getNeededCharacters(Set<Integer> usedCharacters) { for (int i = 0; i < getFrameCount(); i++) { getNeededCharacters(i, usedCharacters); } } public void getNeededCharacters(List<Integer> frames, Set<Integer> usedCharacters) { for (int frame = 0; frame < getFrameCount(); frame++) { getNeededCharacters(frame, usedCharacters); } } public void getNeededCharacters(int frame, Set<Integer> usedCharacters) { Frame frameObj = getFrame(frame); for (int depth : frameObj.layers.keySet()) { DepthState layer = frameObj.layers.get(depth); if (layer.characterId != -1) { if (!swf.getCharacters().containsKey(layer.characterId)) { continue; } usedCharacters.add(layer.characterId); swf.getCharacter(layer.characterId).getNeededCharactersDeep(usedCharacters); } } } public boolean replaceCharacter(int oldCharacterId, int newCharacterId) { boolean modified = false; for (int i = 0; i < timelined.getTags().size(); i++) { Tag t = timelined.getTags().get(i); if (t instanceof CharacterIdTag && ((CharacterIdTag) t).getCharacterId() == oldCharacterId) { ((CharacterIdTag) t).setCharacterId(newCharacterId); ((Tag) t).setModified(true); modified = true; } } return modified; } public boolean removeCharacter(int characterId) { boolean modified = false; for (int i = 0; i < timelined.getTags().size(); i++) { Tag t = timelined.getTags().get(i); if (t instanceof CharacterIdTag && ((CharacterIdTag) t).getCharacterId() == characterId) { timelined.removeTag(i); i--; modified = true; } } return modified; } public double roundToPixel(double val) { return Math.rint(val / SWF.unitDivisor) * SWF.unitDivisor; } /*public void toImage(int frame, int time, RenderContext renderContext, SerializableImage image, boolean isClip, Matrix transformation, Matrix prevTransformation, Matrix absoluteTransformation, ColorTransform colorTransform) { ExportRectangle scalingGrid = null; if (timelined instanceof CharacterTag) { DefineScalingGridTag sgt = ((CharacterTag) timelined).getScalingGridTag(); if (sgt != null) { scalingGrid = new ExportRectangle(sgt.splitter); } } if (scalingGrid == null || transformation.rotateSkew0 != 0 || transformation.rotateSkew1 != 0) { toImage(frame, time, renderContext, image, isClip, transformation, absoluteTransformation, colorTransform, null); return; } //9-slice scaling using DefineScalingGrid Matrix diffTransform = prevTransformation.inverse().preConcatenate(transformation); transformation = diffTransform; Matrix prevScale = new Matrix(); prevScale.scaleX = prevTransformation.scaleX; prevScale.scaleY = prevTransformation.scaleY; ExportRectangle boundsRect = new ExportRectangle(timelined.getRect()); 0 | 1 | 2 ------------ 3 | 4 | 5 ------------ 6 | 7 | 8 ExportRectangle[] targetRect = new ExportRectangle[9]; ExportRectangle[] sourceRect = new ExportRectangle[9]; Matrix[] transforms = new Matrix[9]; DefineScalingGridTag.getSlices(transformation, prevScale, boundsRect, scalingGrid, sourceRect, targetRect, transforms); for (int i = 0; i < targetRect.length; i++) { toImage(frame, time, renderContext, image, isClip, transforms[i], absoluteTransformation, colorTransform, targetRect[i]); } }*/ private void drawDrawable(Matrix strokeTransform, DepthState layer, Matrix layerMatrix, Graphics2D g, ColorTransform colorTransForm, int blendMode, List<Clip> clips, Matrix transformation, boolean isClip, int clipDepth, Matrix absMat, int time, int ratio, RenderContext renderContext, SerializableImage image, DrawableTag drawable, List<FILTER> filters, double unzoom, ColorTransform clrTrans) { Matrix drawMatrix = new Matrix(); int drawableFrameCount = drawable.getNumFrames(); if (drawableFrameCount == 0) { drawableFrameCount = 1; } RECT boundRect = drawable.getRect(); ExportRectangle rect = new ExportRectangle(boundRect); Matrix mat = transformation.concatenate(layerMatrix); rect = mat.transform(rect); boolean cacheAsBitmap = layer.cacheAsBitmap() && layer.placeObjectTag != null && drawable.isSingleFrame(); /* // draw bounds AffineTransform trans = mat.preConcatenate(Matrix.getScaleInstance(1 / SWF.unitDivisor)).toTransform(); g.setTransform(trans); BoundedTag b = (BoundedTag) drawable; g.setPaint(new Color(255, 255, 255, 128)); g.setComposite(BlendComposite.Invert); g.setStroke(new BasicStroke((int) SWF.unitDivisor)); RECT r = b.getRect(); g.setFont(g.getFont().deriveFont((float) (12 * SWF.unitDivisor))); g.drawString(drawable.toString(), r.Xmin + (int) (3 * SWF.unitDivisor), r.Ymin + (int) (15 * SWF.unitDivisor)); g.draw(new Rectangle(r.Xmin, r.Ymin, r.getWidth(), r.getHeight())); g.drawLine(r.Xmin, r.Ymin, r.Xmax, r.Ymax); g.drawLine(r.Xmax, r.Ymin, r.Xmin, r.Ymax); g.setComposite(AlphaComposite.Dst);*/ SerializableImage img = null; if (cacheAsBitmap && renderContext.displayObjectCache != null) { img = renderContext.displayObjectCache.get(layer.placeObjectTag); } int stateCount = renderContext.stateUnderCursor == null ? 0 : renderContext.stateUnderCursor.size(); int dframe; if (fontFrameNum != -1) { dframe = fontFrameNum; } else { dframe = time % drawableFrameCount; } if (filters != null && filters.size() > 0) { // calculate size after applying the filters double deltaXMax = 0; double deltaYMax = 0; for (FILTER filter : filters) { double x = filter.getDeltaX(); double y = filter.getDeltaY(); deltaXMax = Math.max(x, deltaXMax); deltaYMax = Math.max(y, deltaYMax); } rect.xMin -= deltaXMax * unzoom; rect.xMax += deltaXMax * unzoom; rect.yMin -= deltaYMax * unzoom; rect.yMax += deltaYMax * unzoom; } rect.xMin -= unzoom; rect.yMin -= unzoom; rect.xMin = Math.max(0, rect.xMin); rect.yMin = Math.max(0, rect.yMin); drawMatrix.translate(rect.xMin, rect.yMin); if (img == null) { int newWidth = (int) (rect.getWidth() / unzoom); int newHeight = (int) (rect.getHeight() / unzoom); int deltaX = (int) (rect.xMin / unzoom); int deltaY = (int) (rect.yMin / unzoom); newWidth = Math.min(image.getWidth() - deltaX, newWidth) + 1; newHeight = Math.min(image.getHeight() - deltaY, newHeight) + 1; if (newWidth <= 0 || newHeight <= 0) { return; } Matrix m = mat.preConcatenate(Matrix.getTranslateInstance(-rect.xMin, -rect.yMin)); //strokeTransform = strokeTransform.clone(); //strokeTransform.translate(-rect.xMin, -rect.yMin); if (drawable instanceof ButtonTag) { dframe = ButtonTag.FRAME_UP; if (renderContext.cursorPosition != null) { Shape buttonShape = drawable.getOutline(ButtonTag.FRAME_HITTEST, time, ratio, renderContext, absMat, true); if (buttonShape.contains(renderContext.cursorPosition)) { renderContext.mouseOverButton = (ButtonTag) drawable; if (renderContext.mouseButton > 0) { dframe = ButtonTag.FRAME_DOWN; } else { dframe = ButtonTag.FRAME_OVER; } } } } img = new SerializableImage(newWidth, newHeight, SerializableImage.TYPE_INT_ARGB_PRE); img.fillTransparent(); drawable.toImage(dframe, time, ratio, renderContext, img, isClip || clipDepth > -1, m, strokeTransform, absMat, clrTrans); if (filters != null) { for (FILTER filter : filters) { img = filter.apply(img); } } if (blendMode > 1) { if (colorTransForm != null) { img = colorTransForm.apply(img); } } if (cacheAsBitmap && renderContext.displayObjectCache != null) { renderContext.displayObjectCache.put(layer.placeObjectTag, img); } } drawMatrix.translateX /= unzoom; drawMatrix.translateY /= unzoom; AffineTransform trans = drawMatrix.toTransform(); switch (blendMode) { case 0: case 1: g.setComposite(AlphaComposite.SrcOver); break; case 2: // Layer g.setComposite(AlphaComposite.SrcOver); break; case 3: g.setComposite(BlendComposite.Multiply); break; case 4: g.setComposite(BlendComposite.Screen); break; case 5: g.setComposite(BlendComposite.Lighten); break; case 6: g.setComposite(BlendComposite.Darken); break; case 7: g.setComposite(BlendComposite.Difference); break; case 8: g.setComposite(BlendComposite.Add); break; case 9: g.setComposite(BlendComposite.Subtract); break; case 10: g.setComposite(BlendComposite.Invert); break; case 11: g.setComposite(BlendComposite.Alpha); break; case 12: g.setComposite(BlendComposite.Erase); break; case 13: g.setComposite(BlendComposite.Overlay); break; case 14: g.setComposite(BlendComposite.HardLight); break; default: // Not implemented g.setComposite(AlphaComposite.SrcOver); break; } if (clipDepth > -1) { BufferedImage mask = new BufferedImage(image.getWidth(), image.getHeight(), image.getType()); Graphics2D gm = (Graphics2D) mask.getGraphics(); gm.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); gm.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); gm.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); gm.setComposite(AlphaComposite.Src); gm.setColor(new Color(0, 0, 0, 0f)); gm.fillRect(0, 0, image.getWidth(), image.getHeight()); gm.setTransform(trans); gm.drawImage(img.getBufferedImage(), 0, 0, null); Clip clip = new Clip(Helper.imageToShape(mask), clipDepth); // Maybe we can get current outline instead converting from image (?) clips.add(clip); } else { if (renderContext.cursorPosition != null) { if (drawable instanceof DefineSpriteTag) { if (renderContext.stateUnderCursor.size() > stateCount) { renderContext.stateUnderCursor.add(layer); } } else if (absMat.transform(new ExportRectangle(boundRect)).contains(renderContext.cursorPosition)) { Shape shape = drawable.getOutline(dframe, time, layer.ratio, renderContext, absMat, true); if (shape.contains(renderContext.cursorPosition)) { renderContext.stateUnderCursor.add(layer); } } } if (renderContext.borderImage != null) { Graphics2D g2 = (Graphics2D) renderContext.borderImage.getGraphics(); g2.setPaint(Color.red); g2.setStroke(new BasicStroke(2)); Shape shape = drawable.getOutline(dframe, time, layer.ratio, renderContext, absMat.preConcatenate(Matrix.getScaleInstance(1 / SWF.unitDivisor)), true); g2.draw(shape); } g.setTransform(trans); g.drawImage(img.getBufferedImage(), 0, 0, null); } } public void toImage(int frame, int time, RenderContext renderContext, SerializableImage image, boolean isClip, Matrix transformation, Matrix strokeTransformation, Matrix absoluteTransformation, ColorTransform colorTransform) { double unzoom = SWF.unitDivisor; if (getFrameCount() <= frame) { return; } Frame frameObj = getFrame(frame); Graphics2D g = (Graphics2D) image.getGraphics(); Shape prevClip = g.getClip(); //if (targetRect != null) { // g.setClip(new Rectangle2D.Double(targetRect.xMin, targetRect.yMin, targetRect.getWidth(), targetRect.getHeight())); //} g.setPaint(frameObj.backgroundColor.toColor()); g.fill(new Rectangle(image.getWidth(), image.getHeight())); g.setTransform(transformation.toTransform()); List<Clip> clips = new ArrayList<>(); int maxDepth = getMaxDepth(); int clipCount = 0; for (int i = 1; i <= maxDepth; i++) { boolean clipChanged = clipCount != clips.size(); for (int c = 0; c < clips.size(); c++) { if (clips.get(c).depth < i) { clips.remove(c); clipChanged = true; } } if (clipChanged) { if (clips.size() > 0) { Area clip = null; for (Clip clip1 : clips) { Shape shape = clip1.shape; if (clip == null) { clip = new Area(shape); } else { clip.intersect(new Area(shape)); } } g.setTransform(new AffineTransform()); g.setClip(clip); // draw clip border //g.setPaint(Color.red); //g.setStroke(new BasicStroke(2)); //g.draw(clip); } else { g.setClip(null); } clipCount = clips.size(); } if (!frameObj.layers.containsKey(i)) { continue; } DepthState layer = frameObj.layers.get(i); if (!swf.getCharacters().containsKey(layer.characterId)) { continue; } if (!layer.isVisible) { continue; } CharacterTag character = swf.getCharacter(layer.characterId); Matrix layerMatrix = new Matrix(layer.matrix); Matrix mat = transformation.concatenate(layerMatrix); Matrix absMat = absoluteTransformation.concatenate(layerMatrix); ColorTransform clrTrans = colorTransform; if (layer.colorTransForm != null && layer.blendMode <= 1) { // Normal blend mode clrTrans = clrTrans == null ? layer.colorTransForm : colorTransform.merge(layer.colorTransForm); } boolean showPlaceholder = false; if (character instanceof DrawableTag) { RECT scalingRect = null; DefineScalingGridTag sgt = character.getScalingGridTag(); if (sgt != null) { scalingRect = sgt.splitter; } if (scalingRect != null) { ExportRectangle[] sourceRect = new ExportRectangle[9]; ExportRectangle[] targetRect = new ExportRectangle[9]; Matrix[] transforms = new Matrix[9]; //mat => image //t => //Matrix tx = transformation.concatenate(layerMatrix); DrawableTag dr = (DrawableTag) character; ExportRectangle boundsRect = new ExportRectangle(dr.getRect()); ExportRectangle targetBoundsRect = layerMatrix.transform(boundsRect); DefineScalingGridTag.getSlices(targetBoundsRect, boundsRect, new ExportRectangle(scalingRect), sourceRect, targetRect, transforms); Shape c = g.getClip(); AffineTransform origTransform = g.getTransform(); for (int s = 0; s < 9; s++) { g.setTransform(new AffineTransform()); ExportRectangle p1 = transformation.transform(targetRect[s]); g.setClip(c); Rectangle2D r = new Rectangle2D.Double(p1.xMin, p1.yMin, p1.getWidth(), p1.getHeight()); g.setClip(r); drawDrawable(strokeTransformation.preConcatenate(layerMatrix), layer, transforms[s], g, colorTransform, layer.blendMode, clips, transformation, isClip, layer.clipDepth, absMat, time, layer.ratio, renderContext, image, (DrawableTag) character, layer.filters, unzoom, clrTrans); } g.setClip(c); /* for (int s = 0; s < 9; s++) { g.setTransform(new AffineTransform()); ExportRectangle p1 = transformation.transform(targetRect[s]); g.setClip(c); Rectangle2D r = new Rectangle2D.Double(p1.xMin, p1.yMin, p1.getWidth(), p1.getHeight()); g.setColor(Color.blue); g.draw(r); }*/ g.setTransform(origTransform); } else { drawDrawable(strokeTransformation, layer, layerMatrix, g, colorTransform, layer.blendMode, clips, transformation, isClip, layer.clipDepth, absMat, time, layer.ratio, renderContext, image, (DrawableTag) character, layer.filters, unzoom, clrTrans); } } else if (character instanceof BoundedTag) { showPlaceholder = true; } if (showPlaceholder) { AffineTransform trans = mat.preConcatenate(Matrix.getScaleInstance(1 / SWF.unitDivisor)).toTransform(); g.setTransform(trans); BoundedTag b = (BoundedTag) character; g.setPaint(new Color(255, 255, 255, 128)); g.setComposite(BlendComposite.Invert); g.setStroke(new BasicStroke((int) SWF.unitDivisor)); RECT r = b.getRect(); g.setFont(g.getFont().deriveFont((float) (12 * SWF.unitDivisor))); g.drawString(character.toString(), r.Xmin + (int) (3 * SWF.unitDivisor), r.Ymin + (int) (15 * SWF.unitDivisor)); g.draw(new Rectangle(r.Xmin, r.Ymin, r.getWidth(), r.getHeight())); g.drawLine(r.Xmin, r.Ymin, r.Xmax, r.Ymax); g.drawLine(r.Xmax, r.Ymin, r.Xmin, r.Ymax); g.setComposite(AlphaComposite.Dst); } } g.setTransform(new AffineTransform()); g.setClip(prevClip); } public void toSVG(int frame, int time, DepthState stateUnderCursor, int mouseButton, SVGExporter exporter, ColorTransform colorTransform, int level) throws IOException { if (getFrameCount() <= frame) { return; } Frame frameObj = getFrame(frame); List<SvgClip> clips = new ArrayList<>(); int maxDepth = getMaxDepth(); int clipCount = 0; Element clipGroup = null; for (int i = 1; i <= maxDepth; i++) { boolean clipChanged = clipCount != clips.size(); for (int c = 0; c < clips.size(); c++) { if (clips.get(c).depth < i) { clips.remove(c); clipChanged = true; } } if (clipChanged) { if (clipGroup != null) { exporter.endGroup(); } if (clips.size() > 0) { String clip = clips.get(clips.size() - 1).shape; // todo: merge clip areas clipGroup = exporter.createSubGroup(null, null); clipGroup.setAttribute("clip-path", "url(#" + clip + ")"); } clipCount = clips.size(); } if (!frameObj.layers.containsKey(i)) { continue; } DepthState layer = frameObj.layers.get(i); if (!swf.getCharacters().containsKey(layer.characterId)) { continue; } if (!layer.isVisible) { continue; } CharacterTag character = swf.getCharacter(layer.characterId); ColorTransform clrTrans = colorTransform; if (layer.colorTransForm != null && layer.blendMode <= 1) { // Normal blend mode clrTrans = clrTrans == null ? layer.colorTransForm : colorTransform.merge(layer.colorTransForm); } if (character instanceof DrawableTag) { DrawableTag drawable = (DrawableTag) character; String assetName; Tag drawableTag = (Tag) drawable; RECT boundRect = drawable.getRect(); if (exporter.exportedTags.containsKey(drawableTag)) { assetName = exporter.exportedTags.get(drawableTag); } else { assetName = getTagIdPrefix(drawableTag, exporter); exporter.exportedTags.put(drawableTag, assetName); exporter.createDefGroup(new ExportRectangle(boundRect), assetName); drawable.toSVG(exporter, layer.ratio, clrTrans, level + 1); exporter.endGroup(); } ExportRectangle rect = new ExportRectangle(boundRect); // TODO: if (layer.filters != null) // TODO: if (layer.blendMode > 1) if (layer.clipDepth > -1) { String clipName = exporter.getUniqueId("clipPath"); exporter.createClipPath(new Matrix(), clipName); SvgClip clip = new SvgClip(clipName, layer.clipDepth); clips.add(clip); Matrix mat = Matrix.getTranslateInstance(rect.xMin, rect.yMin).preConcatenate(new Matrix(layer.matrix)); exporter.addUse(mat, boundRect, assetName, layer.instanceName); exporter.endGroup(); } else { Matrix mat = Matrix.getTranslateInstance(rect.xMin, rect.yMin).preConcatenate(new Matrix(layer.matrix)); exporter.addUse(mat, boundRect, assetName, layer.instanceName); } } } if (clipGroup != null) { exporter.endGroup(); } } private static String getTagIdPrefix(Tag tag, SVGExporter exporter) { if (tag instanceof ShapeTag) { return exporter.getUniqueId("shape"); } if (tag instanceof MorphShapeTag) { return exporter.getUniqueId("morphshape"); } if (tag instanceof DefineSpriteTag) { return exporter.getUniqueId("sprite"); } if (tag instanceof TextTag) { return exporter.getUniqueId("text"); } if (tag instanceof ButtonTag) { return exporter.getUniqueId("button"); } return exporter.getUniqueId("tag"); } public void toHtmlCanvas(StringBuilder result, double unitDivisor, List<Integer> frames) { FrameExporter.framesToHtmlCanvas(result, unitDivisor, this, frames, 0, null, 0, displayRect, null, null); } public void getSounds(int frame, int time, ButtonTag mouseOverButton, int mouseButton, List<Integer> sounds, List<String> soundClasses) { Frame fr = getFrame(frame); sounds.addAll(fr.sounds); soundClasses.addAll(fr.soundClasses); for (int d = maxDepth; d >= 0; d--) { DepthState ds = fr.layers.get(d); if (ds != null) { CharacterTag c = swf.getCharacter(ds.characterId); if (c instanceof Timelined) { int frameCount = ((Timelined) c).getTimeline().frames.size(); if (frameCount == 0) { continue; } int dframe = time % frameCount; if (c instanceof ButtonTag) { dframe = ButtonTag.FRAME_UP; if (mouseOverButton == c) { if (mouseButton > 0) { dframe = ButtonTag.FRAME_DOWN; } else { dframe = ButtonTag.FRAME_OVER; } } } ((Timelined) c).getTimeline().getSounds(dframe, time, mouseOverButton, mouseButton, sounds, soundClasses); } } } } public Shape getOutline(int frame, int time, RenderContext renderContext, Matrix transformation, boolean stroked) { Frame fr = getFrame(frame); Area area = new Area(); Stack<Clip> clips = new Stack<>(); for (int d = maxDepth; d >= 0; d--) { Clip currentClip = null; for (int i = clips.size() - 1; i >= 0; i--) { Clip cl = clips.get(i); if (cl.depth <= d) { clips.remove(i); } } if (!clips.isEmpty()) { currentClip = clips.peek(); } DepthState layer = fr.layers.get(d); if (layer == null) { continue; } if (!layer.isVisible) { continue; } CharacterTag character = swf.getCharacter(layer.characterId); if (character instanceof DrawableTag) { DrawableTag drawable = (DrawableTag) character; Matrix m = transformation.concatenate(new Matrix(layer.matrix)); int drawableFrameCount = drawable.getNumFrames(); if (drawableFrameCount == 0) { drawableFrameCount = 1; } int dframe = time % drawableFrameCount; if (character instanceof ButtonTag) { dframe = ButtonTag.FRAME_UP; if (renderContext.cursorPosition != null) { ButtonTag buttonTag = (ButtonTag) character; Shape buttonShape = buttonTag.getOutline(ButtonTag.FRAME_HITTEST, time, layer.ratio, renderContext, m, stroked); if (buttonShape.contains(renderContext.cursorPosition)) { if (renderContext.mouseButton > 0) { dframe = ButtonTag.FRAME_DOWN; } else { dframe = ButtonTag.FRAME_OVER; } } } } Shape cshape = ((DrawableTag) character).getOutline(dframe, time, layer.ratio, renderContext, m, stroked); Area addArea = new Area(cshape); if (currentClip != null) { Area a = new Area(new Rectangle(displayRect.Xmin, displayRect.Ymin, displayRect.getWidth(), displayRect.getHeight())); a.subtract(new Area(currentClip.shape)); addArea.subtract(a); } if (layer.clipDepth > -1) { Clip clip = new Clip(addArea, layer.clipDepth); clips.push(clip); } else { area.add(addArea); } } } return area; } public boolean isSingleFrame() { for (int i = 0; i < getFrameCount(); i++) { if (!isSingleFrame(i)) { return false; } } return true; } public boolean isSingleFrame(int frame) { Frame frameObj = getFrame(frame); for (int i = 1; i <= maxDepth; i++) { if (!frameObj.layers.containsKey(i)) { continue; } DepthState layer = frameObj.layers.get(i); if (!swf.getCharacters().containsKey(layer.characterId)) { continue; } if (!layer.isVisible) { continue; } CharacterTag character = swf.getCharacter(layer.characterId); if (character instanceof DrawableTag) { DrawableTag drawable = (DrawableTag) character; if (!drawable.isSingleFrame()) { return false; } } } return true; } @Override public boolean equals(Object obj) { if (obj instanceof Timeline) { Timeline timelineObj = (Timeline) obj; return timelined.equals(timelineObj.timelined); } return false; } }