2020import static java .lang .Math .max ;
2121import static java .lang .Math .min ;
2222
23- import java .awt .image .BufferedImage ;
2423import java .io .ByteArrayInputStream ;
25- import java .io .IOException ;
2624import java .security .MessageDigest ;
2725import java .util .Formatter ;
28- import java .util .Iterator ;
2926import java .util .Map ;
3027import java .util .Objects ;
3128import 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. */
3731class 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 );
0 commit comments