From 48bf2447652746918bce2fc5b57b68cdd209b21e Mon Sep 17 00:00:00 2001 From: Mikael Grankvist Date: Wed, 18 Mar 2026 10:27:24 +0200 Subject: [PATCH 1/2] chore: optimize pwa icon generation Do not collect all generated icons into memory, but instead write each icon to disc flushing the cache to not use so much memory. --- .../server/frontend/TaskGeneratePWAIcons.java | 61 ++++++++++++++----- 1 file changed, 45 insertions(+), 16 deletions(-) diff --git a/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/TaskGeneratePWAIcons.java b/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/TaskGeneratePWAIcons.java index e9b89f248a5..3384d2459e5 100644 --- a/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/TaskGeneratePWAIcons.java +++ b/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/TaskGeneratePWAIcons.java @@ -17,6 +17,9 @@ import javax.imageio.ImageIO; +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Image; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; @@ -45,6 +48,11 @@ /** * Generates necessary PWA icons. *

+ * Icons are processed in parallel but each thread draws, writes the PNG + * directly to disk, and immediately flushes the scaled image. This avoids + * accumulating all icon data in memory while still benefiting from concurrent + * I/O and image scaling. + *

* For internal use only. May be renamed or removed in a future release. */ public class TaskGeneratePWAIcons implements FallibleCommand { @@ -94,8 +102,7 @@ public void execute() throws ExecutionFailedException { ExecutorService executor = Executors.newFixedThreadPool(4); CompletableFuture[] iconsGenerators = PwaRegistry .getIconTemplates(pwaConfiguration.getIconPath()).stream() - .map(icon -> new InternalPwaIcon(icon, baseImage)) - .map(this::generateIcon) + .map(icon -> generateIconTask(icon, baseImage)) .map(task -> CompletableFuture.runAsync(task, executor)) .toArray(CompletableFuture[]::new); try { @@ -115,6 +122,7 @@ public void execute() throws ExecutionFailedException { } finally { executor.shutdown(); } + baseImage.flush(); } finally { if (headless == null) { System.clearProperty(HEADLESS_PROPERTY); @@ -173,30 +181,51 @@ private URL findIcon(PwaConfiguration pwaConfiguration) { return iconURL; } - private Runnable generateIcon(InternalPwaIcon icon) { - Path iconPath = generatedIconsPath.resolve(icon.getRelHref() - .substring(1).replace('/', File.separatorChar)); + private Runnable generateIconTask(PwaIcon icon, BufferedImage baseImage) { + String relHref = "/" + icon.getHref().split("\\?")[0]; + Path iconPath = generatedIconsPath + .resolve(relHref.substring(1).replace('/', File.separatorChar)); + int targetWidth = icon.getWidth(); + int targetHeight = icon.getHeight(); return () -> { + BufferedImage scaled = drawIconImage(baseImage, targetWidth, + targetHeight); try (OutputStream os = Files.newOutputStream(iconPath)) { - icon.write(os); + ImageIO.write(scaled, "png", os); } catch (IOException e) { throw new UncheckedIOException(e); + } finally { + scaled.flush(); } }; } - private static class InternalPwaIcon extends PwaIcon { - private final BufferedImage baseImage; + private static BufferedImage drawIconImage(BufferedImage baseImage, + int targetWidth, int targetHeight) { + int bgColor = baseImage.getRGB(0, 0); - public InternalPwaIcon(PwaIcon icon, BufferedImage baseImage) { - super(icon); - this.baseImage = baseImage; - } + BufferedImage bimage = new BufferedImage(targetWidth, targetHeight, + BufferedImage.TYPE_INT_ARGB); + Graphics2D graphics = bimage.createGraphics(); + try { + graphics.setBackground(new Color(bgColor, true)); + graphics.clearRect(0, 0, targetWidth, targetHeight); - @Override - protected BufferedImage getBaseImage() { - return baseImage; - } + float ratio = Math.max((float) baseImage.getWidth() / targetWidth, + (float) baseImage.getHeight() / targetHeight); + ratio = Math.max(ratio, 1.0f); + + int newWidth = Math.round(baseImage.getHeight() / ratio); + int newHeight = Math.round(baseImage.getWidth() / ratio); + graphics.drawImage( + baseImage.getScaledInstance(newWidth, newHeight, + Image.SCALE_SMOOTH), + (targetWidth - newWidth) / 2, + (targetHeight - newHeight) / 2, null); + } finally { + graphics.dispose(); + } + return bimage; } } From ea48ded8c13c38eb7c10bebcabd2afd3ca0d3517 Mon Sep 17 00:00:00 2001 From: Mikael Grankvist Date: Thu, 19 Mar 2026 11:03:38 +0200 Subject: [PATCH 2/2] Unify drawIconImage to one place --- .../server/frontend/TaskGeneratePWAIcons.java | 34 +------- .../java/com/vaadin/flow/server/PwaIcon.java | 82 ++++++++++++------- 2 files changed, 52 insertions(+), 64 deletions(-) diff --git a/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/TaskGeneratePWAIcons.java b/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/TaskGeneratePWAIcons.java index 3384d2459e5..ed42ef9e79e 100644 --- a/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/TaskGeneratePWAIcons.java +++ b/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/TaskGeneratePWAIcons.java @@ -17,9 +17,6 @@ import javax.imageio.ImageIO; -import java.awt.Color; -import java.awt.Graphics2D; -import java.awt.Image; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; @@ -188,7 +185,7 @@ private Runnable generateIconTask(PwaIcon icon, BufferedImage baseImage) { int targetWidth = icon.getWidth(); int targetHeight = icon.getHeight(); return () -> { - BufferedImage scaled = drawIconImage(baseImage, targetWidth, + BufferedImage scaled = PwaIcon.drawIconImage(baseImage, targetWidth, targetHeight); try (OutputStream os = Files.newOutputStream(iconPath)) { ImageIO.write(scaled, "png", os); @@ -199,33 +196,4 @@ private Runnable generateIconTask(PwaIcon icon, BufferedImage baseImage) { } }; } - - private static BufferedImage drawIconImage(BufferedImage baseImage, - int targetWidth, int targetHeight) { - int bgColor = baseImage.getRGB(0, 0); - - BufferedImage bimage = new BufferedImage(targetWidth, targetHeight, - BufferedImage.TYPE_INT_ARGB); - Graphics2D graphics = bimage.createGraphics(); - try { - graphics.setBackground(new Color(bgColor, true)); - graphics.clearRect(0, 0, targetWidth, targetHeight); - - float ratio = Math.max((float) baseImage.getWidth() / targetWidth, - (float) baseImage.getHeight() / targetHeight); - ratio = Math.max(ratio, 1.0f); - - int newWidth = Math.round(baseImage.getHeight() / ratio); - int newHeight = Math.round(baseImage.getWidth() / ratio); - - graphics.drawImage( - baseImage.getScaledInstance(newWidth, newHeight, - Image.SCALE_SMOOTH), - (targetWidth - newWidth) / 2, - (targetHeight - newHeight) / 2, null); - } finally { - graphics.dispose(); - } - return bimage; - } } diff --git a/flow-server/src/main/java/com/vaadin/flow/server/PwaIcon.java b/flow-server/src/main/java/com/vaadin/flow/server/PwaIcon.java index b86b172f9e3..4db6fd5d42e 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/PwaIcon.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/PwaIcon.java @@ -276,7 +276,8 @@ public void write(OutputStream outputStream) { if (data == null) { // New image with wanted size // Store byte array and hashcode of image (GeneratedImage) - setImage(drawIconImage(getBaseImage())); + setImage(drawIconImage(getBaseImage(), this.getWidth(), + this.getHeight())); } try { outputStream.write(data); @@ -292,43 +293,62 @@ protected BufferedImage getBaseImage() { return registry.getBaseImage(); } - private BufferedImage drawIconImage(BufferedImage baseImage) { + /** + * Draws a resized version of the given base image centered on a new image + * of the specified dimensions. The top-left pixel of the base image is used + * as the background fill color. The image is scaled down to fit within the + * target dimensions while preserving its aspect ratio; upscaling is not + * performed. + * + * @param baseImage + * the source image to resize + * @param targetWidth + * the width of the resulting image in pixels + * @param targetHeight + * the height of the resulting image in pixels + * @return a new {@link BufferedImage} with the resized icon drawn centered + */ + public static BufferedImage drawIconImage(BufferedImage baseImage, + int targetWidth, int targetHeight) { // Pick top-left pixel as fill color if needed for image // resizing int bgColor = baseImage.getRGB(0, 0); - BufferedImage bimage = new BufferedImage(this.getWidth(), - this.getHeight(), BufferedImage.TYPE_INT_ARGB); + BufferedImage bimage = new BufferedImage(targetWidth, targetHeight, + BufferedImage.TYPE_INT_ARGB); // Draw the image on to the buffered image Graphics2D graphics = bimage.createGraphics(); - // fill bg with fill-color - graphics.setBackground(new Color(bgColor, true)); - graphics.clearRect(0, 0, this.getWidth(), this.getHeight()); - - // calculate ratio (bigger ratio) for resize - float ratio = (float) baseImage.getWidth() - / (float) this.getWidth() > (float) baseImage.getHeight() - / (float) this.getHeight() - ? (float) baseImage.getWidth() - / (float) this.getWidth() - : (float) baseImage.getHeight() - / (float) this.getHeight(); - - // Forbid upscaling of image - ratio = ratio > 1.0f ? ratio : 1.0f; - - // calculate sizes with ratio - int newWidth = Math.round(baseImage.getHeight() / ratio); - int newHeight = Math.round(baseImage.getWidth() / ratio); - - // draw rescaled img in the center of created image - graphics.drawImage( - baseImage.getScaledInstance(newWidth, newHeight, - Image.SCALE_SMOOTH), - (this.getWidth() - newWidth) / 2, - (this.getHeight() - newHeight) / 2, null); - graphics.dispose(); + try { + // fill bg with fill-color + graphics.setBackground(new Color(bgColor, true)); + graphics.clearRect(0, 0, targetWidth, targetHeight); + + // calculate ratio (bigger ratio) for resize + float ratio = (float) baseImage.getWidth() + / (float) targetWidth > (float) baseImage.getHeight() + / (float) targetHeight + ? (float) baseImage.getWidth() + / (float) targetWidth + : (float) baseImage.getHeight() + / (float) targetHeight; + + // Forbid upscaling of image + ratio = ratio > 1.0f ? ratio : 1.0f; + + // calculate sizes with ratio + int newWidth = Math.round(baseImage.getHeight() / ratio); + int newHeight = Math.round(baseImage.getWidth() / ratio); + + // draw rescaled img in the center of created image + graphics.drawImage( + baseImage.getScaledInstance(newWidth, newHeight, + Image.SCALE_SMOOTH), + (targetWidth - newWidth) / 2, + (targetHeight - newHeight) / 2, null); + } finally { + graphics.dispose(); + } return bimage; }