• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

hazendaz / smartsprites / #43

11 Nov 2023 07:47PM UTC coverage: 88.431%. Remained the same
#43

push

github

hazendaz
[tests] Fix tests given they expected order and now license on everything for +36

1437 of 1625 relevant lines covered (88.43%)

0.88 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.26
/src/main/java/org/carrot2/labs/smartsprites/SpriteImageBuilder.java
1
/*
2
 * SmartSprites Project
3
 *
4
 * Copyright (C) 2007-2009, Stanisław Osiński.
5
 * All rights reserved.
6
 *
7
 * Redistribution and use in source and binary forms, with or without modification,
8
 * are permitted provided that the following conditions are met:
9
 *
10
 * - Redistributions of  source code must  retain the above  copyright notice, this
11
 *   list of conditions and the following disclaimer.
12
 *
13
 * - Redistributions in binary form must reproduce the above copyright notice, this
14
 *   list of conditions and the following  disclaimer in  the documentation  and/or
15
 *   other materials provided with the distribution.
16
 *
17
 * - Neither the name of the SmartSprites Project nor the names of its contributors
18
 *   may  be used  to endorse  or  promote  products derived   from  this  software
19
 *   without specific prior written permission.
20
 *
21
 * - We kindly request that you include in the end-user documentation provided with
22
 *   the redistribution and/or in the software itself an acknowledgement equivalent
23
 *   to  the  following: "This product includes software developed by the SmartSprites
24
 *   Project."
25
 *
26
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"  AND
27
 * ANY EXPRESS OR  IMPLIED WARRANTIES, INCLUDING,  BUT NOT LIMITED  TO, THE IMPLIED
28
 * WARRANTIES  OF  MERCHANTABILITY  AND  FITNESS  FOR  A  PARTICULAR  PURPOSE   ARE
29
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE  FOR
30
 * ANY DIRECT, INDIRECT, INCIDENTAL,  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL  DAMAGES
31
 * (INCLUDING, BUT  NOT LIMITED  TO, PROCUREMENT  OF SUBSTITUTE  GOODS OR SERVICES;
32
 * LOSS OF USE, DATA, OR PROFITS;  OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND  ON
33
 * ANY  THEORY  OF  LIABILITY,  WHETHER  IN  CONTRACT,  STRICT  LIABILITY,  OR TORT
34
 * (INCLUDING NEGLIGENCE OR OTHERWISE)  ARISING IN ANY WAY  OUT OF THE USE  OF THIS
35
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
36
 */
37
package org.carrot2.labs.smartsprites;
38

39
import java.awt.image.BufferedImage;
40
import java.io.ByteArrayOutputStream;
41
import java.io.IOException;
42
import java.io.InputStream;
43
import java.io.OutputStream;
44
import java.util.Collection;
45
import java.util.Collections;
46
import java.util.Date;
47
import java.util.HashMap;
48
import java.util.LinkedHashMap;
49
import java.util.Map;
50

51
import javax.imageio.ImageIO;
52

53
import org.apache.commons.math3.util.ArithmeticUtils;
54
import org.carrot2.labs.smartsprites.SpriteImageDirective.SpriteImageFormat;
55
import org.carrot2.labs.smartsprites.SpriteImageDirective.SpriteImageLayout;
56
import org.carrot2.labs.smartsprites.SpriteLayoutProperties.SpriteAlignment;
57
import org.carrot2.labs.smartsprites.message.Message.MessageType;
58
import org.carrot2.labs.smartsprites.message.MessageLog;
59
import org.carrot2.labs.smartsprites.resource.ResourceHandler;
60
import org.carrot2.util.BufferedImageUtils;
61
import org.carrot2.util.FileUtils;
62

63
import com.google.common.collect.LinkedListMultimap;
64
import com.google.common.collect.Multimap;
65
import com.google.common.io.Closeables;
66

67
/**
68
 * Lays out and builds sprite images based on the collected SmartSprites directives.
69
 */
70
public class SpriteImageBuilder
71
{
72
    /** This builder's configuration */
73
    public final SmartSpritesParameters parameters;
74

75
    /** This builder's message log */
76
    private final MessageLog messageLog;
77

78
    /** Image merger for this builder */
79
    private SpriteImageRenderer spriteImageRenderer;
80

81
    /** The resource handler */
82
    private ResourceHandler resourceHandler;
83

84
    /**
85
     * A timestamp to use for timestamp-based sprite image UIDs. We need this time stamp
86
     * as a field to make sure the timestamp is the same for all sprite image
87
     * replacements.
88
     */
89
    private String timestamp;
90

91
    /**
92
     * Creates a {@link SpriteImageBuilder} with the provided parameters and log.
93
     */
94
    SpriteImageBuilder(SmartSpritesParameters parameters, MessageLog messageLog,
95
        ResourceHandler resourceHandler)
96
    {
1✔
97
        this.messageLog = messageLog;
1✔
98
        this.parameters = parameters;
1✔
99
        this.resourceHandler = resourceHandler;
1✔
100
        spriteImageRenderer = new SpriteImageRenderer(parameters, messageLog);
1✔
101
    }
1✔
102

103
    /**
104
     * Builds all sprite images based on the collected directives.
105
     * @throws IOException 
106
     */
107
    Multimap<String, SpriteReferenceReplacement> buildSpriteImages(
108
        Map<String, SpriteImageOccurrence> spriteImageOccurrencesBySpriteId,
109
        Multimap<String, SpriteReferenceOccurrence> spriteReferenceOccurrencesBySpriteId) throws IOException
110
    {
111
        timestamp = Long.toString(new Date().getTime());
1✔
112

113
        final Multimap<String, SpriteReferenceReplacement> spriteReplacementsByFile = LinkedListMultimap
114
            .create();
1✔
115
        for (final Map.Entry<String, Collection<SpriteReferenceOccurrence>> spriteReferenceOccurrences : spriteReferenceOccurrencesBySpriteId
1✔
116
            .asMap().entrySet())
1✔
117
        {
118
            final Map<SpriteReferenceOccurrence, SpriteReferenceReplacement> spriteReferenceReplacements = buildSpriteReplacements(
1✔
119
                spriteImageOccurrencesBySpriteId.get(spriteReferenceOccurrences.getKey()),
1✔
120
                spriteReferenceOccurrences.getValue());
1✔
121

122
            for (final SpriteReferenceReplacement spriteReferenceReplacement : spriteReferenceReplacements
1✔
123
                .values())
1✔
124
            {
125
                spriteReplacementsByFile.put(
1✔
126
                    spriteReferenceReplacement.spriteReferenceOccurrence.cssFile,
127
                    spriteReferenceReplacement);
128
            }
1✔
129
        }
1✔
130

131
        return spriteReplacementsByFile;
1✔
132
    }
133

134
    /**
135
     * Builds sprite image for a single sprite image directive.
136
     * @throws IOException 
137
     */
138
    Map<SpriteReferenceOccurrence, SpriteReferenceReplacement> buildSpriteReplacements(
139
        SpriteImageOccurrence spriteImageOccurrence,
140
        Collection<SpriteReferenceOccurrence> spriteReferenceOccurrences) throws IOException
141
    {
142
        // Load images into memory. TODO: impose some limit here?
143
        final Map<SpriteReferenceOccurrence, BufferedImage> images = new LinkedHashMap<>();
1✔
144
        for (final SpriteReferenceOccurrence spriteReferenceOccurrence : spriteReferenceOccurrences)
1✔
145
        {
146
            messageLog.setCssFile(spriteReferenceOccurrence.cssFile);
1✔
147
            messageLog.setLine(spriteReferenceOccurrence.line);
1✔
148

149
            final String realImagePath = resourceHandler.getResourcePath(
1✔
150
                spriteReferenceOccurrence.cssFile, spriteReferenceOccurrence.imagePath);
151
            InputStream is = null;
1✔
152
            try
153
            {
154
                is = resourceHandler.getResourceAsInputStream(realImagePath);
1✔
155

156
                // Load image
157
                if (is != null)
1✔
158
                {
159
                    messageLog.info(MessageType.READING_IMAGE, realImagePath);
1✔
160
                    final BufferedImage image = ImageIO.read(is);
1✔
161
                    if (image != null)
1✔
162
                    {
163
                        images.put(spriteReferenceOccurrence, image);
1✔
164
                    }
165
                    else
166
                    {
167
                        messageLog.warning(
1✔
168
                            MessageType.UNSUPPORTED_INDIVIDUAL_IMAGE_FORMAT,
169
                            realImagePath);
170
                    }
171
                }
1✔
172
                else
173
                {
174
                    messageLog.warning(MessageType.CANNOT_NOT_LOAD_IMAGE, realImagePath,
×
175
                        "Can't read input file!");
176
                    continue;
177
                }
178
            }
179
            catch (final IOException e)
1✔
180
            {
181
                messageLog.warning(MessageType.CANNOT_NOT_LOAD_IMAGE, realImagePath,
1✔
182
                    "Can't read input file!");
183
                continue;
184
            }
185
            finally
186
            {
187
                Closeables.close(is, true);
1✔
188
            }
189

190
            messageLog.setCssFile(null);
1✔
191
        }
1✔
192

193
        // Build the sprite image bitmap
194
        final SpriteImage spriteImage = SpriteImageBuilder.buildSpriteImage(
1✔
195
            spriteImageOccurrence, images, messageLog);
196
        if (spriteImage == null)
1✔
197
        {
198
            return Collections
1✔
199
                .<SpriteReferenceOccurrence, SpriteReferenceReplacement> emptyMap();
1✔
200
        }
201

202
        // Render the sprite into the required formats, perform quantization if needed
203
        final BufferedImage [] mergedImages = spriteImageRenderer.render(spriteImage);
1✔
204

205
        writeSprite(spriteImage, mergedImages[0], false);
1✔
206
        if (mergedImages[1] != null)
1✔
207
        {
208
            // Write IE6 version if generated
209
            writeSprite(spriteImage, mergedImages[1], true);
1✔
210
        }
211

212
        return spriteImage.spriteReferenceReplacements;
1✔
213
    }
214

215
    /**
216
     * Writes sprite image to the disk.
217
     * @throws IOException 
218
     */
219
    private void writeSprite(SpriteImage spriteImage, final BufferedImage mergedImage,
220
        boolean ie6Reduced) throws IOException
221
    {
222
        final SpriteImageOccurrence spriteImageOccurrence = spriteImage.spriteImageOccurrence;
1✔
223
        final SpriteImageDirective spriteImageDirective = spriteImageOccurrence.spriteImageDirective;
1✔
224

225
        // Write the image to a byte array first. We need the data to compute an sha512 hash.
226
        final ByteArrayOutputStream spriteImageByteArrayOutputStream = new ByteArrayOutputStream();
1✔
227

228
        // If writing to a JPEG, we need to make a 3-byte-encoded image
229
        final BufferedImage imageToWrite;
230
        if (SpriteImageFormat.JPG.equals(spriteImageDirective.format))
1✔
231
        {
232
            imageToWrite = new BufferedImage(mergedImage.getWidth(),
×
233
                mergedImage.getHeight(), BufferedImage.TYPE_3BYTE_BGR);
×
234
            BufferedImageUtils.drawImage(mergedImage, imageToWrite, 0, 0);
×
235
        }
236
        else
237
        {
238
            imageToWrite = mergedImage;
1✔
239
        }
240

241
        try
242
        {
243
            ImageIO.write(imageToWrite, spriteImageDirective.format.toString(),
1✔
244
                spriteImageByteArrayOutputStream);
245
        }
246
        catch (IOException e)
×
247
        {
248
            // Unlikely to happen.
249
            messageLog.warning(MessageType.CANNOT_WRITE_SPRITE_IMAGE,
×
250
                spriteImageDirective.imagePath, e.getMessage());
×
251
        }
1✔
252

253
        // Build file name
254
        byte [] spriteImageBytes = spriteImageByteArrayOutputStream.toByteArray();
1✔
255
        String resolvedImagePath = spriteImage.resolveImagePath(spriteImageBytes,
1✔
256
            timestamp, ie6Reduced);
257
        if (resolvedImagePath.indexOf('?') >= 0)
1✔
258
        {
259
            resolvedImagePath = resolvedImagePath.substring(0,
1✔
260
                resolvedImagePath.indexOf('?'));
1✔
261
        }
262

263
        // Save the image to the disk
264
        final String mergedImageFile = getImageFile(spriteImageOccurrence.cssFile,
1✔
265
            resolvedImagePath);
266

267
        OutputStream spriteImageOutputStream = null;
1✔
268
        try
269
        {
270
            messageLog.info(MessageType.WRITING_SPRITE_IMAGE, mergedImage.getWidth(),
1✔
271
                mergedImage.getHeight(), spriteImageDirective.spriteId, mergedImageFile);
1✔
272
            spriteImageOutputStream = resourceHandler
1✔
273
                .getResourceAsOutputStream(mergedImageFile);
1✔
274

275
            spriteImageOutputStream.write(spriteImageBytes);
1✔
276
        }
277
        catch (final IOException e)
×
278
        {
279
            messageLog.warning(MessageType.CANNOT_WRITE_SPRITE_IMAGE, mergedImageFile,
×
280
                e.getMessage());
×
281
        }
282
        finally
283
        {
284
            Closeables.close(spriteImageOutputStream, true);
1✔
285
        }
286
    }
1✔
287

288
    /**
289
     * Computes the image path. If the imagePath is relative, it's taken relative to the
290
     * cssFile. If imagePath is absolute (starts with '/') and documentRootDir is not
291
     * null, it's taken relative to documentRootDir.
292
     */
293
    String getImageFile(String cssFile, String imagePath)
294
    {
295
        // Absolute path resolution is done by resourceHandler
296
        final String path = resourceHandler.getResourcePath(cssFile, imagePath);
1✔
297

298
        // Just handle the root directory changing
299
        if (!imagePath.startsWith("/") && parameters.hasOutputDir())
1✔
300
        {
301
            return FileUtils.changeRoot(path, parameters.getRootDir(),
1✔
302
                parameters.getOutputDir());
1✔
303
        }
304
        else
305
        {
306
            return path;
1✔
307
        }
308
    }
309

310
    /**
311
     * Calculates total dimensions and lays out a single sprite image.
312
     */
313
    static SpriteImage buildSpriteImage(SpriteImageOccurrence spriteImageOccurrence,
314
        Map<SpriteReferenceOccurrence, BufferedImage> images, MessageLog messageLog)
315
    {
316
        // First find the least common multiple of the images with 'repeat' alignment
317
        final SpriteImageLayout layout = spriteImageOccurrence.spriteImageDirective.layout;
1✔
318
        final float spriteScale = spriteImageOccurrence.spriteImageDirective.scaleRatio;
1✔
319
        final int leastCommonMultiple = SpriteImageBuilder.calculateLeastCommonMultiple(
1✔
320
            images, layout);
321

322
        // Compute sprite dimension (width for vertical, height for horizontal sprites)
323
        final boolean vertical = layout.equals(SpriteImageLayout.VERTICAL);
1✔
324
        int dimension = leastCommonMultiple;
1✔
325
        for (final Map.Entry<SpriteReferenceOccurrence, BufferedImage> entry : images
1✔
326
            .entrySet())
1✔
327
        {
328
            final BufferedImage image = entry.getValue();
1✔
329
            final SpriteReferenceOccurrence spriteReferenceOccurrence = entry.getKey();
1✔
330

331
            // Compute dimensions
332
            dimension = Math.max(dimension,
1✔
333
                vertical ? spriteReferenceOccurrence.getRequiredWidth(image, layout)
1✔
334
                    : spriteReferenceOccurrence.getRequiredHeight(image, layout));
1✔
335
        }
1✔
336

337
        // Correct for least common multiple
338
        if (dimension % leastCommonMultiple != 0)
1✔
339
        {
340
            dimension += leastCommonMultiple - (dimension % leastCommonMultiple);
1✔
341
        }
342

343
        // Compute the other sprite dimension.
344
        int currentOffset = 0;
1✔
345
        final Map<SpriteReferenceOccurrence, SpriteReferenceReplacement> spriteReplacements = new LinkedHashMap<>();
1✔
346
        final Map<BufferedImageEqualsWrapper, Integer> renderedImageToOffset = new LinkedHashMap<>();
1✔
347
        for (final Map.Entry<SpriteReferenceOccurrence, BufferedImage> entry : images
1✔
348
            .entrySet())
1✔
349
        {
350
            final SpriteReferenceOccurrence spriteReferenceOccurrence = entry.getKey();
1✔
351
            final BufferedImage image = entry.getValue();
1✔
352

353
            final BufferedImage rendered = spriteReferenceOccurrence.render(image,
1✔
354
                layout, dimension);
355
            final BufferedImageEqualsWrapper imageWrapper = new BufferedImageEqualsWrapper(
1✔
356
                rendered);
357
            Integer imageOffset = renderedImageToOffset.get(imageWrapper);
1✔
358
            if (imageOffset == null)
1✔
359
            {
360
                // Draw a new image
361
                imageOffset = currentOffset;
1✔
362
                renderedImageToOffset.put(imageWrapper, imageOffset);
1✔
363
                currentOffset += vertical ? rendered.getHeight() : rendered.getWidth();
1✔
364
            }
365

366
            final float scaledImageWidth = spriteReferenceOccurrence.getRequiredWidth(image, layout) / spriteScale;
1✔
367
            final float scaledImageHeight = spriteReferenceOccurrence.getRequiredHeight(image, layout) / spriteScale;
1✔
368
            if (Math.round(scaledImageWidth) != scaledImageWidth ||
1✔
369
                Math.round(scaledImageHeight) != scaledImageHeight)
1✔
370
            {
371
                messageLog.warning(MessageType.IMAGE_FRACTIONAL_SCALE_VALUE,
1✔
372
                    spriteReferenceOccurrence.imagePath, scaledImageWidth, scaledImageHeight);
1✔
373
            }
374

375
            final int adjustedImageOffset = Math.round(imageOffset / spriteScale);
1✔
376
            spriteReplacements.put(spriteReferenceOccurrence,
1✔
377
                spriteReferenceOccurrence.buildReplacement(layout, adjustedImageOffset));
1✔
378
        }
1✔
379

380
        // Render the sprite image and build sprite reference replacements
381
        final int spriteWidth = vertical ? dimension : currentOffset;
1✔
382
        final int spriteHeight = vertical ? currentOffset : dimension;
1✔
383
        if (spriteWidth == 0 || spriteHeight == 0)
1✔
384
        {
385
            return null;
1✔
386
        }
387

388
        final float scaledWidth = spriteWidth / spriteScale;
1✔
389
        final float scaledHeight = spriteHeight / spriteScale;
1✔
390
        if (Math.round(scaledWidth) != scaledWidth ||
1✔
391
            Math.round(scaledHeight) != scaledHeight)
1✔
392
        {
393
            messageLog.warning(MessageType.FRACTIONAL_SCALE_VALUE,
1✔
394
                spriteImageOccurrence.spriteImageDirective.spriteId, scaledWidth, scaledHeight);
1✔
395
        }
396

397
        final BufferedImage sprite = new BufferedImage(spriteWidth, spriteHeight,
1✔
398
            BufferedImage.TYPE_4BYTE_ABGR);
399

400
        for (final Map.Entry<BufferedImageEqualsWrapper, Integer> entry : renderedImageToOffset
1✔
401
            .entrySet())
1✔
402
        {
403

404
            BufferedImageUtils.drawImage(entry.getKey().image, sprite, vertical ? 0
1✔
405
                : entry.getValue(), vertical ? entry.getValue() : 0);
1✔
406
        }
1✔
407

408
        return new SpriteImage(sprite, spriteImageOccurrence, spriteReplacements, spriteWidth, spriteHeight, spriteScale);
1✔
409
    }
410

411
    /**
412
     * Calculates the width/ height of "repeated" sprites.
413
     */
414
    static int calculateLeastCommonMultiple(
415
        Map<SpriteReferenceOccurrence, BufferedImage> images, SpriteImageLayout layout)
416
    {
417
        int leastCommonMultiple = 1;
1✔
418
        for (final Map.Entry<SpriteReferenceOccurrence, BufferedImage> entry : images
1✔
419
            .entrySet())
1✔
420
        {
421
            final BufferedImage image = entry.getValue();
1✔
422
            final SpriteReferenceOccurrence spriteReferenceOccurrence = entry.getKey();
1✔
423
            if (image != null
1✔
424
                && SpriteAlignment.REPEAT
425
                    .equals(spriteReferenceOccurrence.spriteReferenceDirective.spriteLayoutProperties.alignment))
1✔
426
            {
427
                if (SpriteImageLayout.VERTICAL.equals(layout))
1✔
428
                {
429
                    leastCommonMultiple = ArithmeticUtils.lcm(leastCommonMultiple,
1✔
430
                        spriteReferenceOccurrence.getRequiredWidth(image, layout));
1✔
431
                }
432
                else
433
                {
434
                    leastCommonMultiple = ArithmeticUtils.lcm(leastCommonMultiple,
1✔
435
                        spriteReferenceOccurrence.getRequiredHeight(image, layout));
1✔
436
                }
437
            }
438
        }
1✔
439
        return leastCommonMultiple;
1✔
440
    }
441

442
    /**
443
     * Groups {@link SpriteReferenceReplacement}s by the line number of their
444
     * corresponding directives.
445
     */
446
    static Map<Integer, SpriteReferenceReplacement> getSpriteReplacementsByLineNumber(
447
        Collection<SpriteReferenceReplacement> spriteReferenceReplacements)
448
    {
449
        final Map<Integer, SpriteReferenceReplacement> result = new HashMap<>();
1✔
450

451
        for (final SpriteReferenceReplacement spriteReferenceReplacement : spriteReferenceReplacements)
1✔
452
        {
453
            result.put(spriteReferenceReplacement.spriteReferenceOccurrence.line,
1✔
454
                spriteReferenceReplacement);
455
        }
1✔
456

457
        return result;
1✔
458
    }
459

460
    /**
461
     * Groups {@link SpriteImageOccurrence}s by the line number of their corresponding
462
     * directives.
463
     */
464
    static Map<Integer, SpriteImageOccurrence> getSpriteImageOccurrencesByLineNumber(
465
        Collection<SpriteImageOccurrence> spriteImageOccurrences)
466
    {
467
        final Map<Integer, SpriteImageOccurrence> result = new HashMap<>();
1✔
468

469
        for (final SpriteImageOccurrence spriteImageOccurrence : spriteImageOccurrences)
1✔
470
        {
471
            result.put(spriteImageOccurrence.line, spriteImageOccurrence);
1✔
472
        }
1✔
473

474
        return result;
1✔
475
    }
476

477
    /**
478
     * A wrapper that implements content-aware {@link Object#equals(Object)} and
479
     * {@link Object#hashCode()} on {@link BufferedImage}s.
480
     */
481
    static final class BufferedImageEqualsWrapper
482
    {
483
        BufferedImage image;
484

485
        BufferedImageEqualsWrapper(BufferedImage image)
486
        {
1✔
487
            this.image = image;
1✔
488
        }
1✔
489

490
        @Override
491
        public boolean equals(Object obj)
492
        {
493
            if (!(obj instanceof BufferedImageEqualsWrapper))
1✔
494
            {
495
                return false;
×
496
            }
497

498
            if (obj == this)
1✔
499
            {
500
                return true;
×
501
            }
502

503
            final BufferedImage other = ((BufferedImageEqualsWrapper) obj).image;
1✔
504

505
            boolean equal = other.getWidth() == image.getWidth()
1✔
506
                && other.getHeight() == image.getHeight()
1✔
507
                && other.getType() == image.getType();
1✔
508

509
            if (equal)
1✔
510
            {
511
                for (int y = 0; y < image.getHeight(); y++)
1✔
512
                {
513
                    for (int x = 0; x < image.getWidth(); x++)
1✔
514
                    {
515
                        if (ignoreFullTransparency(image.getRGB(x, y)) != ignoreFullTransparency(other
1✔
516
                            .getRGB(x, y)))
1✔
517
                        {
518
                            return false;
1✔
519
                        }
520
                    }
521
                }
522
            }
523

524
            return equal;
1✔
525
        }
526

527
        @Override
528
        public int hashCode()
529
        {
530
            if (image == null)
1✔
531
            {
532
                return 0;
×
533
            }
534

535
            int hash = image.getWidth() ^ (image.getHeight() << 16);
1✔
536

537
            // Computes the hashCode based on an 4 x 4 to 7 x 7 grid of image's pixels
538
            final int xIncrement = image.getWidth() > 7 ? image.getWidth() >> 2 : 1;
1✔
539
            final int yIncrement = image.getHeight() > 7 ? image.getHeight() >> 2 : 1;
1✔
540

541
            for (int y = 0; y < image.getHeight(); y += yIncrement)
1✔
542
            {
543
                for (int x = 0; x < image.getWidth(); x += xIncrement)
1✔
544
                {
545
                    hash ^= ignoreFullTransparency(image.getRGB(x, y));
1✔
546
                }
547
            }
548

549
            return hash;
1✔
550
        }
551

552
        /**
553
         * If the pixel is fully transparent, returns 0. Otherwise, returns the pixel.
554
         * This is useful in {@link #equals(Object)} and {@link #hashCode()} to ignore
555
         * pixels that have different colors but are invisible anyway because of full
556
         * transparency.
557
         */
558
        private static int ignoreFullTransparency(int pixel)
559
        {
560
            if ((pixel & 0xff000000) == 0x00000000)
1✔
561
            {
562
                return 0;
1✔
563
            }
564
            else
565
            {
566
                return pixel;
1✔
567
            }
568
        }
569
    }
570
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc