diff --git a/CHANGELOG.md b/CHANGELOG.md index 6debcd91e..8cef2c92b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -77,6 +77,7 @@ All notable changes to this project will be documented in this file. - [#2469] Converting shape type did not convert gradient colors transparency - [#2470] Transform - paste matrix, edit current matrix not working - Do not allow to switch PlaceObjects in transform mode +- [#2471] Clipping - multiple clips handling, in display and also SVG export ## [23.0.1] - 2025-05-16 ### Fixed @@ -3881,6 +3882,7 @@ Major version of SWF to XML export changed to 2. [#2405]: https://www.free-decompiler.com/flash/issues/2405 [#1646]: https://www.free-decompiler.com/flash/issues/1646 [#2469]: https://www.free-decompiler.com/flash/issues/2469 +[#2471]: https://www.free-decompiler.com/flash/issues/2471 [#2427]: https://www.free-decompiler.com/flash/issues/2427 [#1826]: https://www.free-decompiler.com/flash/issues/1826 [#2448]: https://www.free-decompiler.com/flash/issues/2448 diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/timeline/SvgClip.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/timeline/SvgClip.java index 0783b1455..e4b063930 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/timeline/SvgClip.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/timeline/SvgClip.java @@ -16,6 +16,8 @@ */ package com.jpexs.decompiler.flash.timeline; +import java.awt.Shape; + /** * SVG clipping region * @@ -23,24 +25,24 @@ package com.jpexs.decompiler.flash.timeline; */ public class SvgClip { - /** - * Shape. - */ - public String shape; - /** * Depth. */ public int depth; + + /** + * Shape + */ + public Shape shape; /** * Constructs SvgClip. * - * @param shape Shape * @param depth Depth + * @param shape Shape */ - public SvgClip(String shape, int depth) { - this.shape = shape; + public SvgClip(int depth, Shape shape) { this.depth = depth; + this.shape = shape; } } diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/timeline/Timeline.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/timeline/Timeline.java index ba7098b26..811adb939 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/timeline/Timeline.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/timeline/Timeline.java @@ -82,13 +82,18 @@ import java.awt.RenderingHints; import java.awt.Shape; import java.awt.geom.AffineTransform; import java.awt.geom.Area; +import java.awt.geom.FlatteningPathIterator; +import java.awt.geom.PathIterator; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.io.IOException; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Set; @@ -212,6 +217,20 @@ public class Timeline { */ public static final int DRAW_MODE_SPRITES = 2; + /** + * Decimal format for clip path + */ + private static final DecimalFormat svgPathDecimalFormat; + + static { + DecimalFormatSymbols symbols = new DecimalFormatSymbols(Locale.US); + svgPathDecimalFormat = new DecimalFormat("0.##", symbols); // max 3 desetinná místa, žádné zbytečné nuly + } + + private static String formatDoubleSvg(double value) { + return svgPathDecimalFormat.format(value); + } + /** * Ensures that the timeline is initialized. */ @@ -1438,6 +1457,7 @@ public class Timeline { for (int c = 0; c < clips.size(); c++) { if (clips.get(c).depth < i) { clips.remove(c); + c--; clipChanged = true; } } @@ -1666,7 +1686,7 @@ public class Timeline { Frame frameObj = getFrame(frame); List clips = new ArrayList<>(); - + int maxDepth = getMaxDepth(); int clipCount = 0; Element clipGroup = null; @@ -1675,6 +1695,7 @@ public class Timeline { for (int c = 0; c < clips.size(); c++) { if (clips.get(c).depth < i) { clips.remove(c); + c--; clipChanged = true; } } @@ -1686,9 +1707,63 @@ public class Timeline { } if (!clips.isEmpty()) { - String clip = clips.get(clips.size() - 1).shape; // todo: merge clip areas + String clipName = exporter.getUniqueId("clipPath"); + exporter.createClipPath(null, clipName); + Area a = null; + for (SvgClip c : clips) { + if (a == null) { + a = new Area(c.shape); + } else { + a.intersect(new Area(c.shape)); + } + } + + StringBuilder sb = new StringBuilder(); + PathIterator pathIterator = new FlatteningPathIterator(a.getPathIterator(AffineTransform.getScaleInstance(1 / 20.0, 1 / 20.0)), 0.01); + float[] coords = new float[6]; + + while (!pathIterator.isDone()) { + int type = pathIterator.currentSegment(coords); + switch (type) { + case PathIterator.SEG_MOVETO: + sb.append("M ") + .append(formatDoubleSvg(coords[0])).append(" ") + .append(formatDoubleSvg(coords[1])).append(" "); + break; + case PathIterator.SEG_LINETO: + sb.append("L ") + .append(formatDoubleSvg(coords[0])).append(" ") + .append(formatDoubleSvg(coords[1])).append(" "); + break; + case PathIterator.SEG_QUADTO: + sb.append("Q ") + .append(formatDoubleSvg(coords[0])).append(" ") + .append(formatDoubleSvg(coords[1])).append(" ") + .append(formatDoubleSvg(coords[2])).append(" ") + .append(formatDoubleSvg(coords[3])).append(" "); + break; + case PathIterator.SEG_CUBICTO: + sb.append("C ") + .append(formatDoubleSvg(coords[0])).append(" ") + .append(formatDoubleSvg(coords[1])).append(" ") + .append(formatDoubleSvg(coords[2])).append(" ") + .append(formatDoubleSvg(coords[3])).append(" ") + .append(formatDoubleSvg(coords[4])).append(" ") + .append(formatDoubleSvg(coords[5])).append(" "); + break; + case PathIterator.SEG_CLOSE: + sb.append("Z "); + break; + } + pathIterator.next(); + } + + Element path = exporter.createElement("path"); + path.setAttribute("d", sb.toString().trim()); + exporter.addToGroup(path); + exporter.endGroup(); clipGroup = exporter.createSubGroup(null, null); - clipGroup.setAttribute("clip-path", "url(#" + clip + ")"); + clipGroup.setAttribute("clip-path", "url(#" + clipName + ")"); } clipCount = clips.size(); @@ -1729,13 +1804,9 @@ public class Timeline { // TODO: if (layer.filters != null) // TODO: if (layer.blendMode > 1) if (layer.clipDepth > -1) { - String clipName = exporter.getUniqueId("clipPath"); - Matrix mat = new Matrix(layer.matrix); - exporter.createClipPath(mat, clipName); - SvgClip clip = new SvgClip(clipName, layer.clipDepth); + Shape shape = drawable.getOutline(false, 0, 0, layer.ratio, new RenderContext(), layerMatrix, false, null, 1);; + SvgClip clip = new SvgClip(layer.clipDepth, shape); clips.add(clip); - drawable.toSVG(exporter, layer.ratio, clrTrans, level + 1, transformation, strokeTransformation); - exporter.endGroup(); } else { boolean createNew = false;