Skip to content

Commit

Permalink
feat: generate PWA icons at build time (#20516)
Browse files Browse the repository at this point in the history
Generates PWA icons during the production build, preventing the need to use
AWT APIs at runtime and making first requests to the application faster.
Also prevents potential issues caused by loading AWT native library in
native images.

Fixes #19497
  • Loading branch information
mcollovati authored Nov 20, 2024
1 parent 0a7c507 commit 9e80eb8
Show file tree
Hide file tree
Showing 7 changed files with 500 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,11 @@ public final class Constants implements Serializable {
*/
public static final String VAADIN_WEBAPP = "webapp/";

/**
* The generated PWA icons folder.
*/
public static final String VAADIN_PWA_ICONS = "pwa-icons/";

/**
* The path to meta-inf/VAADIN/ where static resources are put on the
* servlet.
Expand Down
38 changes: 37 additions & 1 deletion flow-server/src/main/java/com/vaadin/flow/server/PwaIcon.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@
package com.vaadin.flow.server;

import javax.imageio.ImageIO;

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.io.UncheckedIOException;
Expand Down Expand Up @@ -99,6 +101,15 @@ public enum Domain {
setRelativeName();
}

protected PwaIcon(PwaIcon icon) {
this.width = icon.width;
this.height = icon.height;
this.baseName = icon.baseName;
this.domain = icon.domain;
this.shouldBeCached = icon.shouldBeCached;
this.attributes.putAll(icon.attributes);
}

/**
* Gets an {@link Element} presentation of the icon.
*
Expand Down Expand Up @@ -236,6 +247,25 @@ public void setImage(BufferedImage image) {
}
}

void setImage(InputStream image) throws IOException {
if (image != null) {
data = image.readAllBytes();
fileHash = Arrays.hashCode(data);
setRelativeName();
}
}

/**
* Gets if the icon can be written on a stream or not.
*
* @return {@literal true} if the icon can be written, otherwise
* {@literal false}.
* @see #write(OutputStream)
*/
boolean isAvailable() {
return data != null || registry.getBaseImage() != null;
}

/**
* Writes the icon image to output stream.
*
Expand All @@ -246,7 +276,7 @@ public void write(OutputStream outputStream) {
if (data == null) {
// New image with wanted size
// Store byte array and hashcode of image (GeneratedImage)
setImage(drawIconImage(registry.getBaseImage()));
setImage(drawIconImage(getBaseImage()));
}
try {
outputStream.write(data);
Expand All @@ -257,6 +287,11 @@ public void write(OutputStream outputStream) {
}
}

// visible for test
protected BufferedImage getBaseImage() {
return registry.getBaseImage();
}

private BufferedImage drawIconImage(BufferedImage baseImage) {
// Pick top-left pixel as fill color if needed for image
// resizing
Expand Down Expand Up @@ -296,4 +331,5 @@ private BufferedImage drawIconImage(BufferedImage baseImage) {
graphics.dispose();
return bimage;
}

}
84 changes: 64 additions & 20 deletions flow-server/src/main/java/com/vaadin/flow/server/PwaRegistry.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,10 @@
*/
package com.vaadin.flow.server;

import javax.imageio.ImageIO;
import jakarta.servlet.ServletContext;

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Image;
import javax.imageio.ImageIO;

import java.awt.image.BufferedImage;
import java.io.BufferedReader;
import java.io.IOException;
Expand All @@ -29,21 +27,21 @@
import java.io.Serializable;
import java.io.UncheckedIOException;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.vaadin.flow.di.Lookup;
import com.vaadin.flow.di.ResourceProvider;
import com.vaadin.flow.server.communication.PwaHandler;
import com.vaadin.flow.server.startup.ApplicationConfiguration;
import com.vaadin.flow.server.startup.ApplicationRouteRegistry;
Expand Down Expand Up @@ -85,6 +83,7 @@ public class PwaRegistry implements Serializable {
private List<PwaIcon> icons = new ArrayList<>();
private final PwaConfiguration pwaConfiguration;

private URL baseImageUrl;
private BufferedImage baseImage;

/**
Expand Down Expand Up @@ -113,7 +112,21 @@ public PwaRegistry(PWA pwa, ServletContext servletContext)
initializeResources(servletContext);
}

// Lazy load base image to prevent using AWT api unless icon
// generation is required at runtime.
// baseImageUrl is computed during registry initialization and used on to
// load the image.
BufferedImage getBaseImage() {
if (baseImage == null && baseImageUrl != null) {
try {
baseImage = getBaseImage(baseImageUrl);
} catch (IOException ex) {
getLogger().error("Image is not found or can't be loaded: {}",
baseImageUrl);
} finally {
baseImageUrl = null;
}
}
return baseImage;
}

Expand All @@ -124,24 +137,19 @@ private void initializeResources(ServletContext servletContext)
}
long start = System.currentTimeMillis();

// Load base logo from servlet context if available
// fall back to local image if unavailable
URL logo = getResourceUrl(servletContext,
pwaConfiguration.relIconPath());
baseImageUrl = logo != null ? logo
: BootstrapHandler.class.getResource("default-logo.png");

URL offlinePage = pwaConfiguration.isOfflinePathEnabled()
? getResourceUrl(servletContext,
pwaConfiguration.relOfflinePath())
: null;

// Load base logo from servlet context if available
// fall back to local image if unavailable
baseImage = getBaseImage(logo);

if (baseImage == null) {
getLogger().error("Image is not found or can't be loaded: " + logo);
} else {
// initialize icons
icons = initializeIcons();
}
icons = initializeIcons(servletContext);

// Load offline page as string, from servlet context if
// available, fall back to default page
Expand Down Expand Up @@ -175,14 +183,43 @@ private URL getResourceUrl(ServletContext context, String path)
return resourceUrl;
}

private List<PwaIcon> initializeIcons() {
private List<PwaIcon> initializeIcons(ServletContext servletContext) {
Optional<ResourceProvider> optionalResourceProvider = Optional
.ofNullable(new VaadinServletContext(servletContext)
.getAttribute(Lookup.class))
.map(lookup -> lookup.lookup(ResourceProvider.class));
for (PwaIcon icon : getIconTemplates(pwaConfiguration.getIconPath())) {
icon.setRegistry(this);
icons.add(icon);
// Try to find a pre-generated image
String iconPath = Constants.VAADIN_WEBAPP_RESOURCES
+ Constants.VAADIN_PWA_ICONS
+ icon.getRelHref().substring(1);
optionalResourceProvider.ifPresent(
provider -> tryLoadGeneratedIcon(provider, icon, iconPath));
if (icon.isAvailable()) {
icons.add(icon);
}
}
return icons;
}

private static void tryLoadGeneratedIcon(ResourceProvider resourceProvider,
PwaIcon icon, String iconPath) {
URL iconResource = resourceProvider.getApplicationResource(iconPath);
if (iconResource != null) {
try (InputStream data = iconResource.openStream()) {
icon.setImage(data);
getLogger().trace("Loading generated PWA image from {}",
iconPath);
} catch (IOException ex) {
// Ignore, icon will be generated at runtime
getLogger().debug(
"Cannot load generated PWA image from {}. Icon will be regenerated at runtime.",
iconPath, ex);
}
}
}

/**
* Creates manifest.webmanifest json object.
*
Expand Down Expand Up @@ -443,7 +480,14 @@ public PwaConfiguration getPwaConfiguration() {
return pwaConfiguration;
}

static List<PwaIcon> getIconTemplates(String baseName) {
/**
* Gets all PWA icon variants for the give base icon.
*
* @param baseName
* path of the base icon.
* @return list of PWA icons variants.
*/
public static List<PwaIcon> getIconTemplates(String baseName) {
List<PwaIcon> icons = new ArrayList<>();
// Basic manifest icons for android support
icons.add(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.vaadin.experimental.FeatureFlags;
import com.vaadin.flow.di.Lookup;
import com.vaadin.flow.internal.UsageStatistics;
import com.vaadin.flow.server.Constants;
Expand Down Expand Up @@ -80,6 +79,7 @@ public class NodeTasks implements FallibleCommand {
TaskGenerateEndpoint.class,
TaskCopyFrontendFiles.class,
TaskCopyLocalFrontendFiles.class,
TaskGeneratePWAIcons.class,
TaskUpdateSettingsFile.class,
TaskUpdateVite.class,
TaskUpdateImports.class,
Expand Down Expand Up @@ -259,6 +259,9 @@ public NodeTasks(Options options) {
} else {
pwa = new PwaConfiguration();
}
if (options.isProductionMode() && pwa.isEnabled()) {
commands.add(new TaskGeneratePWAIcons(options, pwa));
}
commands.add(new TaskUpdateSettingsFile(options, themeName, pwa));
if (options.isFrontendHotdeploy() || options.isBundleBuild()) {
commands.add(new TaskUpdateVite(options, webComponentTags));
Expand Down
Loading

0 comments on commit 9e80eb8

Please sign in to comment.