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

hazendaz / smartsprites / 555

21 May 2026 03:26PM UTC coverage: 87.799%. Remained the same
555

push

github

hazendaz
[tests] Fix tests that were looking at size of original license before spdx change

557 of 670 branches covered (83.13%)

Branch coverage included in aggregate %.

1350 of 1502 relevant lines covered (89.88%)

0.9 hits per line

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

87.8
/src/main/java/org/carrot2/labs/smartsprites/SpriteImageBuilder.java
1
/*
2
 * SPDX-License-Identifier: BSD-3-Clause
3
 * See LICENSE file for details.
4
 *
5
 * Copyright 2021-2026 Hazendaz
6
 * Copyright (C) 2007-2009, Stanisław Osiński.
7
 */
8
package org.carrot2.labs.smartsprites;
9

10
import com.google.common.collect.LinkedListMultimap;
11
import com.google.common.collect.Multimap;
12

13
import java.awt.image.BufferedImage;
14
import java.io.ByteArrayOutputStream;
15
import java.io.IOException;
16
import java.io.InputStream;
17
import java.io.OutputStream;
18
import java.time.Instant;
19
import java.util.Collection;
20
import java.util.HashMap;
21
import java.util.LinkedHashMap;
22
import java.util.Locale;
23
import java.util.Map;
24

25
import javax.imageio.ImageIO;
26

27
import org.apache.batik.transcoder.TranscoderException;
28
import org.apache.batik.transcoder.TranscoderInput;
29
import org.apache.batik.transcoder.TranscoderOutput;
30
import org.apache.batik.transcoder.image.ImageTranscoder;
31
import org.apache.commons.math3.util.ArithmeticUtils;
32
import org.carrot2.labs.smartsprites.SpriteImageDirective.SpriteImageFormat;
33
import org.carrot2.labs.smartsprites.SpriteImageDirective.SpriteImageLayout;
34
import org.carrot2.labs.smartsprites.SpriteLayoutProperties.SpriteAlignment;
35
import org.carrot2.labs.smartsprites.message.Message.MessageType;
36
import org.carrot2.labs.smartsprites.message.MessageLog;
37
import org.carrot2.labs.smartsprites.resource.ResourceHandler;
38
import org.carrot2.util.BufferedImageUtils;
39
import org.carrot2.util.FileUtils;
40

41
/**
42
 * Lays out and builds sprite images based on the collected SmartSprites directives.
43
 */
44
public class SpriteImageBuilder {
45

46
    private static final String CANNOT_READ_INPUT_FILE_MESSAGE = "Can't read input file!";
47

48
    /** This builder's configuration. */
49
    public final SmartSpritesParameters parameters;
50

51
    /** This builder's message log. */
52
    private final MessageLog messageLog;
53

54
    /** Image merger for this builder. */
55
    private SpriteImageRenderer spriteImageRenderer;
56

57
    /** The resource handler. */
58
    private ResourceHandler resourceHandler;
59

60
    /**
61
     * A timestamp to use for timestamp-based sprite image UIDs. We need this time stamp as a field to make sure the
62
     * timestamp is the same for all sprite image replacements.
63
     */
64
    private Instant timestamp;
65

66
    /**
67
     * Creates a {@link SpriteImageBuilder} with the provided parameters and log.
68
     *
69
     * @param parameters
70
     *            the parameters
71
     * @param messageLog
72
     *            the message log
73
     * @param resourceHandler
74
     *            the resource handler
75
     */
76
    SpriteImageBuilder(SmartSpritesParameters parameters, MessageLog messageLog, ResourceHandler resourceHandler) {
1✔
77
        this.messageLog = messageLog;
1✔
78
        this.parameters = parameters;
1✔
79
        this.resourceHandler = resourceHandler;
1✔
80
        spriteImageRenderer = new SpriteImageRenderer(parameters, messageLog);
1✔
81
    }
1✔
82

83
    /**
84
     * Builds all sprite images based on the collected directives.
85
     *
86
     * @param spriteImageOccurrencesBySpriteId
87
     *            the sprite image occurrences by sprite id
88
     * @param spriteReferenceOccurrencesBySpriteId
89
     *            the sprite reference occurrences by sprite id
90
     *
91
     * @return the multimap
92
     *
93
     * @throws IOException
94
     *             Signals that an I/O exception has occurred.
95
     */
96
    Multimap<String, SpriteReferenceReplacement> buildSpriteImages(
97
            Map<String, SpriteImageOccurrence> spriteImageOccurrencesBySpriteId,
98
            Multimap<String, SpriteReferenceOccurrence> spriteReferenceOccurrencesBySpriteId) throws IOException {
99
        timestamp = Instant.now();
1✔
100

101
        final Multimap<String, SpriteReferenceReplacement> spriteReplacementsByFile = LinkedListMultimap.create();
1✔
102
        for (final Map.Entry<String, Collection<SpriteReferenceOccurrence>> spriteReferenceOccurrences : spriteReferenceOccurrencesBySpriteId
1✔
103
                .asMap().entrySet()) {
1✔
104
            final Map<SpriteReferenceOccurrence, SpriteReferenceReplacement> spriteReferenceReplacements = buildSpriteReplacements(
1✔
105
                    spriteImageOccurrencesBySpriteId.get(spriteReferenceOccurrences.getKey()),
1✔
106
                    spriteReferenceOccurrences.getValue());
1✔
107

108
            for (final SpriteReferenceReplacement spriteReferenceReplacement : spriteReferenceReplacements.values()) {
1✔
109
                spriteReplacementsByFile.put(spriteReferenceReplacement.spriteReferenceOccurrence.cssFile,
1✔
110
                        spriteReferenceReplacement);
111
            }
1✔
112
        }
1✔
113

114
        return spriteReplacementsByFile;
1✔
115
    }
116

117
    /**
118
     * Reads an image from an input stream.
119
     *
120
     * @param imageStream
121
     *            the image stream
122
     * @param imagePath
123
     *            the image path
124
     *
125
     * @return the image
126
     *
127
     * @throws IOException
128
     *             Signals that an I/O exception has occurred.
129
     */
130
    private BufferedImage readImage(InputStream imageStream, String imagePath) throws IOException {
131
        if (isSvgPath(imagePath)) {
1✔
132
            return readSvgImage(imageStream, imagePath);
1✔
133
        }
134
        return ImageIO.read(imageStream);
1✔
135
    }
136

137
    /**
138
     * Returns true if the image path is an SVG file.
139
     *
140
     * @param imagePath
141
     *            the image path
142
     *
143
     * @return true, if is SVG path
144
     */
145
    private boolean isSvgPath(String imagePath) {
146
        return imagePath.toLowerCase(Locale.ENGLISH).endsWith(".svg");
1✔
147
    }
148

149
    /**
150
     * Reads an SVG image into a buffered image.
151
     *
152
     * @param imageStream
153
     *            the image stream
154
     * @param imagePath
155
     *            the image path
156
     *
157
     * @return the buffered image
158
     *
159
     * @throws IOException
160
     *             Signals that an I/O exception has occurred.
161
     */
162
    private BufferedImage readSvgImage(InputStream imageStream, String imagePath) throws IOException {
163
        final BufferedImageTranscoder transcoder = new BufferedImageTranscoder();
1✔
164
        try {
165
            transcoder.transcode(new TranscoderInput(imageStream), null);
1✔
166
            return transcoder.getImage();
1✔
167
        } catch (TranscoderException e) {
×
168
            throw new IOException("Cannot read SVG input file: " + imagePath, e);
×
169
        }
170
    }
171

172
    /**
173
     * Builds sprite image for a single sprite image directive.
174
     *
175
     * @param spriteImageOccurrence
176
     *            the sprite image occurrence
177
     * @param spriteReferenceOccurrences
178
     *            the sprite reference occurrences
179
     *
180
     * @return the map
181
     *
182
     * @throws IOException
183
     *             Signals that an I/O exception has occurred.
184
     */
185
    Map<SpriteReferenceOccurrence, SpriteReferenceReplacement> buildSpriteReplacements(
186
            SpriteImageOccurrence spriteImageOccurrence,
187
            Collection<SpriteReferenceOccurrence> spriteReferenceOccurrences) throws IOException {
188
        // Load images into memory. TODO: impose some limit here?
189
        final Map<SpriteReferenceOccurrence, BufferedImage> images = new LinkedHashMap<>();
1✔
190
        for (final SpriteReferenceOccurrence spriteReferenceOccurrence : spriteReferenceOccurrences) {
1✔
191
            messageLog.setCssFile(spriteReferenceOccurrence.cssFile);
1✔
192
            messageLog.setLine(spriteReferenceOccurrence.line);
1✔
193

194
            final String realImagePath = resourceHandler.getResourcePath(spriteReferenceOccurrence.cssFile,
1✔
195
                    spriteReferenceOccurrence.imagePath);
196

197
            try (InputStream is = resourceHandler.getResourceAsInputStream(realImagePath)) {
1✔
198

199
                // Load image
200
                if (is == null) {
1!
201
                    messageLog.warning(MessageType.CANNOT_NOT_LOAD_IMAGE, realImagePath,
×
202
                            CANNOT_READ_INPUT_FILE_MESSAGE);
203
                    continue;
×
204
                }
205
                messageLog.info(MessageType.READING_IMAGE, realImagePath);
1✔
206
                final BufferedImage image = readImage(is, realImagePath);
1✔
207
                if (image != null) {
1✔
208
                    images.put(spriteReferenceOccurrence, image);
1✔
209
                } else {
210
                    messageLog.warning(MessageType.UNSUPPORTED_INDIVIDUAL_IMAGE_FORMAT, realImagePath);
1✔
211
                }
212
            } catch (final IOException e) {
1!
213
                final String errorMessage;
214
                if (isSvgPath(realImagePath) && e.getMessage() != null && !e.getMessage().isBlank()) {
1!
215
                    errorMessage = e.getMessage();
×
216
                } else {
217
                    errorMessage = CANNOT_READ_INPUT_FILE_MESSAGE;
1✔
218
                }
219
                messageLog.warning(MessageType.CANNOT_NOT_LOAD_IMAGE, realImagePath, errorMessage);
1✔
220
                continue;
1✔
221
            }
1✔
222

223
            messageLog.setCssFile(null);
1✔
224
        }
1✔
225

226
        // Build the sprite image bitmap
227
        final SpriteImage spriteImage = SpriteImageBuilder.buildSpriteImage(spriteImageOccurrence, images, messageLog);
1✔
228
        if (spriteImage == null) {
1✔
229
            return Map.of();
1✔
230
        }
231

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

235
        writeSprite(spriteImage, mergedImages[0]);
1✔
236

237
        return spriteImage.spriteReferenceReplacements;
1✔
238
    }
239

240
    /**
241
     * Writes sprite image to the disk.
242
     *
243
     * @param spriteImage
244
     *            the sprite image
245
     * @param mergedImage
246
     *            the merged image
247
     *
248
     * @throws IOException
249
     *             Signals that an I/O exception has occurred.
250
     */
251
    private void writeSprite(SpriteImage spriteImage, final BufferedImage mergedImage) throws IOException {
252
        final SpriteImageOccurrence spriteImageOccurrence = spriteImage.spriteImageOccurrence;
1✔
253
        final SpriteImageDirective spriteImageDirective = spriteImageOccurrence.spriteImageDirective;
1✔
254

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

258
        // If writing to a JPEG, we need to make a 3-byte-encoded image
259
        final BufferedImage imageToWrite;
260
        if (SpriteImageFormat.JPG.equals(spriteImageDirective.format)) {
1!
261
            imageToWrite = new BufferedImage(mergedImage.getWidth(), mergedImage.getHeight(),
×
262
                    BufferedImage.TYPE_3BYTE_BGR);
263
            BufferedImageUtils.drawImage(mergedImage, imageToWrite, 0, 0);
×
264
        } else {
265
            imageToWrite = mergedImage;
1✔
266
        }
267

268
        try {
269
            ImageIO.write(imageToWrite, spriteImageDirective.format.toString(), spriteImageByteArrayOutputStream);
1✔
270
        } catch (IOException e) {
×
271
            // Unlikely to happen.
272
            messageLog.warning(MessageType.CANNOT_WRITE_SPRITE_IMAGE, spriteImageDirective.imagePath, e.getMessage());
×
273
        }
1✔
274

275
        // Build file name
276
        byte[] spriteImageBytes = spriteImageByteArrayOutputStream.toByteArray();
1✔
277
        String resolvedImagePath = spriteImage.resolveImagePath(spriteImageBytes, timestamp.toString());
1✔
278
        if (resolvedImagePath.indexOf('?') >= 0) {
1✔
279
            resolvedImagePath = resolvedImagePath.substring(0, resolvedImagePath.indexOf('?'));
1✔
280
        }
281

282
        // Save the image to the disk
283
        final String mergedImageFile = getImageFile(spriteImageOccurrence.cssFile, resolvedImagePath);
1✔
284

285
        try (OutputStream spriteImageOutputStream = resourceHandler.getResourceAsOutputStream(mergedImageFile)) {
1✔
286
            messageLog.info(MessageType.WRITING_SPRITE_IMAGE, mergedImage.getWidth(), mergedImage.getHeight(),
1✔
287
                    spriteImageDirective.spriteId, mergedImageFile);
288

289
            spriteImageOutputStream.write(spriteImageBytes);
1✔
290
        } catch (final IOException e) {
×
291
            messageLog.warning(MessageType.CANNOT_WRITE_SPRITE_IMAGE, mergedImageFile, e.getMessage());
×
292
        }
1✔
293
    }
1✔
294

295
    /**
296
     * Computes the image path. If the imagePath is relative, it's taken relative to the cssFile. If imagePath is
297
     * absolute (starts with '/') and documentRootDir is not null, it's taken relative to documentRootDir.
298
     *
299
     * @param cssFile
300
     *            the css file
301
     * @param imagePath
302
     *            the image path
303
     *
304
     * @return the image file
305
     */
306
    String getImageFile(String cssFile, String imagePath) {
307
        // Absolute path resolution is done by resourceHandler
308
        final String path = resourceHandler.getResourcePath(cssFile, imagePath);
1✔
309

310
        // Just handle the root directory changing
311
        if (!imagePath.startsWith("/") && parameters.hasOutputDir()) {
1✔
312
            return FileUtils.changeRoot(path, parameters.getRootDir(), parameters.getOutputDir());
1✔
313
        }
314
        return path;
1✔
315
    }
316

317
    /**
318
     * Calculates total dimensions and lays out a single sprite image.
319
     *
320
     * @param spriteImageOccurrence
321
     *            the sprite image occurrence
322
     * @param images
323
     *            the images
324
     * @param messageLog
325
     *            the message log
326
     *
327
     * @return the sprite image
328
     */
329
    static SpriteImage buildSpriteImage(SpriteImageOccurrence spriteImageOccurrence,
330
            Map<SpriteReferenceOccurrence, BufferedImage> images, MessageLog messageLog) {
331
        // First find the least common multiple of the images with 'repeat' alignment
332
        final SpriteImageLayout layout = spriteImageOccurrence.spriteImageDirective.layout;
1✔
333
        final float spriteScale = spriteImageOccurrence.spriteImageDirective.scaleRatio;
1✔
334
        final int leastCommonMultiple = SpriteImageBuilder.calculateLeastCommonMultiple(images, layout);
1✔
335

336
        // Compute sprite dimension (width for vertical, height for horizontal sprites)
337
        final boolean vertical = layout.equals(SpriteImageLayout.VERTICAL);
1✔
338
        int dimension = leastCommonMultiple;
1✔
339
        for (final Map.Entry<SpriteReferenceOccurrence, BufferedImage> entry : images.entrySet()) {
1✔
340
            final BufferedImage image = entry.getValue();
1✔
341
            final SpriteReferenceOccurrence spriteReferenceOccurrence = entry.getKey();
1✔
342

343
            // Compute dimensions
344
            dimension = Math.max(dimension, vertical ? spriteReferenceOccurrence.getRequiredWidth(image, layout)
1✔
345
                    : spriteReferenceOccurrence.getRequiredHeight(image, layout));
1✔
346
        }
1✔
347

348
        // Correct for least common multiple
349
        if (dimension % leastCommonMultiple != 0) {
1✔
350
            dimension += leastCommonMultiple - dimension % leastCommonMultiple;
1✔
351
        }
352

353
        // Compute the other sprite dimension.
354
        int currentOffset = 0;
1✔
355
        final Map<SpriteReferenceOccurrence, SpriteReferenceReplacement> spriteReplacements = new LinkedHashMap<>();
1✔
356
        final Map<BufferedImageEqualsWrapper, Integer> renderedImageToOffset = new LinkedHashMap<>();
1✔
357
        for (final Map.Entry<SpriteReferenceOccurrence, BufferedImage> entry : images.entrySet()) {
1✔
358
            final SpriteReferenceOccurrence spriteReferenceOccurrence = entry.getKey();
1✔
359
            final BufferedImage image = entry.getValue();
1✔
360

361
            final BufferedImage rendered = spriteReferenceOccurrence.render(image, layout, dimension);
1✔
362
            final BufferedImageEqualsWrapper imageWrapper = new BufferedImageEqualsWrapper(rendered);
1✔
363
            Integer imageOffset = renderedImageToOffset.get(imageWrapper);
1✔
364
            if (imageOffset == null) {
1✔
365
                // Draw a new image
366
                imageOffset = currentOffset;
1✔
367
                renderedImageToOffset.put(imageWrapper, imageOffset);
1✔
368
                currentOffset += vertical ? rendered.getHeight() : rendered.getWidth();
1✔
369
            }
370

371
            final float scaledImageWidth = spriteReferenceOccurrence.getRequiredWidth(image, layout) / spriteScale;
1✔
372
            final float scaledImageHeight = spriteReferenceOccurrence.getRequiredHeight(image, layout) / spriteScale;
1✔
373
            if (Math.round(scaledImageWidth) != scaledImageWidth
1✔
374
                    || Math.round(scaledImageHeight) != scaledImageHeight) {
1!
375
                messageLog.warning(MessageType.IMAGE_FRACTIONAL_SCALE_VALUE, spriteReferenceOccurrence.imagePath,
1✔
376
                        scaledImageWidth, scaledImageHeight);
1✔
377
            }
378

379
            final int adjustedImageOffset = Math.round(imageOffset / spriteScale);
1✔
380
            spriteReplacements.put(spriteReferenceOccurrence,
1✔
381
                    spriteReferenceOccurrence.buildReplacement(layout, adjustedImageOffset));
1✔
382
        }
1✔
383

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

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

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

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

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

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

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

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

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

456
        return result;
1✔
457
    }
458

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

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

475
        return result;
1✔
476
    }
477

478
    /**
479
     * A Batik transcoder implementation returning a buffered image in memory.
480
     */
481
    private static final class BufferedImageTranscoder extends ImageTranscoder {
482

483
        /** The image. */
484
        private BufferedImage image;
485

486
        @Override
487
        public BufferedImage createImage(int width, int height) {
488
            // Preserve full alpha information from SVG content for downstream sprite composition.
489
            return new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
1✔
490
        }
491

492
        @Override
493
        public void writeImage(BufferedImage image, TranscoderOutput output) {
494
            this.image = image;
1✔
495
        }
1✔
496

497
        /**
498
         * Gets the image.
499
         *
500
         * @return the image
501
         */
502
        public BufferedImage getImage() {
503
            return image;
1✔
504
        }
505
    }
506

507
    /**
508
     * A wrapper that implements content-aware {@link Object#equals(Object)} and {@link Object#hashCode()} on
509
     * {@link BufferedImage}s.
510
     */
511
    static final class BufferedImageEqualsWrapper {
512

513
        /** The image. */
514
        BufferedImage image;
515

516
        /**
517
         * Instantiates a new buffered image equals wrapper.
518
         *
519
         * @param image
520
         *            the image
521
         */
522
        BufferedImageEqualsWrapper(BufferedImage image) {
1✔
523
            this.image = image;
1✔
524
        }
1✔
525

526
        @Override
527
        public boolean equals(Object obj) {
528
            if (!(obj instanceof BufferedImageEqualsWrapper)) {
1!
529
                return false;
×
530
            }
531

532
            if (obj == this) {
1!
533
                return true;
×
534
            }
535

536
            final BufferedImage other = ((BufferedImageEqualsWrapper) obj).image;
1✔
537

538
            boolean equal = other.getWidth() == image.getWidth() && other.getHeight() == image.getHeight()
1!
539
                    && other.getType() == image.getType();
1!
540

541
            if (equal) {
1!
542
                for (int y = 0; y < image.getHeight(); y++) {
1✔
543
                    for (int x = 0; x < image.getWidth(); x++) {
1✔
544
                        if (ignoreFullTransparency(image.getRGB(x, y)) != ignoreFullTransparency(other.getRGB(x, y))) {
1✔
545
                            return false;
1✔
546
                        }
547
                    }
548
                }
549
            }
550

551
            return equal;
1✔
552
        }
553

554
        @Override
555
        public int hashCode() {
556
            if (image == null) {
1!
557
                return 0;
×
558
            }
559

560
            int hash = image.getWidth() ^ image.getHeight() << 16;
1✔
561

562
            // Computes the hashCode based on an 4 x 4 to 7 x 7 grid of image's pixels
563
            final int xIncrement = image.getWidth() > 7 ? image.getWidth() >> 2 : 1;
1!
564
            final int yIncrement = image.getHeight() > 7 ? image.getHeight() >> 2 : 1;
1!
565

566
            for (int y = 0; y < image.getHeight(); y += yIncrement) {
1✔
567
                for (int x = 0; x < image.getWidth(); x += xIncrement) {
1✔
568
                    hash ^= ignoreFullTransparency(image.getRGB(x, y));
1✔
569
                }
570
            }
571

572
            return hash;
1✔
573
        }
574

575
        /**
576
         * If the pixel is fully transparent, returns 0. Otherwise, returns the pixel. This is useful in
577
         * {@link #equals(Object)} and {@link #hashCode()} to ignore pixels that have different colors but are invisible
578
         * anyway because of full transparency.
579
         *
580
         * @param pixel
581
         *            the pixel
582
         *
583
         * @return the int
584
         */
585
        private static int ignoreFullTransparency(int pixel) {
586
            if ((pixel & 0xff000000) == 0x00000000) {
1✔
587
                return 0;
1✔
588
            }
589
            return pixel;
1✔
590
        }
591
    }
592
}
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