Skip to content

Commit 30aa0f1

Browse files
committed
Add WebP conversion fallback and install tools in CI
1 parent 530d0df commit 30aa0f1

File tree

2 files changed

+126
-3
lines changed

2 files changed

+126
-3
lines changed

.github/workflows/avd-skin-conversion.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ jobs:
2525
distribution: temurin
2626
java-version: '21'
2727

28+
- name: Install WebP conversion tools
29+
run: |
30+
sudo apt-get update
31+
sudo apt-get install -y webp
32+
2833
- name: Download sample AVD skin
2934
run: |
3035
set -euo pipefail

AvdSkinToCodenameOneSkin.java

Lines changed: 121 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,19 @@
33
import java.awt.image.ImageObserver;
44
import java.awt.image.PixelGrabber;
55
import java.io.*;
6+
import java.nio.charset.StandardCharsets;
67
import java.nio.file.*;
78
import java.time.LocalDateTime;
89
import java.time.format.DateTimeFormatter;
910
import java.util.*;
1011
import java.util.List;
1112
import java.util.zip.ZipEntry;
1213
import java.util.zip.ZipOutputStream;
14+
import javax.imageio.ImageIO;
15+
import javax.imageio.spi.IIORegistry;
16+
import javax.imageio.spi.ImageReaderSpi;
17+
import javax.imageio.ImageReader;
18+
import javax.imageio.stream.ImageInputStream;
1319

1420
/**
1521
* Command line utility that converts a standard Android emulator skin into a
@@ -30,11 +36,13 @@
3036
*/
3137
public class AvdSkinToCodenameOneSkin {
3238

39+
static {
40+
ensureWebpSupport();
41+
}
42+
3343
private static final double TABLET_INCH_THRESHOLD = 6.5d;
3444

3545
public static void main(String[] args) throws Exception {
36-
System.setProperty("java.awt.headless", "true");
37-
3846
if (args.length == 0 || args.length > 2) {
3947
System.err.println("Usage: java AvdSkinToCodenameOneSkin.java <avd-skin-dir> [output.skin]");
4048
System.exit(1);
@@ -147,11 +155,48 @@ private static DeviceImages buildDeviceImages(Path skinDir, OrientationInfo orie
147155
}
148156

149157
private static BufferedImage readImage(Path imagePath) throws IOException {
150-
BufferedImage standard = javax.imageio.ImageIO.read(imagePath.toFile());
158+
String lowerName = imagePath.getFileName().toString().toLowerCase(Locale.ROOT);
159+
160+
BufferedImage standard = ImageIO.read(imagePath.toFile());
151161
if (standard != null) {
152162
return standard;
153163
}
154164

165+
try (InputStream in = Files.newInputStream(imagePath)) {
166+
BufferedImage viaStream = ImageIO.read(in);
167+
if (viaStream != null) {
168+
return viaStream;
169+
}
170+
}
171+
172+
try (ImageInputStream iis = ImageIO.createImageInputStream(Files.newInputStream(imagePath))) {
173+
if (iis != null) {
174+
Iterator<ImageReader> readers = ImageIO.getImageReaders(iis);
175+
if (readers.hasNext()) {
176+
ImageReader reader = readers.next();
177+
try {
178+
iis.seek(0);
179+
reader.setInput(iis, true, true);
180+
BufferedImage decoded = reader.read(0);
181+
if (decoded != null) {
182+
return decoded;
183+
}
184+
} finally {
185+
reader.dispose();
186+
}
187+
}
188+
}
189+
}
190+
191+
BufferedImage viaDwebp = decodeWithDwebp(imagePath);
192+
if (viaDwebp != null) {
193+
return viaDwebp;
194+
}
195+
196+
if (GraphicsEnvironment.isHeadless() && lowerName.endsWith(".webp")) {
197+
throw new IllegalStateException("WebP decoding requires the 'dwebp' command when running headless. Install the 'webp' package and ensure 'dwebp' is on the PATH.");
198+
}
199+
155200
byte[] data = Files.readAllBytes(imagePath);
156201
Image toolkitImage;
157202
try {
@@ -193,6 +238,79 @@ private static BufferedImage readImage(Path imagePath) throws IOException {
193238
return result;
194239
}
195240

241+
private static BufferedImage decodeWithDwebp(Path imagePath) throws IOException {
242+
String lower = imagePath.getFileName().toString().toLowerCase(Locale.ROOT);
243+
if (!lower.endsWith(".webp")) {
244+
return null;
245+
}
246+
Path tempFile = null;
247+
try {
248+
tempFile = Files.createTempFile("avd-webp-", ".png");
249+
Process process = new ProcessBuilder("dwebp", imagePath.toString(), "-o", tempFile.toString())
250+
.redirectErrorStream(true)
251+
.start();
252+
String output;
253+
try (InputStream stdout = process.getInputStream()) {
254+
output = new String(stdout.readAllBytes(), StandardCharsets.UTF_8).trim();
255+
}
256+
int exit = process.waitFor();
257+
if (exit != 0) {
258+
throw new IOException("dwebp exited with status " + exit + (output.isEmpty() ? "" : ": " + output));
259+
}
260+
try (InputStream pngStream = Files.newInputStream(tempFile)) {
261+
BufferedImage converted = ImageIO.read(pngStream);
262+
if (converted == null) {
263+
throw new IOException("dwebp produced an unreadable PNG for " + imagePath);
264+
}
265+
return converted;
266+
}
267+
} catch (IOException err) {
268+
String message = err.getMessage();
269+
if (message != null && message.contains("No such file or directory")) {
270+
System.err.println("Warning: dwebp command not available. Install the 'webp' package to enable WebP decoding.");
271+
return null;
272+
}
273+
throw err;
274+
} catch (InterruptedException err) {
275+
Thread.currentThread().interrupt();
276+
throw new IOException("Interrupted while running dwebp for " + imagePath, err);
277+
} finally {
278+
if (tempFile != null) {
279+
try {
280+
Files.deleteIfExists(tempFile);
281+
} catch (IOException ignored) {
282+
}
283+
}
284+
}
285+
}
286+
287+
private static boolean ensureWebpSupport() {
288+
try {
289+
ImageIO.setUseCache(false);
290+
ImageIO.scanForPlugins();
291+
if (hasWebpReader()) {
292+
return true;
293+
}
294+
Class<?> spiClass = Class.forName("jdk.imageio.webp.WebPImageReaderSpi");
295+
Object instance = spiClass.getDeclaredConstructor().newInstance();
296+
if (instance instanceof ImageReaderSpi spi) {
297+
IIORegistry registry = IIORegistry.getDefaultInstance();
298+
registry.registerServiceProvider(spi);
299+
}
300+
} catch (ClassNotFoundException err) {
301+
return hasWebpReader();
302+
} catch (ReflectiveOperationException | LinkageError err) {
303+
System.err.println("Warning: Unable to initialise WebP decoder: " + err.getMessage());
304+
return hasWebpReader();
305+
}
306+
return hasWebpReader();
307+
}
308+
309+
private static boolean hasWebpReader() {
310+
return ImageIO.getImageReadersBySuffix("webp").hasNext()
311+
|| ImageIO.getImageReadersByMIMEType("image/webp").hasNext();
312+
}
313+
196314
private static void writeEntry(ZipOutputStream zos, String name, BufferedImage image) throws IOException {
197315
ZipEntry entry = new ZipEntry(name);
198316
zos.putNextEntry(entry);

0 commit comments

Comments
 (0)