diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4db05adec..4182c98b0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -67,16 +67,8 @@ jobs: architecture: ${{ matrix.arch }} - name: Setup Ant uses: cedx/setup-ant@v3 - - name: Install Certificates for Code Signing - if: ${{ matrix.os_prefix == 'macos' }} - uses: apple-actions/import-codesign-certs@v3 - with: - p12-file-base64: ${{ secrets.CERTIFICATES_P12 }} - p12-password: ${{ secrets.CERTIFICATES_P12_PASSWORD }} - name: Build Release run: ant -noinput -buildfile build/build.xml ${{ matrix.os_prefix }}-dist -Dversion="${{ github.sha }}" - env: - PROCESSING_APP_SIGNING: true - name: Add artifact uses: actions/upload-artifact@v4 with: diff --git a/app/build.xml b/app/build.xml index 5ca75a877..0b91c0a14 100644 --- a/app/build.xml +++ b/app/build.xml @@ -4,14 +4,14 @@ - + + value="https://dlcdn.apache.org//ant/binaries/${ant.zip}" /> diff --git a/build/shared/lib/languages/PDE.properties b/build/shared/lib/languages/PDE.properties index fb2042f22..66fcf1729 100644 --- a/build/shared/lib/languages/PDE.properties +++ b/build/shared/lib/languages/PDE.properties @@ -25,6 +25,7 @@ menu.file.close = Close menu.file.save = Save menu.file.save_as = Save As... menu.file.export_application = Export Application... +menu.file.export_pdez = Export as PDEZ... menu.file.page_setup = Page Setup menu.file.print = Print... menu.file.preferences = Preferences... diff --git a/core/src/processing/awt/PShapeJava2D.java b/core/src/processing/awt/PShapeJava2D.java index 2b9b968a4..4238ef924 100644 --- a/core/src/processing/awt/PShapeJava2D.java +++ b/core/src/processing/awt/PShapeJava2D.java @@ -33,6 +33,9 @@ import java.awt.image.ColorModel; import java.awt.image.Raster; import java.awt.image.WritableRaster; +import java.util.Arrays; +import java.awt.Color; + import processing.core.PApplet; import processing.core.PGraphics; import processing.core.PShapeSVG; @@ -96,16 +99,29 @@ public class PShapeJava2D extends PShapeSVG { */ - static class LinearGradientPaint implements Paint { + public static class LinearGradientPaint implements Paint { float x1, y1, x2, y2; float[] offset; int[] color; + Color[] colors; int count; float opacity; + AffineTransform xform; + + public static enum CycleMethod { + NO_CYCLE, + REFLECT, + REPEAT + } + + public static enum ColorSpaceType { + SRGB, + LINEAR_RGB + } public LinearGradientPaint(float x1, float y1, float x2, float y2, float[] offset, int[] color, int count, - float opacity) { + float opacity, AffineTransform xform) { this.x1 = x1; this.y1 = y1; this.x2 = x2; @@ -114,6 +130,13 @@ public class PShapeJava2D extends PShapeSVG { this.color = color; this.count = count; this.opacity = opacity; + this.xform = xform; + + //set an array of type Color + this.colors = new Color[this.color.length]; + for (int i = 0; i < this.color.length; i++) { + this.colors[i] = new Color(this.color[i], true); + } } public PaintContext createContext(ColorModel cm, @@ -125,6 +148,35 @@ public class PShapeJava2D extends PShapeSVG { (float) t2.getX(), (float) t2.getY()); } + public Point2D getStartPoint() { + return new Point2D.Float(this.x1, this.y1); + } + + public Point2D getEndPoint() { + return new Point2D.Float(this.x2, this.y2); + } + + /* MultipleGradientPaint methods... */ + public AffineTransform getTransform() { + return this.xform; + } + + public ColorSpaceType getColorSpace() { + return ColorSpaceType.SRGB; + } + + public CycleMethod getCycleMethod() { + return CycleMethod.NO_CYCLE; + } + + public Color[] getColors() { + return Arrays.copyOf(this.colors, this.colors.length); + } + + public float[] getFractions() { + return Arrays.copyOf(this.offset, this.offset.length); + } + public int getTransparency() { return TRANSLUCENT; // why not.. rather than checking each color } @@ -221,16 +273,29 @@ public class PShapeJava2D extends PShapeSVG { } - static class RadialGradientPaint implements Paint { + public static class RadialGradientPaint implements Paint { float cx, cy, radius; float[] offset; int[] color; + Color[] colors; int count; float opacity; + AffineTransform xform; + + public static enum CycleMethod { + NO_CYCLE, + REFLECT, + REPEAT + } + + public static enum ColorSpaceType { + SRGB, + LINEAR_RGB + } public RadialGradientPaint(float cx, float cy, float radius, float[] offset, int[] color, int count, - float opacity) { + float opacity, AffineTransform xform) { this.cx = cx; this.cy = cy; this.radius = radius; @@ -238,6 +303,13 @@ public class PShapeJava2D extends PShapeSVG { this.color = color; this.count = count; this.opacity = opacity; + this.xform = xform; + + //set an array of type Color + this.colors = new Color[this.color.length]; + for (int i = 0; i < this.color.length; i++) { + this.colors[i] = new Color(this.color[i], true); + } } public PaintContext createContext(ColorModel cm, @@ -246,6 +318,41 @@ public class PShapeJava2D extends PShapeSVG { return new RadialGradientContext(); } + public Point2D getCenterPoint() { + return new Point2D.Double(this.cx, this.cy); + } + + //TODO: investigate how to change a focus point for 0% x of the gradient + //for now default to center x/y + public Point2D getFocusPoint() { + return new Point2D.Double(this.cx, this.cy); + } + + public float getRadius() { + return this.radius; + } + + /* MultipleGradientPaint methods... */ + public AffineTransform getTransform() { + return this.xform; + } + + public ColorSpaceType getColorSpace() { + return ColorSpaceType.SRGB; + } + + public CycleMethod getCycleMethod() { + return CycleMethod.NO_CYCLE; + } + + public Color[] getColors() { + return Arrays.copyOf(this.colors, this.colors.length); + } + + public float[] getFractions() { + return Arrays.copyOf(this.offset, this.offset.length); + } + public int getTransparency() { return TRANSLUCENT; } @@ -305,14 +412,14 @@ public class PShapeJava2D extends PShapeSVG { LinearGradient grad = (LinearGradient) gradient; return new LinearGradientPaint(grad.x1, grad.y1, grad.x2, grad.y2, grad.offset, grad.color, grad.count, - opacity); + opacity, grad.transform); } else if (gradient instanceof RadialGradient) { // System.out.println("creating radial gradient"); RadialGradient grad = (RadialGradient) gradient; return new RadialGradientPaint(grad.cx, grad.cy, grad.r, grad.offset, grad.color, grad.count, - opacity); + opacity, grad.transform); } return null; } diff --git a/core/src/processing/awt/PSurfaceAWT.java b/core/src/processing/awt/PSurfaceAWT.java index 06337ba38..57649d6ba 100644 --- a/core/src/processing/awt/PSurfaceAWT.java +++ b/core/src/processing/awt/PSurfaceAWT.java @@ -33,8 +33,7 @@ import java.net.URL; import java.util.ArrayList; import java.util.List; -import javax.swing.JFrame; -import javax.swing.SwingUtilities; +import javax.swing.*; import processing.core.PApplet; import processing.core.PConstants; @@ -227,7 +226,7 @@ public class PSurfaceAWT extends PSurfaceNone { } - synchronized protected void render() { + protected void render() { if (canvas.isDisplayable() && graphics.image != null) { if (canvas.getBufferStrategy() == null) { @@ -1021,6 +1020,9 @@ public class PSurfaceAWT extends PSurfaceNone { //sketch.postWindowMoved(x - currentInsets.left, y - currentInsets.top); sketch.postWindowMoved(x, y); // presumably user wants drawing area } + }else{ + // Solves #862 - Grey/White bar on right side of sketches + setFrameSize(); } } diff --git a/core/src/processing/core/PApplet.java b/core/src/processing/core/PApplet.java index ec16e4291..24c0d5d39 100644 --- a/core/src/processing/core/PApplet.java +++ b/core/src/processing/core/PApplet.java @@ -9663,7 +9663,7 @@ public class PApplet implements PConstants { // WINDOW METHODS - Map windowEventQueue = new ConcurrentHashMap<>(); + Map windowEventQueue = new ConcurrentHashMap<>(); public void windowTitle(String title) { @@ -9683,8 +9683,7 @@ public class PApplet implements PConstants { * only the notification that the resize has happened. */ public void postWindowResized(int newWidth, int newHeight) { - windowEventQueue.put("w", newWidth); - windowEventQueue.put("h", newHeight); + windowEventQueue.put("wh", new WindowEventValuePairs(newWidth, newHeight)); } @@ -9724,8 +9723,7 @@ public class PApplet implements PConstants { frameMoved(newX, newY); } - windowEventQueue.put("x", newX); - windowEventQueue.put("y", newY); + windowEventQueue.put("xy", new WindowEventValuePairs(newX, newY)); } @@ -9734,21 +9732,28 @@ public class PApplet implements PConstants { private void dequeueWindowEvents() { - if (windowEventQueue.containsKey("x")) { - windowX = windowEventQueue.remove("x"); - windowY = windowEventQueue.remove("y"); + if (windowEventQueue.containsKey("xy")) { + WindowEventValuePairs xy = windowEventQueue.remove("xy"); + windowX = xy.num1; + windowY = xy.num2; windowMoved(); } - if (windowEventQueue.containsKey("w")) { - // these should already match width/height - //windowResized(windowEventQueue.remove("w"), - // windowEventQueue.remove("h")); - windowEventQueue.remove("w"); - windowEventQueue.remove("h"); + if (windowEventQueue.containsKey("wh")) { + WindowEventValuePairs wh = windowEventQueue.remove("wh"); windowResized(); } } + protected class WindowEventValuePairs { + + public int num1; + public int num2; + + public WindowEventValuePairs(int num1, int num2) { + this.num1 = num1; + this.num2 = num2; + } + } /** * Scale the sketch as if it fits this specific width and height. diff --git a/core/src/processing/core/PShapeSVG.java b/core/src/processing/core/PShapeSVG.java index a51e94276..f8aa3400f 100644 --- a/core/src/processing/core/PShapeSVG.java +++ b/core/src/processing/core/PShapeSVG.java @@ -1397,7 +1397,8 @@ public class PShapeSVG extends PShape { } - void setColor(String colorText, boolean isFill) { + //making this public allows us to set gradient fills on a PShape + public void setColor(String colorText, boolean isFill) { colorText = colorText.trim(); int opacityMask = fillColor & 0xFF000000; boolean visible = true; @@ -1620,7 +1621,7 @@ public class PShapeSVG extends PShape { static public class Gradient extends PShapeSVG { - AffineTransform transform; + public AffineTransform transform; public float[] offset; public int[] color; diff --git a/java/libraries/svg/build.xml b/java/libraries/svg/build.xml index cf82369e8..de0b9e41a 100644 --- a/java/libraries/svg/build.xml +++ b/java/libraries/svg/build.xml @@ -3,7 +3,7 @@ - + @@ -13,7 +13,7 @@ + value="https://dlcdn.apache.org//xmlgraphics/batik/binaries/${batik.zip}" /> diff --git a/java/libraries/svg/src/processing/svg/GradientExtensionHandler.java b/java/libraries/svg/src/processing/svg/GradientExtensionHandler.java new file mode 100644 index 000000000..370adbea9 --- /dev/null +++ b/java/libraries/svg/src/processing/svg/GradientExtensionHandler.java @@ -0,0 +1,209 @@ +package processing.svg; + +import static org.apache.batik.util.SVGConstants.*; + +import processing.awt.PShapeJava2D.LinearGradientPaint; +import processing.awt.PShapeJava2D.RadialGradientPaint; + +import java.awt.Color; +import java.awt.MultipleGradientPaint; +import java.awt.Paint; +import java.awt.geom.AffineTransform; +import java.awt.geom.Point2D; + +import java.util.Objects; + +import org.apache.batik.svggen.DefaultExtensionHandler; +import org.apache.batik.svggen.SVGColor; +import org.apache.batik.svggen.SVGGeneratorContext; +import org.apache.batik.svggen.SVGPaintDescriptor; +import org.w3c.dom.Element; + +/** + * Extension of Batik's {@link DefaultExtensionHandler} which handles different kinds of Paint objects + * based on the extenstion by Martin Steiger https://gist.github.com/msteiger/4509119 + * modified to work with Processing's SVG export library, by Benjamin Fox https://github.com/tracerstar + */ +public class GradientExtensionHandler extends DefaultExtensionHandler { + + @Override + public SVGPaintDescriptor handlePaint(Paint paint, SVGGeneratorContext genCtx) { + + // Handle LinearGradientPaint + if (paint instanceof LinearGradientPaint) { + return getLgpDescriptor((LinearGradientPaint) paint, genCtx); + } + + // Handle RadialGradientPaint + if (paint instanceof RadialGradientPaint) { + return getRgpDescriptor((RadialGradientPaint) paint, genCtx); + } + + return super.handlePaint(paint, genCtx); + } + + private SVGPaintDescriptor getLgpDescriptor(LinearGradientPaint gradient, SVGGeneratorContext genCtx) { + Element gradElem = genCtx.getDOMFactory().createElementNS(SVG_NAMESPACE_URI, SVG_LINEAR_GRADIENT_TAG); + + // Create and set unique XML id + String id = genCtx.getIDGenerator().generateID("gradient"); + gradElem.setAttribute(SVG_ID_ATTRIBUTE, id); + + // Set x,y pairs + Point2D startPt = gradient.getStartPoint(); + gradElem.setAttribute("x1", String.valueOf(startPt.getX())); + gradElem.setAttribute("y1", String.valueOf(startPt.getY())); + + Point2D endPt = gradient.getEndPoint(); + gradElem.setAttribute("x2", String.valueOf(endPt.getX())); + gradElem.setAttribute("y2", String.valueOf(endPt.getY())); + + //TODO: change this to be: addMgpAttributes after refactoring the paint methods + addLgpAttributes(gradElem, genCtx, gradient); + + return new SVGPaintDescriptor("url(#" + id + ")", SVG_OPAQUE_VALUE, gradElem); + } + + private SVGPaintDescriptor getRgpDescriptor(RadialGradientPaint gradient, SVGGeneratorContext genCtx) { + Element gradElem = genCtx.getDOMFactory().createElementNS(SVG_NAMESPACE_URI, SVG_RADIAL_GRADIENT_TAG); + + // Create and set unique XML id + String id = genCtx.getIDGenerator().generateID("gradient"); + gradElem.setAttribute(SVG_ID_ATTRIBUTE, id); + + // Set x,y pairs + Point2D centerPt = gradient.getCenterPoint(); + gradElem.setAttribute("cx", String.valueOf(centerPt.getX())); + gradElem.setAttribute("cy", String.valueOf(centerPt.getY())); + + Point2D focusPt = gradient.getFocusPoint(); + gradElem.setAttribute("fx", String.valueOf(focusPt.getX())); + gradElem.setAttribute("fy", String.valueOf(focusPt.getY())); + + gradElem.setAttribute("r", String.valueOf(gradient.getRadius())); + + //TODO: change this to be: addMgpAttributes after refactoring the paint methods + addRgpAttributes(gradElem, genCtx, gradient); + + return new SVGPaintDescriptor("url(#" + id + ")", SVG_OPAQUE_VALUE, gradElem); + } + + + /* + Being lazy here to duplicate the methods so we don't have to refactor the two gradient paints + to implement java.awt.MultipleGradientPaint + + TODO: make the effort to refactor them to properly implement java.awt.MultipleGradientPaint + */ + private void addLgpAttributes(Element gradElem, SVGGeneratorContext genCtx, LinearGradientPaint gradient) { + gradElem.setAttribute(SVG_GRADIENT_UNITS_ATTRIBUTE, SVG_USER_SPACE_ON_USE_VALUE); + + // Set cycle method + switch (gradient.getCycleMethod()) { + case REFLECT: + gradElem.setAttribute(SVG_SPREAD_METHOD_ATTRIBUTE, SVG_REFLECT_VALUE); + break; + case REPEAT: + gradElem.setAttribute(SVG_SPREAD_METHOD_ATTRIBUTE, SVG_REPEAT_VALUE); + break; + case NO_CYCLE: + default: + gradElem.setAttribute(SVG_SPREAD_METHOD_ATTRIBUTE, SVG_PAD_VALUE); // this is the default + break; + } + + // Set color space + switch (gradient.getColorSpace()) { + case LINEAR_RGB: + gradElem.setAttribute(SVG_COLOR_INTERPOLATION_ATTRIBUTE, SVG_LINEAR_RGB_VALUE); + break; + case SRGB: + default: + gradElem.setAttribute(SVG_COLOR_INTERPOLATION_ATTRIBUTE, SVG_SRGB_VALUE); + break; + } + + // Set transform matrix if not identity + AffineTransform tf = gradient.getTransform(); + if (!Objects.isNull(tf) && !tf.isIdentity()) { + String matrix = "matrix(" + + tf.getScaleX() + " " + tf.getShearX() + " " + tf.getTranslateX() + " " + + tf.getScaleY() + " " + tf.getShearY() + " " + tf.getTranslateY() + ")"; + gradElem.setAttribute(SVG_TRANSFORM_ATTRIBUTE, matrix); + } + + // Convert gradient stops + Color[] colors = gradient.getColors(); + float[] fracs = gradient.getFractions(); + + for (int i = 0; i < colors.length; i++) { + Element stop = genCtx.getDOMFactory().createElementNS(SVG_NAMESPACE_URI, SVG_STOP_TAG); + SVGPaintDescriptor pd = SVGColor.toSVG(colors[i], genCtx); + + stop.setAttribute(SVG_OFFSET_ATTRIBUTE, (int) (fracs[i] * 100.0f) + "%"); + stop.setAttribute(SVG_STOP_COLOR_ATTRIBUTE, pd.getPaintValue()); + + if (colors[i].getAlpha() != 255) { + stop.setAttribute(SVG_STOP_OPACITY_ATTRIBUTE, pd.getOpacityValue()); + } + + gradElem.appendChild(stop); + } + } + + private void addRgpAttributes(Element gradElem, SVGGeneratorContext genCtx, RadialGradientPaint gradient) { + gradElem.setAttribute(SVG_GRADIENT_UNITS_ATTRIBUTE, SVG_USER_SPACE_ON_USE_VALUE); + + // Set cycle method + switch (gradient.getCycleMethod()) { + case REFLECT: + gradElem.setAttribute(SVG_SPREAD_METHOD_ATTRIBUTE, SVG_REFLECT_VALUE); + break; + case REPEAT: + gradElem.setAttribute(SVG_SPREAD_METHOD_ATTRIBUTE, SVG_REPEAT_VALUE); + break; + case NO_CYCLE: + default: + gradElem.setAttribute(SVG_SPREAD_METHOD_ATTRIBUTE, SVG_PAD_VALUE); // this is the default + break; + } + + // Set color space + switch (gradient.getColorSpace()) { + case LINEAR_RGB: + gradElem.setAttribute(SVG_COLOR_INTERPOLATION_ATTRIBUTE, SVG_LINEAR_RGB_VALUE); + break; + case SRGB: + default: + gradElem.setAttribute(SVG_COLOR_INTERPOLATION_ATTRIBUTE, SVG_SRGB_VALUE); + break; + } + + // Set transform matrix if not identity + AffineTransform tf = gradient.getTransform(); + if (!Objects.isNull(tf) && !tf.isIdentity()) { + String matrix = "matrix(" + + tf.getScaleX() + " " + tf.getShearX() + " " + tf.getTranslateX() + " " + + tf.getScaleY() + " " + tf.getShearY() + " " + tf.getTranslateY() + ")"; + gradElem.setAttribute(SVG_TRANSFORM_ATTRIBUTE, matrix); + } + + // Convert gradient stops + Color[] colors = gradient.getColors(); + float[] fracs = gradient.getFractions(); + + for (int i = 0; i < colors.length; i++) { + Element stop = genCtx.getDOMFactory().createElementNS(SVG_NAMESPACE_URI, SVG_STOP_TAG); + SVGPaintDescriptor pd = SVGColor.toSVG(colors[i], genCtx); + + stop.setAttribute(SVG_OFFSET_ATTRIBUTE, (int) (fracs[i] * 100.0f) + "%"); + stop.setAttribute(SVG_STOP_COLOR_ATTRIBUTE, pd.getPaintValue()); + + if (colors[i].getAlpha() != 255) { + stop.setAttribute(SVG_STOP_OPACITY_ATTRIBUTE, pd.getOpacityValue()); + } + + gradElem.appendChild(stop); + } + } +} diff --git a/java/libraries/svg/src/processing/svg/PGraphicsSVG.java b/java/libraries/svg/src/processing/svg/PGraphicsSVG.java index 3b170353b..857f5d93a 100644 --- a/java/libraries/svg/src/processing/svg/PGraphicsSVG.java +++ b/java/libraries/svg/src/processing/svg/PGraphicsSVG.java @@ -87,6 +87,10 @@ public class PGraphicsSVG extends PGraphicsJava2D { g2 = new SVGGraphics2D(document); ((SVGGraphics2D) g2).setSVGCanvasSize(new Dimension(width, height)); + //set the extension handler to allow linear and radial gradients to be exported as svg + GradientExtensionHandler gradH = new GradientExtensionHandler(); + ((SVGGraphics2D) g2).setExtensionHandler(gradH); + // Done with our work, let's check on defaults and the rest //super.beginDraw(); // Can't call super.beginDraw() because it'll nuke our g2 diff --git a/java/src/processing/mode/java/JavaEditor.java b/java/src/processing/mode/java/JavaEditor.java index 9c4bb8435..815792955 100644 --- a/java/src/processing/mode/java/JavaEditor.java +++ b/java/src/processing/mode/java/JavaEditor.java @@ -28,9 +28,13 @@ import java.awt.event.*; import java.io.*; import java.net.HttpURLConnection; import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; import javax.swing.*; import javax.swing.event.*; @@ -230,7 +234,17 @@ public class JavaEditor extends Editor { } }); - return buildFileMenu(new JMenuItem[] { exportApplication }); + var exportPDEZ = new JMenuItem(Language.text("menu.file.export_pdez")); + exportPDEZ.addActionListener(e -> { + if (sketch.isUntitled() || sketch.isReadOnly()) { + Messages.showMessage("Save First", "Please first save the sketch."); + } else { + handleExportPDEZ(); + } + }); + + + return buildFileMenu(new JMenuItem[] { exportApplication, exportPDEZ }); } @@ -491,6 +505,52 @@ public class JavaEditor extends Editor { } } + /** + * Handler for File → Export PDEZ + */ + public void handleExportPDEZ() { + if (handleExportCheckModified()) { + var sketch = getSketch(); + var folder = sketch.getFolder().toPath(); + var target = new File(folder + ".pdez").toPath(); + if (Files.exists(target)) { + try { + Platform.deleteFile(target.toFile()); + } catch (IOException e) { + Messages.showError("Export Error", "Could not delete existing file: " + target, e); + } + } + + try (var zs = new ZipOutputStream(Files.newOutputStream(target))) { + Files.walk(folder) + .filter(path -> !Files.isDirectory(path)) + .forEach(path -> { + var zipEntry = new ZipEntry(folder.getParent().relativize(path).toString()); + try { + zs.putNextEntry(zipEntry); + Files.copy(path, zs); + zs.closeEntry(); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } catch (IOException e) { + throw new RuntimeException(e); + } + if (Desktop.isDesktopSupported()) { + var desktop = Desktop.getDesktop(); + if (desktop.isSupported(Desktop.Action.BROWSE_FILE_DIR)) { + desktop.browseFileDirectory(target.toFile()); + } else { + try { + desktop.open(target.getParent().toFile()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + } + } /** * Checks to see if the sketch has been modified, and if so,