Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@
/**
* Generates necessary PWA icons.
* <p>
* 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.
* <p>
* For internal use only. May be renamed or removed in a future release.
*/
public class TaskGeneratePWAIcons implements FallibleCommand {
Expand Down Expand Up @@ -94,8 +99,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 {
Expand All @@ -115,6 +119,7 @@ public void execute() throws ExecutionFailedException {
} finally {
executor.shutdown();
}
baseImage.flush();
} finally {
if (headless == null) {
System.clearProperty(HEADLESS_PROPERTY);
Expand Down Expand Up @@ -173,30 +178,22 @@ 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 = PwaIcon.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;

public InternalPwaIcon(PwaIcon icon, BufferedImage baseImage) {
super(icon);
this.baseImage = baseImage;
}

@Override
protected BufferedImage getBaseImage() {
return baseImage;
}

}
}
82 changes: 51 additions & 31 deletions flow-server/src/main/java/com/vaadin/flow/server/PwaIcon.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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;
}

Expand Down
Loading