1717
1818import javax .imageio .ImageIO ;
1919
20+ import java .awt .Color ;
21+ import java .awt .Graphics2D ;
22+ import java .awt .Image ;
2023import java .awt .image .BufferedImage ;
2124import java .io .File ;
2225import java .io .IOException ;
4548/**
4649 * Generates necessary PWA icons.
4750 * <p>
51+ * Icons are processed in parallel but each thread draws, writes the PNG
52+ * directly to disk, and immediately flushes the scaled image. This avoids
53+ * accumulating all icon data in memory while still benefiting from concurrent
54+ * I/O and image scaling.
55+ * <p>
4856 * For internal use only. May be renamed or removed in a future release.
4957 */
5058public class TaskGeneratePWAIcons implements FallibleCommand {
@@ -94,8 +102,7 @@ public void execute() throws ExecutionFailedException {
94102 ExecutorService executor = Executors .newFixedThreadPool (4 );
95103 CompletableFuture <?>[] iconsGenerators = PwaRegistry
96104 .getIconTemplates (pwaConfiguration .getIconPath ()).stream ()
97- .map (icon -> new InternalPwaIcon (icon , baseImage ))
98- .map (this ::generateIcon )
105+ .map (icon -> generateIconTask (icon , baseImage ))
99106 .map (task -> CompletableFuture .runAsync (task , executor ))
100107 .toArray (CompletableFuture []::new );
101108 try {
@@ -115,6 +122,7 @@ public void execute() throws ExecutionFailedException {
115122 } finally {
116123 executor .shutdown ();
117124 }
125+ baseImage .flush ();
118126 } finally {
119127 if (headless == null ) {
120128 System .clearProperty (HEADLESS_PROPERTY );
@@ -173,30 +181,51 @@ private URL findIcon(PwaConfiguration pwaConfiguration) {
173181 return iconURL ;
174182 }
175183
176- private Runnable generateIcon (InternalPwaIcon icon ) {
177- Path iconPath = generatedIconsPath .resolve (icon .getRelHref ()
178- .substring (1 ).replace ('/' , File .separatorChar ));
184+ private Runnable generateIconTask (PwaIcon icon , BufferedImage baseImage ) {
185+ String relHref = "/" + icon .getHref ().split ("\\ ?" )[0 ];
186+ Path iconPath = generatedIconsPath
187+ .resolve (relHref .substring (1 ).replace ('/' , File .separatorChar ));
188+ int targetWidth = icon .getWidth ();
189+ int targetHeight = icon .getHeight ();
179190 return () -> {
191+ BufferedImage scaled = drawIconImage (baseImage , targetWidth ,
192+ targetHeight );
180193 try (OutputStream os = Files .newOutputStream (iconPath )) {
181- icon .write (os );
194+ ImageIO .write (scaled , "png" , os );
182195 } catch (IOException e ) {
183196 throw new UncheckedIOException (e );
197+ } finally {
198+ scaled .flush ();
184199 }
185200 };
186201 }
187202
188- private static class InternalPwaIcon extends PwaIcon {
189- private final BufferedImage baseImage ;
203+ private static BufferedImage drawIconImage (BufferedImage baseImage ,
204+ int targetWidth , int targetHeight ) {
205+ int bgColor = baseImage .getRGB (0 , 0 );
190206
191- public InternalPwaIcon (PwaIcon icon , BufferedImage baseImage ) {
192- super (icon );
193- this .baseImage = baseImage ;
194- }
207+ BufferedImage bimage = new BufferedImage (targetWidth , targetHeight ,
208+ BufferedImage .TYPE_INT_ARGB );
209+ Graphics2D graphics = bimage .createGraphics ();
210+ try {
211+ graphics .setBackground (new Color (bgColor , true ));
212+ graphics .clearRect (0 , 0 , targetWidth , targetHeight );
195213
196- @ Override
197- protected BufferedImage getBaseImage () {
198- return baseImage ;
199- }
214+ float ratio = Math .max ((float ) baseImage .getWidth () / targetWidth ,
215+ (float ) baseImage .getHeight () / targetHeight );
216+ ratio = Math .max (ratio , 1.0f );
217+
218+ int newWidth = Math .round (baseImage .getHeight () / ratio );
219+ int newHeight = Math .round (baseImage .getWidth () / ratio );
200220
221+ graphics .drawImage (
222+ baseImage .getScaledInstance (newWidth , newHeight ,
223+ Image .SCALE_SMOOTH ),
224+ (targetWidth - newWidth ) / 2 ,
225+ (targetHeight - newHeight ) / 2 , null );
226+ } finally {
227+ graphics .dispose ();
228+ }
229+ return bimage ;
201230 }
202231}
0 commit comments