|
3 | 3 | import java.awt.image.ImageObserver; |
4 | 4 | import java.awt.image.PixelGrabber; |
5 | 5 | import java.io.*; |
| 6 | +import java.nio.charset.StandardCharsets; |
6 | 7 | import java.nio.file.*; |
7 | 8 | import java.time.LocalDateTime; |
8 | 9 | import java.time.format.DateTimeFormatter; |
9 | 10 | import java.util.*; |
10 | 11 | import java.util.List; |
11 | 12 | import java.util.zip.ZipEntry; |
12 | 13 | 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; |
13 | 19 |
|
14 | 20 | /** |
15 | 21 | * Command line utility that converts a standard Android emulator skin into a |
|
30 | 36 | */ |
31 | 37 | public class AvdSkinToCodenameOneSkin { |
32 | 38 |
|
| 39 | + static { |
| 40 | + ensureWebpSupport(); |
| 41 | + } |
| 42 | + |
33 | 43 | private static final double TABLET_INCH_THRESHOLD = 6.5d; |
34 | 44 |
|
35 | 45 | public static void main(String[] args) throws Exception { |
36 | | - System.setProperty("java.awt.headless", "true"); |
37 | | - |
38 | 46 | if (args.length == 0 || args.length > 2) { |
39 | 47 | System.err.println("Usage: java AvdSkinToCodenameOneSkin.java <avd-skin-dir> [output.skin]"); |
40 | 48 | System.exit(1); |
@@ -147,11 +155,48 @@ private static DeviceImages buildDeviceImages(Path skinDir, OrientationInfo orie |
147 | 155 | } |
148 | 156 |
|
149 | 157 | 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()); |
151 | 161 | if (standard != null) { |
152 | 162 | return standard; |
153 | 163 | } |
154 | 164 |
|
| 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 | + |
155 | 200 | byte[] data = Files.readAllBytes(imagePath); |
156 | 201 | Image toolkitImage; |
157 | 202 | try { |
@@ -193,6 +238,79 @@ private static BufferedImage readImage(Path imagePath) throws IOException { |
193 | 238 | return result; |
194 | 239 | } |
195 | 240 |
|
| 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 | + |
196 | 314 | private static void writeEntry(ZipOutputStream zos, String name, BufferedImage image) throws IOException { |
197 | 315 | ZipEntry entry = new ZipEntry(name); |
198 | 316 | zos.putNextEntry(entry); |
|
0 commit comments