Skip to content

Commit 16a792f

Browse files
Move image processing to a separate interface with implementation. (#95)
1 parent 6290f83 commit 16a792f

File tree

8 files changed

+190
-101
lines changed

8 files changed

+190
-101
lines changed

play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/DrawableResourceDetails.java

Lines changed: 74 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -20,39 +20,33 @@
2020
import static java.lang.Math.max;
2121
import static java.lang.Math.min;
2222

23-
import java.awt.image.BufferedImage;
2423
import java.io.ByteArrayInputStream;
25-
import java.io.IOException;
2624
import java.security.MessageDigest;
2725
import java.util.Formatter;
28-
import java.util.Iterator;
2926
import java.util.Map;
3027
import java.util.Objects;
3128
import java.util.Optional;
32-
import javax.imageio.ImageIO;
33-
import javax.imageio.ImageReader;
34-
import javax.imageio.stream.ImageInputStream;
3529

3630
/** Details about a drawable resource that are relevant for the memory footprint calculation. */
3731
class DrawableResourceDetails {
3832
private static final int CHANNEL_MASK_A = 0xff000000;
3933

4034
/**
41-
* A lookup table used for computing the loss of precision whern an 8bit value is qualtized to a
35+
* A lookup table used for computing the loss of precision where an 8bit value is quantized to a
4236
* 5bit value.
4337
*/
44-
private static final int QUANTZATION_ERROR_LUT5[] =
45-
create8bppToNbppQualtizationErrorLookUpTable(5);
38+
private static final int QUANTIZATION_ERROR_LUT5[] =
39+
create8bppToNbppQuantizationErrorLookUpTable(5);
4640

4741
/**
48-
* A lookup table used for computing the loss of precision whern an 8bit value is qualtized to a
42+
* A lookup table used for computing the loss of precision where an 8bit value is quantized to a
4943
* 6bit value.
5044
*/
51-
private static final int QUANTZATION_ERROR_LUT6[] =
52-
create8bppToNbppQualtizationErrorLookUpTable(6);
45+
private static final int QUANTIZATION_ERROR_LUT6[] =
46+
create8bppToNbppQuantizationErrorLookUpTable(6);
5347

5448
/** This corresponds to an average difference in luminosity of 5/10th of an 8bit value. */
55-
private static final double MAX_ACCEPTIABLE_QUANTIZATION_ERROR = 0.5f;
49+
private static final double MAX_ACCEPTABLE_QUANTIZATION_ERROR = 0.5f;
5650

5751
public static class Bounds {
5852
int left;
@@ -123,11 +117,13 @@ public java.lang.String toString() {
123117
* take in its uncompressed format.
124118
*
125119
* @param resource the resource from a watch face package.
120+
* @param imageProcessor the image processing implementation.
126121
* @return the memory footprint of that asset file or {@code Optional.empty()} if the file is
127122
* not a drawable asset.
128123
* @throws java.lang.IllegalArgumentException when the image cannot be processed.
129124
*/
130-
static Optional<DrawableResourceDetails> fromPackageResource(AndroidResource resource) {
125+
static Optional<DrawableResourceDetails> fromPackageResource(
126+
AndroidResource resource, ImageProcessor imageProcessor) {
131127
// For fonts we assume the raw size of the resource is the MCU footprint.
132128
if (resource.isFont()) {
133129
return Optional.of(
@@ -153,58 +149,54 @@ static Optional<DrawableResourceDetails> fromPackageResource(AndroidResource res
153149
String.format("Error while processing image %s", resource.getFilePath()), e);
154150
}
155151

156-
try (ImageInputStream imageInputStream =
157-
ImageIO.createImageInputStream(new ByteArrayInputStream(resource.getData()))) {
158-
Iterator<ImageReader> imageReaders = ImageIO.getImageReaders(imageInputStream);
159-
if (!imageReaders.hasNext()) {
160-
return Optional.empty();
161-
}
162-
ImageReader reader = imageReaders.next();
163-
reader.setInput(imageInputStream);
164-
// allowSearch forces the reader to return the true number of images even if the file
165-
// format does not specify it, requiring an exhaustive search.
166-
int numberOfImages = reader.getNumImages(/* allowSearch= */ true);
167-
int maxWidth = 0;
168-
int maxHeight = 0;
169-
double maxQuantizationError = 0.0;
170-
DrawableResourceDetails.Bounds accumulatedBounds = null;
171-
for (int i = 0; i < numberOfImages; i++) {
172-
// If an asset such as a GIF or a WEBP has more than 1 frame, then find the
173-
// maximum size for any frame and multiply that by the number of frames.
174-
maxWidth = max(maxWidth, reader.getWidth(i));
175-
maxHeight = max(maxHeight, reader.getHeight(i));
176-
177-
BufferedImage image = reader.read(i);
178-
Bounds bounds = computeBounds(image);
179-
180-
if (bounds != null) {
181-
if (accumulatedBounds == null) {
182-
accumulatedBounds = bounds;
183-
} else {
184-
accumulatedBounds = accumulatedBounds.computeUnion(bounds);
185-
}
186-
}
152+
ImageProcessor.ImageReader reader =
153+
imageProcessor.createImageReader(new ByteArrayInputStream(resource.getData()));
154+
155+
if (reader == null) {
156+
return Optional.empty();
157+
}
187158

188-
QualtizationStats stats = computeQualtizationStats(image);
189-
maxQuantizationError = max(maxQuantizationError, stats.getVisibleError());
159+
int numberOfImages = reader.getNumImages();
160+
int maxWidth = 0;
161+
int maxHeight = 0;
162+
double maxQuantizationError = 0.0;
163+
DrawableResourceDetails.Bounds accumulatedBounds = null;
164+
165+
for (int i = 0; i < numberOfImages; i++) {
166+
// If an asset such as a GIF or a WEBP has more than 1 frame, then find the
167+
// maximum size for any frame and multiply that by the number of frames.
168+
maxWidth = max(maxWidth, reader.getWidth(i));
169+
maxHeight = max(maxHeight, reader.getHeight(i));
170+
171+
ImageProcessor.ImageData image = reader.read(i);
172+
Bounds bounds = computeBounds(image);
173+
174+
if (bounds != null) {
175+
if (accumulatedBounds == null) {
176+
accumulatedBounds = bounds;
177+
} else {
178+
accumulatedBounds = accumulatedBounds.computeUnion(bounds);
179+
}
190180
}
191-
long biggestFrameMemoryFootprint = ((long) maxWidth) * maxHeight * 4;
192-
boolean canBeQuantized = (maxQuantizationError < MAX_ACCEPTIABLE_QUANTIZATION_ERROR);
193-
return Optional.of(
194-
new Builder()
195-
.setName(resource.getResourceName())
196-
.setNumberOfImages(numberOfImages)
197-
.setBiggestFrameFootprintBytes(biggestFrameMemoryFootprint)
198-
.setBounds(accumulatedBounds)
199-
.setWidth(maxWidth)
200-
.setHeight(maxHeight)
201-
.setSha1(sha1)
202-
.setCanUseRGB565(canBeQuantized)
203-
.build());
204-
} catch (IOException e) {
205-
throw new IllegalArgumentException(
206-
String.format("Error while processing image %s", resource.getFilePath()), e);
181+
182+
QuantizationStats stats = computeQualtizationStats(image);
183+
maxQuantizationError = max(maxQuantizationError, stats.getVisibleError());
207184
}
185+
186+
long biggestFrameMemoryFootprint = ((long) maxWidth) * maxHeight * 4;
187+
boolean canBeQuantized = (maxQuantizationError < MAX_ACCEPTABLE_QUANTIZATION_ERROR);
188+
189+
return Optional.of(
190+
new Builder()
191+
.setName(resource.getResourceName())
192+
.setNumberOfImages(numberOfImages)
193+
.setBiggestFrameFootprintBytes(biggestFrameMemoryFootprint)
194+
.setBounds(accumulatedBounds)
195+
.setWidth(maxWidth)
196+
.setHeight(maxHeight)
197+
.setSha1(sha1)
198+
.setCanUseRGB565(canBeQuantized)
199+
.build());
208200
}
209201

210202
private final String name;
@@ -437,10 +429,10 @@ public DrawableResourceDetails build() {
437429
* Reads the image with the specified index and then computes the {@link Bounds} of the visible
438430
* pixels.
439431
*
440-
* @param image the {@link BufferedImage}
432+
* @param image the {@link ImageProcessor.ImageData}
441433
* @return The {@link Bounds} of the visible pixels.
442434
*/
443-
private static Bounds computeBounds(BufferedImage image) {
435+
private static Bounds computeBounds(ImageProcessor.ImageData image) {
444436
Bounds bounds = new Bounds();
445437

446438
// Scan from the top down to find the first non-transparent row.
@@ -454,11 +446,11 @@ private static Bounds computeBounds(BufferedImage image) {
454446
}
455447

456448
if (y == height) {
457-
// The image is fully transparent!
449+
// The image is fully transparent.
458450
return null;
459451
}
460452

461-
// Scan from the bottum up to find the first non-transparent row.
453+
// Scan from the bottom up to find the first non-transparent row.
462454
for (y = height; y > 0; ) {
463455
y--;
464456
if (!isRowFullyTransparent(image, y)) {
@@ -488,20 +480,20 @@ private static Bounds computeBounds(BufferedImage image) {
488480
return bounds;
489481
}
490482

491-
private static boolean isRowFullyTransparent(BufferedImage image, int y) {
483+
private static boolean isRowFullyTransparent(ImageProcessor.ImageData image, int y) {
492484
int width = image.getWidth();
493485
for (int x = 0; x < width; x++) {
494-
if (!isFullyTransparent(image.getRGB(x, y))) {
486+
if (!isFullyTransparent(image.getRgb(x, y))) {
495487
return false;
496488
}
497489
}
498490
return true;
499491
}
500492

501493
private static boolean isColumnFullyTransparent(
502-
BufferedImage image, int x, int top, int bottom) {
494+
ImageProcessor.ImageData image, int x, int top, int bottom) {
503495
for (int y = top; y < bottom; y++) {
504-
if (!isFullyTransparent(image.getRGB(x, y))) {
496+
if (!isFullyTransparent(image.getRgb(x, y))) {
505497
return false;
506498
}
507499
}
@@ -512,7 +504,7 @@ private static boolean isFullyTransparent(int argb) {
512504
return (argb & CHANNEL_MASK_A) == 0;
513505
}
514506

515-
private static class QualtizationStats {
507+
private static class QuantizationStats {
516508
long visiblePixels = 0;
517509
long visiblePixelQuantizationErrorSum = 0;
518510

@@ -521,34 +513,34 @@ private static class QualtizationStats {
521513
}
522514
}
523515

524-
private static QualtizationStats computeQualtizationStats(BufferedImage image) {
516+
private static QuantizationStats computeQualtizationStats(ImageProcessor.ImageData image) {
525517
int width = image.getWidth();
526518
int height = image.getHeight();
527-
QualtizationStats qualtizationStats = new QualtizationStats();
519+
QuantizationStats quantizationStats = new QuantizationStats();
528520
for (int y = 0; y < height; y++) {
529521
for (int x = 0; x < width; x++) {
530-
int argb = image.getRGB(x, y);
522+
int argb = image.getRgb(x, y);
531523
int a = (argb >> 24) & 0xff;
532524
if (a < 255) {
533525
continue;
534526
}
535527

536-
qualtizationStats.visiblePixels++;
528+
quantizationStats.visiblePixels++;
537529

538530
int r = (argb >> 16) & 0xff;
539531
int g = (argb >> 8) & 0xff;
540532
int b = argb & 0xff;
541533

542-
qualtizationStats.visiblePixelQuantizationErrorSum += QUANTZATION_ERROR_LUT5[r];
543-
qualtizationStats.visiblePixelQuantizationErrorSum += QUANTZATION_ERROR_LUT6[g];
544-
qualtizationStats.visiblePixelQuantizationErrorSum += QUANTZATION_ERROR_LUT5[b];
534+
quantizationStats.visiblePixelQuantizationErrorSum += QUANTIZATION_ERROR_LUT5[r];
535+
quantizationStats.visiblePixelQuantizationErrorSum += QUANTIZATION_ERROR_LUT6[g];
536+
quantizationStats.visiblePixelQuantizationErrorSum += QUANTIZATION_ERROR_LUT5[b];
545537
}
546538
}
547-
return qualtizationStats;
539+
return quantizationStats;
548540
}
549541

550542
/** Constructs a table of the error introduced by quantizing an 8 bit value to a N bit value. */
551-
private static int[] create8bppToNbppQualtizationErrorLookUpTable(int n) {
543+
private static int[] create8bppToNbppQuantizationErrorLookUpTable(int n) {
552544
int[] table = new int[256];
553545
int bitsLost = 8 - n;
554546
int twoPowN = 1 << bitsLost;
@@ -557,7 +549,7 @@ private static int[] create8bppToNbppQualtizationErrorLookUpTable(int n) {
557549
// This rounds i to the nearest n-bit value before converting back to an 8 bit value.
558550
int quantizedValue = min(((i + halfTwoPowN) / twoPowN) * twoPowN, 255);
559551

560-
// Record the error due to qualtization in the table. This has a saw-tooth pattern where
552+
// Record the error due to quantization in the table. This has a saw-tooth pattern where
561553
// n-bit values that correspond directly to 8 bit ones have an error of 0, rising to a
562554
// maximum error of halfPlusOne in between.
563555
table[i] = abs(i - quantizedValue);
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package com.google.wear.watchface.dfx.memory
2+
3+
import java.io.IOException
4+
import java.io.InputStream
5+
import java.util.Optional
6+
7+
/** An interface for processing image data. */
8+
interface ImageProcessor {
9+
/** Holds the dimensions and pixel data of an image. */
10+
interface ImageData {
11+
/** Width of the image in pixels. */
12+
val width: Int
13+
14+
/** Height of the image in pixels. */
15+
val height: Int
16+
17+
/** Returns the RGB color of the pixel at the specified coordinates. */
18+
fun getRgb(x: Int, y: Int): Int
19+
}
20+
21+
/** A reader for decoding image files, including animated formats. */
22+
interface ImageReader {
23+
/** Returns the width of a specific image frame. */
24+
fun getWidth(imageIndex: Int): Int
25+
26+
/** Returns the height of a specific image frame. */
27+
fun getHeight(imageIndex: Int): Int
28+
29+
/** Total number of frames in the source. */
30+
val numImages: Int
31+
32+
/** Reads and decodes a specific image frame into an [ImageData] object. */
33+
fun read(imageIndex: Int): ImageData?
34+
}
35+
36+
/**
37+
* Creates an [ImageReader] for the given image input stream.
38+
*
39+
* @param stream The input stream of the image data.
40+
*/
41+
fun createImageReader(stream: InputStream): ImageReader?
42+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package com.google.wear.watchface.dfx.memory
2+
3+
import com.google.wear.watchface.dfx.memory.ImageProcessor.ImageData
4+
import java.io.InputStream
5+
import javax.imageio.ImageIO
6+
7+
/** JVM-specific implementation of ImageProcessor using AWT and ImageIO. */
8+
class JvmImageProcessor : ImageProcessor {
9+
override fun createImageReader(stream: InputStream): ImageProcessor.ImageReader? {
10+
val imageInputStream = ImageIO.createImageInputStream(stream)
11+
val imageReaders = ImageIO.getImageReaders(imageInputStream)
12+
13+
if (!imageReaders.hasNext()) {
14+
return null
15+
}
16+
17+
val reader = imageReaders.next()
18+
reader.input = imageInputStream
19+
20+
return object : ImageProcessor.ImageReader {
21+
override fun getWidth(imageIndex: Int) = reader.getWidth(imageIndex)
22+
23+
override fun getHeight(imageIndex: Int) = reader.getHeight(imageIndex)
24+
25+
override val numImages: Int = reader.getNumImages(true)
26+
27+
override fun read(imageIndex: Int): ImageData {
28+
val bufferedImage = reader.read(imageIndex)
29+
30+
return object : ImageData {
31+
override val width: Int = bufferedImage.width
32+
33+
override val height: Int = bufferedImage.height
34+
35+
override fun getRgb(x: Int, y: Int): Int {
36+
return bufferedImage.getRGB(x, y)
37+
}
38+
}
39+
}
40+
}
41+
}
42+
}

play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/ResourceMemoryEvaluator.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,9 @@ static List<MemoryFootprint> evaluateMemoryFootprint(EvaluationSettings evaluati
114114
try (InputPackage inputPackage = InputPackage.open(evaluationSettings.getWatchFacePath())) {
115115
WatchFaceData watchFaceData =
116116
WatchFaceData.fromResourcesStream(
117-
inputPackage.getWatchFaceFiles(), evaluationSettings);
117+
inputPackage.getWatchFaceFiles(),
118+
evaluationSettings,
119+
new JvmImageProcessor());
118120

119121
if (!evaluationSettings.isHoneyfaceMode()) {
120122
String wffVersion =

0 commit comments

Comments
 (0)