Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: generate PWA icons at build time #20516

Merged
merged 3 commits into from
Nov 20, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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 @@ -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
Loading