diff --git a/CHANGELOG.md b/CHANGELOG.md index b1ffe2010..2abde7ef6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ All notable changes to this project will be documented in this file. - AS3 Direct editation - Error when accessing inaccessible namespace - AS3 ambiguous namespace detection (back again) - [#2648] Dockerfile +- SVG export - Gradient bevel filter ### Fixed - [#2643] APNG export - images containing multiple IDAT chunks diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/commonshape/SVGExporter.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/commonshape/SVGExporter.java index f272cf5fe..527441e7d 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/commonshape/SVGExporter.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/commonshape/SVGExporter.java @@ -564,6 +564,11 @@ public class SVGExporter implements RequiresNormalizedFonts { return; } Element filtersElement = _svg.createElement("filter"); + filtersElement.setAttribute("x", "-100%"); + filtersElement.setAttribute("y", "-100%"); + filtersElement.setAttribute("width", "300%"); + filtersElement.setAttribute("height", "300%"); + filtersElement.setAttribute("color-interpolation-filters", "sRGB"); String filterId = getUniqueId("filter"); String in = "SourceGraphic"; boolean empty = true; diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/types/filters/BEVELFILTER.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/types/filters/BEVELFILTER.java index 12f1ddb56..982562928 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/types/filters/BEVELFILTER.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/types/filters/BEVELFILTER.java @@ -129,130 +129,11 @@ public class BEVELFILTER extends FILTER { @Override public String toSvg(Document document, Element filtersElement, SVGExporter exporter, String in) { - int type = Filtering.INNER; - if (onTop && !innerShadow) { - type = Filtering.FULL; - } else if (!innerShadow) { - type = Filtering.OUTER; - } - - String shadowInner = null; - String hilightInner = null; - if (type != Filtering.OUTER) { - String hilight = dropShadowSvg(distance, angle, highlightColor, true, true, true, 0, 0, strength, passes, document, filtersElement, exporter, in); - String shadow = dropShadowSvg(distance, angle + Math.PI, shadowColor, true, true, true, 0, 0, strength, passes, document, filtersElement, exporter, in); - - Element feComposite1 = document.createElement("feComposite"); - feComposite1.setAttribute("in", hilight); - feComposite1.setAttribute("in2", shadow); - feComposite1.setAttribute("operator", "out"); - hilightInner = exporter.getUniqueId("filterResult"); - feComposite1.setAttribute("result", hilightInner); - filtersElement.appendChild(feComposite1); - - Element feComposite2 = document.createElement("feComposite"); - feComposite2.setAttribute("in", shadow); - feComposite2.setAttribute("in2", hilight); - feComposite2.setAttribute("operator", "out"); - shadowInner = exporter.getUniqueId("filterResult"); - feComposite2.setAttribute("result", shadowInner); - filtersElement.appendChild(feComposite2); - } - - String shadowOuter = null; - String hilightOuter = null; - - if (type != Filtering.INNER) { - String hilight = dropShadowSvg(distance, angle + Math.PI, highlightColor, false, true, true, 0, 0, strength, passes, document, filtersElement, exporter, in); - String shadow = dropShadowSvg(distance, angle, shadowColor, false, true, true, 0, 0, strength, passes, document, filtersElement, exporter, in); - - Element feComposite1 = document.createElement("feComposite"); - feComposite1.setAttribute("in", hilight); - feComposite1.setAttribute("in2", shadow); - feComposite1.setAttribute("operator", "out"); - shadowOuter = exporter.getUniqueId("filterResult"); - feComposite1.setAttribute("result", shadowOuter); - filtersElement.appendChild(feComposite1); - - Element feComposite2 = document.createElement("feComposite"); - feComposite2.setAttribute("in", shadow); - feComposite2.setAttribute("in2", hilight); - feComposite2.setAttribute("operator", "out"); - hilightOuter = exporter.getUniqueId("filterResult"); - feComposite2.setAttribute("result", hilightOuter); - filtersElement.appendChild(feComposite2); - } - - String hilight = null; - String shadow = null; - - switch (type) { - case Filtering.OUTER: - hilight = hilightOuter; - shadow = shadowOuter; - break; - case Filtering.INNER: - hilight = hilightInner; - shadow = shadowInner; - break; - case Filtering.FULL: - Element feComposite1 = document.createElement("feComposite"); - feComposite1.setAttribute("in", hilightInner); - feComposite1.setAttribute("in2", hilightOuter); - feComposite1.setAttribute("operator", "over"); - hilight = exporter.getUniqueId("filterResult"); - feComposite1.setAttribute("result", hilight); - filtersElement.appendChild(feComposite1); - - Element feComposite2 = document.createElement("feComposite"); - feComposite2.setAttribute("in", shadowInner); - feComposite2.setAttribute("in2", shadowOuter); - feComposite2.setAttribute("operator", "over"); - shadow = exporter.getUniqueId("filterResult"); - feComposite2.setAttribute("result", shadow); - filtersElement.appendChild(feComposite2); - break; - } - - Element feComposite3 = document.createElement("feComposite"); - feComposite3.setAttribute("in", shadow); - feComposite3.setAttribute("in2", hilight); - feComposite3.setAttribute("operator", "over"); - String result = exporter.getUniqueId("filterResult"); - feComposite3.setAttribute("result", result); - filtersElement.appendChild(feComposite3); - - result = blurSvg(blurX, blurY, passes, document, filtersElement, exporter, result); - - if (type == Filtering.INNER) { - Element feComposite4 = document.createElement("feComposite"); - feComposite4.setAttribute("in", result); - feComposite4.setAttribute("in2", in); - feComposite4.setAttribute("operator", "in"); - result = exporter.getUniqueId("filterResult"); - feComposite4.setAttribute("result", result); - filtersElement.appendChild(feComposite4); - } - if (type == Filtering.OUTER) { - Element feComposite4 = document.createElement("feComposite"); - feComposite4.setAttribute("in", result); - feComposite4.setAttribute("in2", in); - feComposite4.setAttribute("operator", "out"); - result = exporter.getUniqueId("filterResult"); - feComposite4.setAttribute("result", result); - filtersElement.appendChild(feComposite4); - } - - if (!knockout) { - Element feComposite4 = document.createElement("feComposite"); - feComposite4.setAttribute("in", result); - feComposite4.setAttribute("in2", in); - feComposite4.setAttribute("operator", "over"); - result = exporter.getUniqueId("filterResult"); - feComposite4.setAttribute("result", result); - filtersElement.appendChild(feComposite4); - } - return result; + RGBA shadowColorTransparent = new RGBA(shadowColor); + shadowColorTransparent.alpha = 0; + RGBA highlightColorTransparent = new RGBA(highlightColor); + highlightColorTransparent.alpha = 0; + return bevelSvg(distance, angle, new RGBA[]{shadowColor, shadowColorTransparent, highlightColorTransparent, highlightColor}, new int[]{0, 127, 128, 255}, knockout, onTop, innerShadow, blurX, blurY, strength, passes, document, filtersElement, exporter, in); } @Override diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/types/filters/FILTER.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/types/filters/FILTER.java index 6c692ba34..e2b64b052 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/types/filters/FILTER.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/types/filters/FILTER.java @@ -23,9 +23,12 @@ import com.jpexs.decompiler.flash.types.RGBA; import com.jpexs.decompiler.flash.types.annotations.Internal; import com.jpexs.decompiler.flash.types.annotations.SWFType; import com.jpexs.helpers.ConcreteClasses; +import com.jpexs.helpers.GradientUtil; import com.jpexs.helpers.SerializableImage; +import java.awt.Color; import java.io.Serializable; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import org.w3c.dom.Document; import org.w3c.dom.Element; @@ -132,11 +135,69 @@ public abstract class FILTER implements Serializable { Element filtersElement, SVGExporter exporter, String in + ) { + return dropShadowSvg( + distance, + angle, + new RGBA[]{dropShadowColor}, + new int[0], + innerShadow, + knockout, + compositeSource, + blurX, + blurY, + strength, + iterations, + document, + filtersElement, + exporter, + in + ); + } + + /** + * Converts drop shadow to SVG. + * + * @param distance Distance + * @param angle Angle + * @param dropShadowColor Drop shadow color + * @param innerShadow Inner shadow + * @param knockout Knockout + * @param compositeSource Composite source + * @param blurX Blur X + * @param blurY Blur Y + * @param strength Strength + * @param iterations Iterations + * @param document Document + * @param filtersElement Filters element + * @param exporter SVG exporter + * @param in Input + * @return SVG id of the drop shadow + */ + protected String dropShadowSvg( + double distance, + double angle, + RGBA[] gradientColors, + int[] gradientRatio, + boolean innerShadow, + boolean knockout, + boolean compositeSource, + double blurX, + double blurY, + double strength, + int iterations, + Document document, + Element filtersElement, + SVGExporter exporter, + String in ) { double dx = distance * Math.cos(angle); double dy = distance * Math.sin(angle); + RGBA dropShadowColor = gradientColors.length == 1 ? gradientColors[0] : new RGBA(0, 0, 0, 255); + if (innerShadow) { + Element feFlood = document.createElement("feFlood"); feFlood.setAttribute("flood-color", dropShadowColor.toHexRGB()); feFlood.setAttribute("flood-opacity", "" + dropShadowColor.getAlphaFloat()); @@ -297,12 +358,12 @@ public abstract class FILTER implements Serializable { int orderX = (int) Math.ceil(blurX * exporter.getZoom()); int orderY = (int) Math.ceil(blurY * exporter.getZoom()); - if (orderX == 0) { - orderX = 1; + if (orderX % 2 == 0) { + orderX++; } - if (orderY == 0) { - orderY = 1; + if (orderY % 2 == 0) { + orderY++; } double divisor = orderX * orderY; @@ -315,9 +376,10 @@ public abstract class FILTER implements Serializable { element.setAttribute("divisor", "" + divisor); element.setAttribute("kernelMatrix", String.join(" ", parts)); - element.setAttribute("in", in); + element.setAttribute("kernelUnitLength", "1"); } - + element.setAttribute("in", in); + String result = exporter.getUniqueId("filterResult"); element.setAttribute("result", result); @@ -328,6 +390,7 @@ public abstract class FILTER implements Serializable { /** * Converts gradient ratios to Java format float ratios. + * * @param input 0-255 values * @return 0f - 1f values, strictly increasing */ @@ -368,4 +431,226 @@ public abstract class FILTER implements Serializable { return output; } + + protected String bevelSvg(double distance, double angle, RGBA[] gradientColors, int[] gradientRatio, boolean knockout, boolean onTop, boolean innerShadow, double blurX, double blurY, double strength, int passes, Document document, Element filtersElement, SVGExporter exporter, String in) { + RGBA highlightColor = new RGBA(255, 0, 0, 255); + RGBA shadowColor = new RGBA(0, 0, 255, 255); + + int type = Filtering.INNER; + if (onTop && !innerShadow) { + type = Filtering.FULL; + } else if (!innerShadow) { + type = Filtering.OUTER; + } + + String shadowInner = null; + String hilightInner = null; + if (type != Filtering.OUTER) { + String hilight = dropShadowSvg(distance, angle, highlightColor, true, true, true, 0, 0, strength, passes, document, filtersElement, exporter, in); + String shadow = dropShadowSvg(distance, angle + Math.PI, shadowColor, true, true, true, 0, 0, strength, passes, document, filtersElement, exporter, in); + + Element feComposite1 = document.createElement("feComposite"); + feComposite1.setAttribute("in", hilight); + feComposite1.setAttribute("in2", shadow); + feComposite1.setAttribute("operator", "out"); + hilightInner = exporter.getUniqueId("filterResult"); + feComposite1.setAttribute("result", hilightInner); + filtersElement.appendChild(feComposite1); + + Element feComposite2 = document.createElement("feComposite"); + feComposite2.setAttribute("in", shadow); + feComposite2.setAttribute("in2", hilight); + feComposite2.setAttribute("operator", "out"); + shadowInner = exporter.getUniqueId("filterResult"); + feComposite2.setAttribute("result", shadowInner); + filtersElement.appendChild(feComposite2); + } + + String shadowOuter = null; + String hilightOuter = null; + + if (type != Filtering.INNER) { + String hilight = dropShadowSvg(distance, angle + Math.PI, highlightColor, false, true, true, 0, 0, strength, passes, document, filtersElement, exporter, in); + String shadow = dropShadowSvg(distance, angle, shadowColor, false, true, true, 0, 0, strength, passes, document, filtersElement, exporter, in); + + Element feComposite1 = document.createElement("feComposite"); + feComposite1.setAttribute("in", hilight); + feComposite1.setAttribute("in2", shadow); + feComposite1.setAttribute("operator", "out"); + hilightOuter = exporter.getUniqueId("filterResult"); + feComposite1.setAttribute("result", hilightOuter); + filtersElement.appendChild(feComposite1); + + Element feComposite2 = document.createElement("feComposite"); + feComposite2.setAttribute("in", shadow); + feComposite2.setAttribute("in2", hilight); + feComposite2.setAttribute("operator", "out"); + shadowOuter = exporter.getUniqueId("filterResult"); + feComposite2.setAttribute("result", shadowOuter); + filtersElement.appendChild(feComposite2); + } + + String hilight = null; + String shadow = null; + + switch (type) { + case Filtering.OUTER: + hilight = hilightOuter; + shadow = shadowOuter; + break; + case Filtering.INNER: + hilight = hilightInner; + shadow = shadowInner; + break; + case Filtering.FULL: + Element feComposite1 = document.createElement("feComposite"); + feComposite1.setAttribute("in", hilightInner); + feComposite1.setAttribute("in2", hilightOuter); + feComposite1.setAttribute("operator", "over"); + hilight = exporter.getUniqueId("filterResult"); + feComposite1.setAttribute("result", hilight); + filtersElement.appendChild(feComposite1); + + Element feComposite2 = document.createElement("feComposite"); + feComposite2.setAttribute("in", shadowInner); + feComposite2.setAttribute("in2", shadowOuter); + feComposite2.setAttribute("operator", "over"); + shadow = exporter.getUniqueId("filterResult"); + feComposite2.setAttribute("result", shadow); + filtersElement.appendChild(feComposite2); + break; + } + + Element feFlood = document.createElement("feFlood"); + feFlood.setAttribute("flood-color", "black"); + feFlood.setAttribute("flood-opacity", "1"); + String black = exporter.getUniqueId("filterResult"); + feFlood.setAttribute("result", black); + filtersElement.appendChild(feFlood); + + String result; + + Element feComposite4 = document.createElement("feComposite"); + feComposite4.setAttribute("in", shadow); + feComposite4.setAttribute("in2", black); + feComposite4.setAttribute("operator", "over"); + result = exporter.getUniqueId("filterResult"); + feComposite4.setAttribute("result", result); + filtersElement.appendChild(feComposite4); + + Element feComposite5 = document.createElement("feComposite"); + feComposite5.setAttribute("in", hilight); + feComposite5.setAttribute("in2", result); + feComposite5.setAttribute("operator", "over"); + result = exporter.getUniqueId("filterResult"); + feComposite5.setAttribute("result", result); + filtersElement.appendChild(feComposite5); + + result = blurSvg(blurX, blurY, passes, document, filtersElement, exporter, result); + + Element feColorMatrix = document.createElement("feColorMatrix"); + feColorMatrix.setAttribute("type", "matrix"); + feColorMatrix.setAttribute("in", result); + double halfStrength = strength / 2; + String matrixRow = "" + halfStrength + " 0 " + (-halfStrength) + " 0 0.5"; + feColorMatrix.setAttribute("values", + matrixRow + " " + + matrixRow + " " + + matrixRow + " " + + matrixRow + ); + result = exporter.getUniqueId("filterResult"); + feColorMatrix.setAttribute("result", result); + filtersElement.appendChild(feColorMatrix); + + result = prepareFeComponentTransfer(gradientColors, gradientRatio, document, filtersElement, exporter, result); + + if (type == Filtering.INNER) { + Element feComposite6 = document.createElement("feComposite"); + feComposite6.setAttribute("in", result); + feComposite6.setAttribute("in2", in); + feComposite6.setAttribute("operator", "in"); + result = exporter.getUniqueId("filterResult"); + feComposite6.setAttribute("result", result); + filtersElement.appendChild(feComposite6); + } + if (type == Filtering.OUTER) { + Element feComposite6 = document.createElement("feComposite"); + feComposite6.setAttribute("in", result); + feComposite6.setAttribute("in2", in); + feComposite6.setAttribute("operator", "out"); + result = exporter.getUniqueId("filterResult"); + feComposite6.setAttribute("result", result); + filtersElement.appendChild(feComposite6); + } + + if (!knockout) { + Element feComposite7 = document.createElement("feComposite"); + feComposite7.setAttribute("in", result); + feComposite7.setAttribute("in2", in); + feComposite7.setAttribute("operator", "over"); + result = exporter.getUniqueId("filterResult"); + feComposite7.setAttribute("result", result); + filtersElement.appendChild(feComposite7); + } + return result; + } + + private String prepareFeComponentTransfer(RGBA[] gradientColors, int[] gradientRatio, Document document, Element filtersElement, SVGExporter exporter, String in) { + Element feComponentTransfer = document.createElement("feComponentTransfer"); + feComponentTransfer.setAttribute("in", in); + + List redValues = new ArrayList<>(); + List greenValues = new ArrayList<>(); + List blueValues = new ArrayList<>(); + List alphaValues = new ArrayList<>(); + + for (int i = 0; i < 256; i++) { + RGBA color = GradientUtil.colorAt(gradientColors, gradientRatio, i, GradientUtil.ColorInterpolation.SRGB); + redValues.add("" + (color.red / 255f)); + greenValues.add("" + (color.green / 255f)); + blueValues.add("" + (color.blue / 255f)); + alphaValues.add("" + color.getAlphaFloat()); + } + + /* + //In case we want to map 128 to center + + for (int i = 0; i < 126; i++) { //126 colors + RGBA color = GradientUtil.colorAt(gradientColors, gradientRatio, i * 127f / 125f, GradientUtil.ColorInterpolation.SRGB); + redValues.add("" + (color.red / 255f)); + greenValues.add("" + (color.green / 255f)); + blueValues.add("" + (color.blue / 255f)); + alphaValues.add("" + color.getAlphaFloat()); + } + for (int i = 128; i < 256; i++) { //1 center + 126 colors + RGBA color = GradientUtil.colorAt(gradientColors, gradientRatio, i, GradientUtil.ColorInterpolation.SRGB); + redValues.add("" + (color.red / 255f)); + greenValues.add("" + (color.green / 255f)); + blueValues.add("" + (color.blue / 255f)); + alphaValues.add("" + color.getAlphaFloat()); + } + */ + Element feFuncR = document.createElement("feFuncR"); + feFuncR.setAttribute("type", "table"); + feFuncR.setAttribute("tableValues", String.join(" ", redValues)); + Element feFuncG = document.createElement("feFuncG"); + feFuncG.setAttribute("type", "table"); + feFuncG.setAttribute("tableValues", String.join(" ", greenValues)); + Element feFuncB = document.createElement("feFuncB"); + feFuncB.setAttribute("type", "table"); + feFuncB.setAttribute("tableValues", String.join(" ", blueValues)); + Element feFuncA = document.createElement("feFuncA"); + feFuncA.setAttribute("type", "table"); + feFuncA.setAttribute("tableValues", String.join(" ", alphaValues)); + feComponentTransfer.appendChild(feFuncR); + feComponentTransfer.appendChild(feFuncG); + feComponentTransfer.appendChild(feFuncB); + feComponentTransfer.appendChild(feFuncA); + + String result = exporter.getUniqueId("filterResult"); + feComponentTransfer.setAttribute("result", result); + filtersElement.appendChild(feComponentTransfer); + return result; + } } diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/types/filters/GRADIENTBEVELFILTER.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/types/filters/GRADIENTBEVELFILTER.java index 6adf7398f..c63afe857 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/types/filters/GRADIENTBEVELFILTER.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/types/filters/GRADIENTBEVELFILTER.java @@ -120,7 +120,7 @@ public class GRADIENTBEVELFILTER extends FILTER { colorsArr[i] = gradientColors[i].toColor(); } float[] ratiosArr = convertRatiosToJavaGradient(gradientRatio); - + int type = Filtering.INNER; if (onTop && !innerShadow) { type = Filtering.FULL; @@ -143,7 +143,7 @@ public class GRADIENTBEVELFILTER extends FILTER { @Override public String toSvg(Document document, Element filtersElement, SVGExporter exporter, String in) { - return null; //NOT SUPPORTED + return bevelSvg(distance, angle, gradientColors, gradientRatio, knockout, onTop, innerShadow, blurX, blurY, strength, passes, document, filtersElement, exporter, in); } @Override @@ -211,6 +211,5 @@ public class GRADIENTBEVELFILTER extends FILTER { } return Arrays.equals(this.gradientRatio, other.gradientRatio); } - - + } diff --git a/libsrc/ffdec_lib/src/com/jpexs/helpers/GradientUtil.java b/libsrc/ffdec_lib/src/com/jpexs/helpers/GradientUtil.java new file mode 100644 index 000000000..a219379a8 --- /dev/null +++ b/libsrc/ffdec_lib/src/com/jpexs/helpers/GradientUtil.java @@ -0,0 +1,288 @@ +/* + * Copyright (C) 2010-2026 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.helpers; + + +import com.jpexs.decompiler.flash.types.RGBA; +import java.util.ArrayList; +import java.util.List; + +public final class GradientUtil { + + private GradientUtil() { + } + + /** + * Matches SVG's color-interpolation attribute. + */ + public enum ColorInterpolation { + /** + * Interpolate directly in sRGB-encoded component values (0..255). + */ + SRGB, + /** + * Convert sRGB -> linear RGB, interpolate in linear space, convert back + * to sRGB. + */ + LINEAR_RGB + } + + /** + * Returns the gradient color at position k (0..255), where "ratio" defines + * stop positions (0..255) and "colors" defines stop colors. The stop lookup + * is identical for both modes; only interpolation differs. + * + * Assumptions: - ratio[] is sorted ascending (0..255). - colors.length == + * ratio.length. + */ + public static RGBA colorAt(RGBA[] colors, int[] ratio, float k, ColorInterpolation mode) { + if (colors == null || ratio == null || colors.length == 0 || colors.length != ratio.length) { + throw new IllegalArgumentException("colors and ratio must be non-empty and have the same length."); + } + + // Clamp query position into [0, 255] + k = clamp(k, 0f, 255f); + + // Handle outside range quickly + int n = ratio.length; + if (k <= ratio[0]) { + return colors[0]; + } + if (k >= ratio[n - 1]) { + return colors[n - 1]; + } + + // Find the segment [i, i+1] such that ratio[i] <= k <= ratio[i+1] + int i = 0; + while (i < n - 1 && k > ratio[i + 1]) { + i++; + } + + int p0 = ratio[i]; + int p1 = ratio[i + 1]; + RGBA c0 = colors[i]; + RGBA c1 = colors[i + 1]; + + // Degenerate case: two stops at the same position + if (p1 == p0) { + return c1; + } + + float t = (k - p0) / (float) (p1 - p0); // 0..1 + + // Interpolate alpha linearly in all modes + int a = lerp8(c0.alpha, c1.alpha, t); + + if (mode == ColorInterpolation.SRGB) { + // SVG color-interpolation="sRGB": interpolate directly in sRGB component values + int r = lerp8(c0.red, c1.red, t); + int g = lerp8(c0.green, c1.green, t); + int b = lerp8(c0.blue, c1.blue, t); + return new RGBA(r, g, b, a); + } else { + // SVG color-interpolation="linearRGB": gamma-correct interpolation + float r = lerpLinearRgbChannel(c0.red, c1.red, t); + float g = lerpLinearRgbChannel(c0.green, c1.green, t); + float b = lerpLinearRgbChannel(c0.blue, c1.blue, t); + + return new RGBA( + clamp255(Math.round(r * 255f)), + clamp255(Math.round(g * 255f)), + clamp255(Math.round(b * 255f)), + a + ); + } + } + + public static final class SplitResult { + public final RGBA[] colorsA; + public final int[] ratioA; + public final RGBA[] colorsB; + public final int[] ratioB; + + public SplitResult(RGBA[] colorsA, int[] ratioA, RGBA[] colorsB, int[] ratioB) { + this.colorsA = colorsA; + this.ratioA = ratioA; + this.colorsB = colorsB; + this.ratioB = ratioB; + } + } + + /** + * Splits a gradient into two halves: + * - A: original positions 0..127 mapped to 0..255 + * - B: original positions 128..255 mapped to 0..255 + * + * Notes: + * - Stop lookup is based on original ratio[]. + * - Boundary stops at 127 (A) and 128 (B) are ensured (interpolated if missing). + * - ratio[] is assumed sorted ascending. + */ + public static SplitResult splitIntoHalves( + RGBA[] colors, int[] ratio, ColorInterpolation mode + ) { + // Build left half (0..127) + Stops left = extractRange(colors, ratio, 0, 127, mode); + + // Build right half (128..255) + Stops right = extractRange(colors, ratio, 128, 255, mode); + + // Remap ratios to 0..255 in each half + int[] ratioA = remap(left.ratio, 0, 127); + int[] ratioB = remap(right.ratio, 128, 255); + + return new SplitResult( + left.colors.toArray(new RGBA[0]), ratioA, + right.colors.toArray(new RGBA[0]), ratioB + ); + } + + // ----- Internal representation of stops ----- + + private static final class Stops { + final List colors = new ArrayList<>(); + final List ratio = new ArrayList<>(); + } + + /** + * Extracts all stops within [from..to] (inclusive), and ensures stops at both ends exist. + * If an endpoint stop is missing, it is computed via SvgGradient.colorAt(...). + */ + private static Stops extractRange(RGBA[] colors, int[] ratio, int from, int to, + ColorInterpolation mode) { + Stops out = new Stops(); + + // Ensure start stop + addStop(out, from, stopColorAtOrExisting(colors, ratio, from, mode)); + + // Add internal stops strictly inside (from, to) + for (int i = 0; i < ratio.length; i++) { + int p = ratio[i]; + if (p > from && p < to) { + addStop(out, p, colors[i]); + } + } + + // Ensure end stop + addStop(out, to, stopColorAtOrExisting(colors, ratio, to, mode)); + + return out; + } + + /** + * If there is an existing stop exactly at pos, returns its color; + * otherwise computes the color by interpolation at that position. + */ + private static RGBA stopColorAtOrExisting(RGBA[] colors, int[] ratio, int pos, + ColorInterpolation mode) { + for (int i = 0; i < ratio.length; i++) { + if (ratio[i] == pos) return colors[i]; + } + return colorAt(colors, ratio, pos, mode); + } + + /** + * Adds a stop keeping order; if the same position already exists, it overwrites the color. + */ + private static void addStop(Stops stops, int pos, RGBA color) { + // Insert in ascending order (ratio is small, linear insert is fine) + for (int i = 0; i < stops.ratio.size(); i++) { + int existing = stops.ratio.get(i); + if (existing == pos) { + stops.colors.set(i, color); + return; + } + if (existing > pos) { + stops.ratio.add(i, pos); + stops.colors.add(i, color); + return; + } + } + stops.ratio.add(pos); + stops.colors.add(color); + } + + /** + * Remaps original integer positions in [from..to] to [0..255]. + * Uses rounding and clamps, guaranteeing endpoints map to 0 and 255. + */ + private static int[] remap(List original, int from, int to) { + int span = to - from; // for 0..127 and 128..255 span is 127 + int[] out = new int[original.size()]; + + for (int i = 0; i < original.size(); i++) { + int p = original.get(i); + float t = (p - from) / (float) span; // 0..1 + int mapped = Math.round(t * 255f); // 0..255 + out[i] = clamp255(mapped); + } + + // Force exact endpoints (avoid any rounding surprises) + if (out.length > 0) { + out[0] = 0; + out[out.length - 1] = 255; + } + return out; + } + + // ----- Interpolation helpers ----- + private static int lerp8(int v0, int v1, float t) { + return clamp255(Math.round(v0 + (v1 - v0) * t)); + } + + /** + * Interpolates a single channel using linearRGB mode: sRGB (0..1) -> linear + * (0..1) -> lerp -> sRGB (0..1) + */ + private static float lerpLinearRgbChannel(int c0_8bit, int c1_8bit, float t) { + float s0 = c0_8bit / 255f; + float s1 = c1_8bit / 255f; + + float l0 = srgbToLinear(s0); + float l1 = srgbToLinear(s1); + + float l = l0 + (l1 - l0) * t; + + return linearToSrgb(l); + } + + // ----- sRGB transfer functions ----- + private static float srgbToLinear(float c) { + // IEC 61966-2-1 sRGB + if (c <= 0.04045f) { + return c / 12.92f; + } + return (float) Math.pow((c + 0.055f) / 1.055f, 2.4); + } + + private static float linearToSrgb(float c) { + // IEC 61966-2-1 sRGB + if (c <= 0.0031308f) { + return 12.92f * c; + } + return 1.055f * (float) Math.pow(c, 1.0 / 2.4) - 0.055f; + } + + // ----- Clamp helpers ----- + private static float clamp(float v, float lo, float hi) { + return Math.max(lo, Math.min(hi, v)); + } + + private static int clamp255(int v) { + return Math.max(0, Math.min(255, v)); + } +} diff --git a/libsrc/ffdec_lib/testdata/graphics/graphics.swf b/libsrc/ffdec_lib/testdata/graphics/graphics.swf index e099e96b7..95c1e79bf 100644 Binary files a/libsrc/ffdec_lib/testdata/graphics/graphics.swf and b/libsrc/ffdec_lib/testdata/graphics/graphics.swf differ diff --git a/libsrc/ffdec_lib/testdata/graphics/graphics/DOMDocument.xml b/libsrc/ffdec_lib/testdata/graphics/graphics/DOMDocument.xml index 7433d1ce7..d38e0db1d 100644 --- a/libsrc/ffdec_lib/testdata/graphics/graphics/DOMDocument.xml +++ b/libsrc/ffdec_lib/testdata/graphics/graphics/DOMDocument.xml @@ -1,13 +1,13 @@ - + - + - + - + @@ -36,7 +36,7 @@ - + @@ -865,10 +865,10 @@ stop();]]> !3980 1990|2980 1990!2980 1990|2980 990!2980 990|3980 990!3980 990|3980 1990"/> - + @@ -1467,18 +1467,18 @@ stop();]]> - + - - + + @@ -2020,7 +2020,7 @@ stop();]]> - + @@ -2028,10 +2028,10 @@ stop();]]> - + - - + + @@ -3335,11 +3335,11 @@ stop();]]> - + - + - + @@ -3383,7 +3383,7 @@ stop();]]> - + @@ -3400,9 +3400,9 @@ stop();]]> - + - + @@ -3417,9 +3417,9 @@ stop();]]> - + - + @@ -3434,9 +3434,9 @@ stop();]]> - + - + @@ -3451,9 +3451,9 @@ stop();]]> - + - + @@ -3468,9 +3468,9 @@ stop();]]> - + - + @@ -3485,9 +3485,9 @@ stop();]]> - + - + @@ -3502,9 +3502,9 @@ stop();]]> - + - + @@ -3519,7 +3519,7 @@ stop();]]> - + @@ -3536,9 +3536,9 @@ stop();]]> - + - + @@ -3553,7 +3553,7 @@ stop();]]> - + @@ -3570,7 +3570,7 @@ stop();]]> - + @@ -3587,7 +3587,7 @@ stop();]]> - + @@ -3604,9 +3604,9 @@ stop();]]> - + - + @@ -3621,9 +3621,9 @@ stop();]]> - + - + @@ -3638,9 +3638,9 @@ stop();]]> - + - + @@ -3655,9 +3655,9 @@ stop();]]> - + - + @@ -3672,9 +3672,9 @@ stop();]]> - + - + @@ -3689,7 +3689,7 @@ stop();]]> - + @@ -3706,9 +3706,9 @@ stop();]]> - + - + @@ -3723,9 +3723,9 @@ stop();]]> - + - + @@ -3740,9 +3740,9 @@ stop();]]> - + - + @@ -3757,9 +3757,9 @@ stop();]]> - + - + @@ -3774,9 +3774,9 @@ stop();]]> - + - + @@ -3791,9 +3791,9 @@ stop();]]> - + - + @@ -3808,9 +3808,9 @@ stop();]]> - + - + @@ -3825,9 +3825,9 @@ stop();]]> - + - + @@ -3842,7 +3842,7 @@ stop();]]> - + @@ -3859,7 +3859,7 @@ stop();]]> - + @@ -3876,9 +3876,9 @@ stop();]]> - + - + @@ -3893,9 +3893,9 @@ stop();]]> - + - + @@ -3910,9 +3910,9 @@ stop();]]> - + - + @@ -3927,7 +3927,7 @@ stop();]]> - + @@ -3944,7 +3944,7 @@ stop();]]> - + @@ -3961,9 +3961,9 @@ stop();]]> - + - + @@ -3978,7 +3978,7 @@ stop();]]> - + @@ -3995,7 +3995,7 @@ stop();]]> - + @@ -4012,7 +4012,7 @@ stop();]]> - + @@ -4029,9 +4029,9 @@ stop();]]> - + - + @@ -4046,7 +4046,7 @@ stop();]]> - + @@ -4063,9 +4063,9 @@ stop();]]> - + - + @@ -4080,9 +4080,9 @@ stop();]]> - + - + @@ -4097,7 +4097,7 @@ stop();]]> - + @@ -4114,7 +4114,7 @@ stop();]]> - + @@ -4122,7 +4122,7 @@ stop();]]> - 062-065 radial/focal tween + 062-065 Radial/focal tween @@ -4131,9 +4131,9 @@ stop();]]> - + - + @@ -4148,7 +4148,7 @@ stop();]]> - + @@ -4172,6 +4172,13 @@ stop();]]> + + + + + + + @@ -4185,12 +4192,5 @@ stop();]]> - - - - - - - \ No newline at end of file diff --git a/libsrc/ffdec_lib/testdata/graphics/graphics/LIBRARY/ScaledRect.xml b/libsrc/ffdec_lib/testdata/graphics/graphics/LIBRARY/ScaledRect.xml index ed64c39a3..36525ab61 100644 --- a/libsrc/ffdec_lib/testdata/graphics/graphics/LIBRARY/ScaledRect.xml +++ b/libsrc/ffdec_lib/testdata/graphics/graphics/LIBRARY/ScaledRect.xml @@ -28,81 +28,81 @@ - - - - - - - - - - - - - - - - + + + + + + + - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libsrc/ffdec_lib/testdata/graphics/graphics/LIBRARY/Sprite1.xml b/libsrc/ffdec_lib/testdata/graphics/graphics/LIBRARY/Sprite1.xml index b8b5cd635..bfbd08dee 100644 --- a/libsrc/ffdec_lib/testdata/graphics/graphics/LIBRARY/Sprite1.xml +++ b/libsrc/ffdec_lib/testdata/graphics/graphics/LIBRARY/Sprite1.xml @@ -25,8 +25,8 @@ - + diff --git a/libsrc/ffdec_lib/testdata/graphics/graphics/LIBRARY/grid.xml b/libsrc/ffdec_lib/testdata/graphics/graphics/LIBRARY/grid.xml index 345298121..08d5000c8 100644 --- a/libsrc/ffdec_lib/testdata/graphics/graphics/LIBRARY/grid.xml +++ b/libsrc/ffdec_lib/testdata/graphics/graphics/LIBRARY/grid.xml @@ -28,98 +28,76 @@ - - - - - - + + + + + + + + + - - - - - - - - - - - - - - - - - - - + - + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libsrc/ffdec_lib/testdata/graphics/graphics/META-INF/metadata.xml b/libsrc/ffdec_lib/testdata/graphics/graphics/META-INF/metadata.xml index 10ccc24e9..87cfd8c88 100644 --- a/libsrc/ffdec_lib/testdata/graphics/graphics/META-INF/metadata.xml +++ b/libsrc/ffdec_lib/testdata/graphics/graphics/META-INF/metadata.xml @@ -5,8 +5,8 @@ xmlns:xmp="http://ns.adobe.com/xap/1.0/"> Adobe Flash Professional CS6 - build 481 2021-03-14T08:29:20+01:00 - 2024-10-26T08:31:23-07:00 - 2024-10-26T08:31:23-07:00 + 2026-03-02T13:36:50-08:00 + 2026-03-02T13:36:50-08:00 @@ -15,7 +15,7 @@ - xmp.iid:740FBFB1A593EF119A09BEAA35197FE1 + xmp.iid:97E446EC7F16F1119B53F77A0E0EAFD1 xmp.did:D6D3FE199784EB1187FEAE6972EC5115 xmp.did:D6D3FE199784EB1187FEAE6972EC5115 @@ -188,6 +188,12 @@ 2021-03-14T08:29:20+01:00 Adobe Flash Professional CS6 - build 481 + + created + xmp.iid:97E446EC7F16F1119B53F77A0E0EAFD1 + 2021-03-14T08:29:20+01:00 + Adobe Flash Professional CS6 - build 481 + diff --git a/libsrc/ffdec_lib/testdata/graphics/graphics/bin/SymDepend.cache b/libsrc/ffdec_lib/testdata/graphics/graphics/bin/SymDepend.cache index 9940c190b..ff8795d6d 100644 Binary files a/libsrc/ffdec_lib/testdata/graphics/graphics/bin/SymDepend.cache and b/libsrc/ffdec_lib/testdata/graphics/graphics/bin/SymDepend.cache differ