Skip to content

Commit 0e82b64

Browse files
authored
chore: optimize pwa icon generation (#23934)
Do not collect all generated icons into memory, but instead write each icon to disc flushing the cache to not use so much memory.
1 parent f842b1f commit 0e82b64

File tree

2 files changed

+69
-52
lines changed

2 files changed

+69
-52
lines changed

flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/TaskGeneratePWAIcons.java

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@
4545
/**
4646
* Generates necessary PWA icons.
4747
* <p>
48+
* Icons are processed in parallel but each thread draws, writes the PNG
49+
* directly to disk, and immediately flushes the scaled image. This avoids
50+
* accumulating all icon data in memory while still benefiting from concurrent
51+
* I/O and image scaling.
52+
* <p>
4853
* For internal use only. May be renamed or removed in a future release.
4954
*/
5055
public class TaskGeneratePWAIcons implements FallibleCommand {
@@ -94,8 +99,7 @@ public void execute() throws ExecutionFailedException {
9499
ExecutorService executor = Executors.newFixedThreadPool(4);
95100
CompletableFuture<?>[] iconsGenerators = PwaRegistry
96101
.getIconTemplates(pwaConfiguration.getIconPath()).stream()
97-
.map(icon -> new InternalPwaIcon(icon, baseImage))
98-
.map(this::generateIcon)
102+
.map(icon -> generateIconTask(icon, baseImage))
99103
.map(task -> CompletableFuture.runAsync(task, executor))
100104
.toArray(CompletableFuture[]::new);
101105
try {
@@ -115,6 +119,7 @@ public void execute() throws ExecutionFailedException {
115119
} finally {
116120
executor.shutdown();
117121
}
122+
baseImage.flush();
118123
} finally {
119124
if (headless == null) {
120125
System.clearProperty(HEADLESS_PROPERTY);
@@ -173,30 +178,22 @@ private URL findIcon(PwaConfiguration pwaConfiguration) {
173178
return iconURL;
174179
}
175180

176-
private Runnable generateIcon(InternalPwaIcon icon) {
177-
Path iconPath = generatedIconsPath.resolve(icon.getRelHref()
178-
.substring(1).replace('/', File.separatorChar));
181+
private Runnable generateIconTask(PwaIcon icon, BufferedImage baseImage) {
182+
String relHref = "/" + icon.getHref().split("\\?")[0];
183+
Path iconPath = generatedIconsPath
184+
.resolve(relHref.substring(1).replace('/', File.separatorChar));
185+
int targetWidth = icon.getWidth();
186+
int targetHeight = icon.getHeight();
179187
return () -> {
188+
BufferedImage scaled = PwaIcon.drawIconImage(baseImage, targetWidth,
189+
targetHeight);
180190
try (OutputStream os = Files.newOutputStream(iconPath)) {
181-
icon.write(os);
191+
ImageIO.write(scaled, "png", os);
182192
} catch (IOException e) {
183193
throw new UncheckedIOException(e);
194+
} finally {
195+
scaled.flush();
184196
}
185197
};
186198
}
187-
188-
private static class InternalPwaIcon extends PwaIcon {
189-
private final BufferedImage baseImage;
190-
191-
public InternalPwaIcon(PwaIcon icon, BufferedImage baseImage) {
192-
super(icon);
193-
this.baseImage = baseImage;
194-
}
195-
196-
@Override
197-
protected BufferedImage getBaseImage() {
198-
return baseImage;
199-
}
200-
201-
}
202199
}

flow-server/src/main/java/com/vaadin/flow/server/PwaIcon.java

Lines changed: 51 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,8 @@ public void write(OutputStream outputStream) {
276276
if (data == null) {
277277
// New image with wanted size
278278
// Store byte array and hashcode of image (GeneratedImage)
279-
setImage(drawIconImage(getBaseImage()));
279+
setImage(drawIconImage(getBaseImage(), this.getWidth(),
280+
this.getHeight()));
280281
}
281282
try {
282283
outputStream.write(data);
@@ -292,43 +293,62 @@ protected BufferedImage getBaseImage() {
292293
return registry.getBaseImage();
293294
}
294295

295-
private BufferedImage drawIconImage(BufferedImage baseImage) {
296+
/**
297+
* Draws a resized version of the given base image centered on a new image
298+
* of the specified dimensions. The top-left pixel of the base image is used
299+
* as the background fill color. The image is scaled down to fit within the
300+
* target dimensions while preserving its aspect ratio; upscaling is not
301+
* performed.
302+
*
303+
* @param baseImage
304+
* the source image to resize
305+
* @param targetWidth
306+
* the width of the resulting image in pixels
307+
* @param targetHeight
308+
* the height of the resulting image in pixels
309+
* @return a new {@link BufferedImage} with the resized icon drawn centered
310+
*/
311+
public static BufferedImage drawIconImage(BufferedImage baseImage,
312+
int targetWidth, int targetHeight) {
296313
// Pick top-left pixel as fill color if needed for image
297314
// resizing
298315
int bgColor = baseImage.getRGB(0, 0);
299316

300-
BufferedImage bimage = new BufferedImage(this.getWidth(),
301-
this.getHeight(), BufferedImage.TYPE_INT_ARGB);
317+
BufferedImage bimage = new BufferedImage(targetWidth, targetHeight,
318+
BufferedImage.TYPE_INT_ARGB);
302319
// Draw the image on to the buffered image
303320
Graphics2D graphics = bimage.createGraphics();
304321

305-
// fill bg with fill-color
306-
graphics.setBackground(new Color(bgColor, true));
307-
graphics.clearRect(0, 0, this.getWidth(), this.getHeight());
308-
309-
// calculate ratio (bigger ratio) for resize
310-
float ratio = (float) baseImage.getWidth()
311-
/ (float) this.getWidth() > (float) baseImage.getHeight()
312-
/ (float) this.getHeight()
313-
? (float) baseImage.getWidth()
314-
/ (float) this.getWidth()
315-
: (float) baseImage.getHeight()
316-
/ (float) this.getHeight();
317-
318-
// Forbid upscaling of image
319-
ratio = ratio > 1.0f ? ratio : 1.0f;
320-
321-
// calculate sizes with ratio
322-
int newWidth = Math.round(baseImage.getHeight() / ratio);
323-
int newHeight = Math.round(baseImage.getWidth() / ratio);
324-
325-
// draw rescaled img in the center of created image
326-
graphics.drawImage(
327-
baseImage.getScaledInstance(newWidth, newHeight,
328-
Image.SCALE_SMOOTH),
329-
(this.getWidth() - newWidth) / 2,
330-
(this.getHeight() - newHeight) / 2, null);
331-
graphics.dispose();
322+
try {
323+
// fill bg with fill-color
324+
graphics.setBackground(new Color(bgColor, true));
325+
graphics.clearRect(0, 0, targetWidth, targetHeight);
326+
327+
// calculate ratio (bigger ratio) for resize
328+
float ratio = (float) baseImage.getWidth()
329+
/ (float) targetWidth > (float) baseImage.getHeight()
330+
/ (float) targetHeight
331+
? (float) baseImage.getWidth()
332+
/ (float) targetWidth
333+
: (float) baseImage.getHeight()
334+
/ (float) targetHeight;
335+
336+
// Forbid upscaling of image
337+
ratio = ratio > 1.0f ? ratio : 1.0f;
338+
339+
// calculate sizes with ratio
340+
int newWidth = Math.round(baseImage.getHeight() / ratio);
341+
int newHeight = Math.round(baseImage.getWidth() / ratio);
342+
343+
// draw rescaled img in the center of created image
344+
graphics.drawImage(
345+
baseImage.getScaledInstance(newWidth, newHeight,
346+
Image.SCALE_SMOOTH),
347+
(targetWidth - newWidth) / 2,
348+
(targetHeight - newHeight) / 2, null);
349+
} finally {
350+
graphics.dispose();
351+
}
332352
return bimage;
333353
}
334354

0 commit comments

Comments
 (0)