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

hazendaz / smartsprites / 530

02 May 2026 08:01PM UTC coverage: 87.799% (-0.2%) from 88.035%
530

push

github

web-flow
Merge pull request #192 from hazendaz/renovate/org.apache.xmlgraphics-batik-transcoder-1.x

Update dependency org.apache.xmlgraphics:batik-transcoder to v1.19

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
 * 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 com.google.common.collect.LinkedListMultimap;
40
import com.google.common.collect.Multimap;
41

42
import java.awt.image.BufferedImage;
43
import java.io.ByteArrayOutputStream;
44
import java.io.IOException;
45
import java.io.InputStream;
46
import java.io.OutputStream;
47
import java.time.Instant;
48
import java.util.Collection;
49
import java.util.Collections;
50
import java.util.HashMap;
51
import java.util.LinkedHashMap;
52
import java.util.Locale;
53
import java.util.Map;
54

55
import javax.imageio.ImageIO;
56

57
import org.apache.batik.transcoder.TranscoderException;
58
import org.apache.batik.transcoder.TranscoderInput;
59
import org.apache.batik.transcoder.TranscoderOutput;
60
import org.apache.batik.transcoder.image.ImageTranscoder;
61
import org.apache.commons.math3.util.ArithmeticUtils;
62
import org.carrot2.labs.smartsprites.SpriteImageDirective.SpriteImageFormat;
63
import org.carrot2.labs.smartsprites.SpriteImageDirective.SpriteImageLayout;
64
import org.carrot2.labs.smartsprites.SpriteLayoutProperties.SpriteAlignment;
65
import org.carrot2.labs.smartsprites.message.Message.MessageType;
66
import org.carrot2.labs.smartsprites.message.MessageLog;
67
import org.carrot2.labs.smartsprites.resource.ResourceHandler;
68
import org.carrot2.util.BufferedImageUtils;
69
import org.carrot2.util.FileUtils;
70

71
/**
72
 * Lays out and builds sprite images based on the collected SmartSprites directives.
73
 */
74
public class SpriteImageBuilder {
75

76
    private static final String CANNOT_READ_INPUT_FILE_MESSAGE = "Can't read input file!";
77

78
    /** This builder's configuration. */
79
    public final SmartSpritesParameters parameters;
80

81
    /** This builder's message log. */
82
    private final MessageLog messageLog;
83

84
    /** Image merger for this builder. */
85
    private SpriteImageRenderer spriteImageRenderer;
86

87
    /** The resource handler. */
88
    private ResourceHandler resourceHandler;
89

90
    /**
91
     * A timestamp to use for timestamp-based sprite image UIDs. We need this time stamp as a field to make sure the
92
     * timestamp is the same for all sprite image replacements.
93
     */
94
    private Instant timestamp;
95

96
    /**
97
     * Creates a {@link SpriteImageBuilder} with the provided parameters and log.
98
     *
99
     * @param parameters
100
     *            the parameters
101
     * @param messageLog
102
     *            the message log
103
     * @param resourceHandler
104
     *            the resource handler
105
     */
106
    SpriteImageBuilder(SmartSpritesParameters parameters, MessageLog messageLog, ResourceHandler resourceHandler) {
1✔
107
        this.messageLog = messageLog;
1✔
108
        this.parameters = parameters;
1✔
109
        this.resourceHandler = resourceHandler;
1✔
110
        spriteImageRenderer = new SpriteImageRenderer(parameters, messageLog);
1✔
111
    }
1✔
112

113
    /**
114
     * Builds all sprite images based on the collected directives.
115
     *
116
     * @param spriteImageOccurrencesBySpriteId
117
     *            the sprite image occurrences by sprite id
118
     * @param spriteReferenceOccurrencesBySpriteId
119
     *            the sprite reference occurrences by sprite id
120
     *
121
     * @return the multimap
122
     *
123
     * @throws IOException
124
     *             Signals that an I/O exception has occurred.
125
     */
126
    Multimap<String, SpriteReferenceReplacement> buildSpriteImages(
127
            Map<String, SpriteImageOccurrence> spriteImageOccurrencesBySpriteId,
128
            Multimap<String, SpriteReferenceOccurrence> spriteReferenceOccurrencesBySpriteId) throws IOException {
129
        timestamp = Instant.now();
1✔
130

131
        final Multimap<String, SpriteReferenceReplacement> spriteReplacementsByFile = LinkedListMultimap.create();
1✔
132
        for (final Map.Entry<String, Collection<SpriteReferenceOccurrence>> spriteReferenceOccurrences : spriteReferenceOccurrencesBySpriteId
1✔
133
                .asMap().entrySet()) {
1✔
134
            final Map<SpriteReferenceOccurrence, SpriteReferenceReplacement> spriteReferenceReplacements = buildSpriteReplacements(
1✔
135
                    spriteImageOccurrencesBySpriteId.get(spriteReferenceOccurrences.getKey()),
1✔
136
                    spriteReferenceOccurrences.getValue());
1✔
137

138
            for (final SpriteReferenceReplacement spriteReferenceReplacement : spriteReferenceReplacements.values()) {
1✔
139
                spriteReplacementsByFile.put(spriteReferenceReplacement.spriteReferenceOccurrence.cssFile,
1✔
140
                        spriteReferenceReplacement);
141
            }
1✔
142
        }
1✔
143

144
        return spriteReplacementsByFile;
1✔
145
    }
146

147
    /**
148
     * Reads an image from an input stream.
149
     *
150
     * @param imageStream
151
     *            the image stream
152
     * @param imagePath
153
     *            the image path
154
     *
155
     * @return the image
156
     *
157
     * @throws IOException
158
     *             Signals that an I/O exception has occurred.
159
     */
160
    private BufferedImage readImage(InputStream imageStream, String imagePath) throws IOException {
161
        if (isSvgPath(imagePath)) {
1✔
162
            return readSvgImage(imageStream, imagePath);
1✔
163
        }
164
        return ImageIO.read(imageStream);
1✔
165
    }
166

167
    /**
168
     * Returns true if the image path is an SVG file.
169
     *
170
     * @param imagePath
171
     *            the image path
172
     *
173
     * @return true, if is SVG path
174
     */
175
    private boolean isSvgPath(String imagePath) {
176
        return imagePath.toLowerCase(Locale.ENGLISH).endsWith(".svg");
1✔
177
    }
178

179
    /**
180
     * Reads an SVG image into a buffered image.
181
     *
182
     * @param imageStream
183
     *            the image stream
184
     * @param imagePath
185
     *            the image path
186
     *
187
     * @return the buffered image
188
     *
189
     * @throws IOException
190
     *             Signals that an I/O exception has occurred.
191
     */
192
    private BufferedImage readSvgImage(InputStream imageStream, String imagePath) throws IOException {
193
        final BufferedImageTranscoder transcoder = new BufferedImageTranscoder();
1✔
194
        try {
195
            transcoder.transcode(new TranscoderInput(imageStream), null);
1✔
196
            return transcoder.getImage();
1✔
197
        } catch (TranscoderException e) {
×
198
            throw new IOException("Cannot read SVG input file: " + imagePath, e);
×
199
        }
200
    }
201

202
    /**
203
     * Builds sprite image for a single sprite image directive.
204
     *
205
     * @param spriteImageOccurrence
206
     *            the sprite image occurrence
207
     * @param spriteReferenceOccurrences
208
     *            the sprite reference occurrences
209
     *
210
     * @return the map
211
     *
212
     * @throws IOException
213
     *             Signals that an I/O exception has occurred.
214
     */
215
    Map<SpriteReferenceOccurrence, SpriteReferenceReplacement> buildSpriteReplacements(
216
            SpriteImageOccurrence spriteImageOccurrence,
217
            Collection<SpriteReferenceOccurrence> spriteReferenceOccurrences) throws IOException {
218
        // Load images into memory. TODO: impose some limit here?
219
        final Map<SpriteReferenceOccurrence, BufferedImage> images = new LinkedHashMap<>();
1✔
220
        for (final SpriteReferenceOccurrence spriteReferenceOccurrence : spriteReferenceOccurrences) {
1✔
221
            messageLog.setCssFile(spriteReferenceOccurrence.cssFile);
1✔
222
            messageLog.setLine(spriteReferenceOccurrence.line);
1✔
223

224
            final String realImagePath = resourceHandler.getResourcePath(spriteReferenceOccurrence.cssFile,
1✔
225
                    spriteReferenceOccurrence.imagePath);
226

227
            try (InputStream is = resourceHandler.getResourceAsInputStream(realImagePath)) {
1✔
228

229
                // Load image
230
                if (is == null) {
1!
231
                    messageLog.warning(MessageType.CANNOT_NOT_LOAD_IMAGE, realImagePath,
×
232
                            CANNOT_READ_INPUT_FILE_MESSAGE);
233
                    continue;
×
234
                }
235
                messageLog.info(MessageType.READING_IMAGE, realImagePath);
1✔
236
                final BufferedImage image = readImage(is, realImagePath);
1✔
237
                if (image != null) {
1✔
238
                    images.put(spriteReferenceOccurrence, image);
1✔
239
                } else {
240
                    messageLog.warning(MessageType.UNSUPPORTED_INDIVIDUAL_IMAGE_FORMAT, realImagePath);
1✔
241
                }
242
            } catch (final IOException e) {
1!
243
                final String errorMessage;
244
                if (isSvgPath(realImagePath) && e.getMessage() != null && !e.getMessage().isBlank()) {
1!
245
                    errorMessage = e.getMessage();
×
246
                } else {
247
                    errorMessage = CANNOT_READ_INPUT_FILE_MESSAGE;
1✔
248
                }
249
                messageLog.warning(MessageType.CANNOT_NOT_LOAD_IMAGE, realImagePath, errorMessage);
1✔
250
                continue;
1✔
251
            }
1✔
252

253
            messageLog.setCssFile(null);
1✔
254
        }
1✔
255

256
        // Build the sprite image bitmap
257
        final SpriteImage spriteImage = SpriteImageBuilder.buildSpriteImage(spriteImageOccurrence, images, messageLog);
1✔
258
        if (spriteImage == null) {
1✔
259
            return Collections.<SpriteReferenceOccurrence, SpriteReferenceReplacement> emptyMap();
1✔
260
        }
261

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

265
        writeSprite(spriteImage, mergedImages[0]);
1✔
266

267
        return spriteImage.spriteReferenceReplacements;
1✔
268
    }
269

270
    /**
271
     * Writes sprite image to the disk.
272
     *
273
     * @param spriteImage
274
     *            the sprite image
275
     * @param mergedImage
276
     *            the merged image
277
     *
278
     * @throws IOException
279
     *             Signals that an I/O exception has occurred.
280
     */
281
    private void writeSprite(SpriteImage spriteImage, final BufferedImage mergedImage) throws IOException {
282
        final SpriteImageOccurrence spriteImageOccurrence = spriteImage.spriteImageOccurrence;
1✔
283
        final SpriteImageDirective spriteImageDirective = spriteImageOccurrence.spriteImageDirective;
1✔
284

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

288
        // If writing to a JPEG, we need to make a 3-byte-encoded image
289
        final BufferedImage imageToWrite;
290
        if (SpriteImageFormat.JPG.equals(spriteImageDirective.format)) {
1!
291
            imageToWrite = new BufferedImage(mergedImage.getWidth(), mergedImage.getHeight(),
×
292
                    BufferedImage.TYPE_3BYTE_BGR);
293
            BufferedImageUtils.drawImage(mergedImage, imageToWrite, 0, 0);
×
294
        } else {
295
            imageToWrite = mergedImage;
1✔
296
        }
297

298
        try {
299
            ImageIO.write(imageToWrite, spriteImageDirective.format.toString(), spriteImageByteArrayOutputStream);
1✔
300
        } catch (IOException e) {
×
301
            // Unlikely to happen.
302
            messageLog.warning(MessageType.CANNOT_WRITE_SPRITE_IMAGE, spriteImageDirective.imagePath, e.getMessage());
×
303
        }
1✔
304

305
        // Build file name
306
        byte[] spriteImageBytes = spriteImageByteArrayOutputStream.toByteArray();
1✔
307
        String resolvedImagePath = spriteImage.resolveImagePath(spriteImageBytes, timestamp.toString());
1✔
308
        if (resolvedImagePath.indexOf('?') >= 0) {
1✔
309
            resolvedImagePath = resolvedImagePath.substring(0, resolvedImagePath.indexOf('?'));
1✔
310
        }
311

312
        // Save the image to the disk
313
        final String mergedImageFile = getImageFile(spriteImageOccurrence.cssFile, resolvedImagePath);
1✔
314

315
        try (OutputStream spriteImageOutputStream = resourceHandler.getResourceAsOutputStream(mergedImageFile)) {
1✔
316
            messageLog.info(MessageType.WRITING_SPRITE_IMAGE, mergedImage.getWidth(), mergedImage.getHeight(),
1✔
317
                    spriteImageDirective.spriteId, mergedImageFile);
318

319
            spriteImageOutputStream.write(spriteImageBytes);
1✔
320
        } catch (final IOException e) {
×
321
            messageLog.warning(MessageType.CANNOT_WRITE_SPRITE_IMAGE, mergedImageFile, e.getMessage());
×
322
        }
1✔
323
    }
1✔
324

325
    /**
326
     * Computes the image path. If the imagePath is relative, it's taken relative to the cssFile. If imagePath is
327
     * absolute (starts with '/') and documentRootDir is not null, it's taken relative to documentRootDir.
328
     *
329
     * @param cssFile
330
     *            the css file
331
     * @param imagePath
332
     *            the image path
333
     *
334
     * @return the image file
335
     */
336
    String getImageFile(String cssFile, String imagePath) {
337
        // Absolute path resolution is done by resourceHandler
338
        final String path = resourceHandler.getResourcePath(cssFile, imagePath);
1✔
339

340
        // Just handle the root directory changing
341
        if (!imagePath.startsWith("/") && parameters.hasOutputDir()) {
1✔
342
            return FileUtils.changeRoot(path, parameters.getRootDir(), parameters.getOutputDir());
1✔
343
        }
344
        return path;
1✔
345
    }
346

347
    /**
348
     * Calculates total dimensions and lays out a single sprite image.
349
     *
350
     * @param spriteImageOccurrence
351
     *            the sprite image occurrence
352
     * @param images
353
     *            the images
354
     * @param messageLog
355
     *            the message log
356
     *
357
     * @return the sprite image
358
     */
359
    static SpriteImage buildSpriteImage(SpriteImageOccurrence spriteImageOccurrence,
360
            Map<SpriteReferenceOccurrence, BufferedImage> images, MessageLog messageLog) {
361
        // First find the least common multiple of the images with 'repeat' alignment
362
        final SpriteImageLayout layout = spriteImageOccurrence.spriteImageDirective.layout;
1✔
363
        final float spriteScale = spriteImageOccurrence.spriteImageDirective.scaleRatio;
1✔
364
        final int leastCommonMultiple = SpriteImageBuilder.calculateLeastCommonMultiple(images, layout);
1✔
365

366
        // Compute sprite dimension (width for vertical, height for horizontal sprites)
367
        final boolean vertical = layout.equals(SpriteImageLayout.VERTICAL);
1✔
368
        int dimension = leastCommonMultiple;
1✔
369
        for (final Map.Entry<SpriteReferenceOccurrence, BufferedImage> entry : images.entrySet()) {
1✔
370
            final BufferedImage image = entry.getValue();
1✔
371
            final SpriteReferenceOccurrence spriteReferenceOccurrence = entry.getKey();
1✔
372

373
            // Compute dimensions
374
            dimension = Math.max(dimension, vertical ? spriteReferenceOccurrence.getRequiredWidth(image, layout)
1✔
375
                    : spriteReferenceOccurrence.getRequiredHeight(image, layout));
1✔
376
        }
1✔
377

378
        // Correct for least common multiple
379
        if (dimension % leastCommonMultiple != 0) {
1✔
380
            dimension += leastCommonMultiple - dimension % leastCommonMultiple;
1✔
381
        }
382

383
        // Compute the other sprite dimension.
384
        int currentOffset = 0;
1✔
385
        final Map<SpriteReferenceOccurrence, SpriteReferenceReplacement> spriteReplacements = new LinkedHashMap<>();
1✔
386
        final Map<BufferedImageEqualsWrapper, Integer> renderedImageToOffset = new LinkedHashMap<>();
1✔
387
        for (final Map.Entry<SpriteReferenceOccurrence, BufferedImage> entry : images.entrySet()) {
1✔
388
            final SpriteReferenceOccurrence spriteReferenceOccurrence = entry.getKey();
1✔
389
            final BufferedImage image = entry.getValue();
1✔
390

391
            final BufferedImage rendered = spriteReferenceOccurrence.render(image, layout, dimension);
1✔
392
            final BufferedImageEqualsWrapper imageWrapper = new BufferedImageEqualsWrapper(rendered);
1✔
393
            Integer imageOffset = renderedImageToOffset.get(imageWrapper);
1✔
394
            if (imageOffset == null) {
1✔
395
                // Draw a new image
396
                imageOffset = currentOffset;
1✔
397
                renderedImageToOffset.put(imageWrapper, imageOffset);
1✔
398
                currentOffset += vertical ? rendered.getHeight() : rendered.getWidth();
1✔
399
            }
400

401
            final float scaledImageWidth = spriteReferenceOccurrence.getRequiredWidth(image, layout) / spriteScale;
1✔
402
            final float scaledImageHeight = spriteReferenceOccurrence.getRequiredHeight(image, layout) / spriteScale;
1✔
403
            if (Math.round(scaledImageWidth) != scaledImageWidth
1✔
404
                    || Math.round(scaledImageHeight) != scaledImageHeight) {
1!
405
                messageLog.warning(MessageType.IMAGE_FRACTIONAL_SCALE_VALUE, spriteReferenceOccurrence.imagePath,
1✔
406
                        scaledImageWidth, scaledImageHeight);
1✔
407
            }
408

409
            final int adjustedImageOffset = Math.round(imageOffset / spriteScale);
1✔
410
            spriteReplacements.put(spriteReferenceOccurrence,
1✔
411
                    spriteReferenceOccurrence.buildReplacement(layout, adjustedImageOffset));
1✔
412
        }
1✔
413

414
        // Render the sprite image and build sprite reference replacements
415
        final int spriteWidth = vertical ? dimension : currentOffset;
1✔
416
        final int spriteHeight = vertical ? currentOffset : dimension;
1✔
417
        if (spriteWidth == 0 || spriteHeight == 0) {
1!
418
            return null;
1✔
419
        }
420

421
        final float scaledWidth = spriteWidth / spriteScale;
1✔
422
        final float scaledHeight = spriteHeight / spriteScale;
1✔
423
        if (Math.round(scaledWidth) != scaledWidth || Math.round(scaledHeight) != scaledHeight) {
1!
424
            messageLog.warning(MessageType.FRACTIONAL_SCALE_VALUE, spriteImageOccurrence.spriteImageDirective.spriteId,
1✔
425
                    scaledWidth, scaledHeight);
1✔
426
        }
427

428
        final BufferedImage sprite = new BufferedImage(spriteWidth, spriteHeight, BufferedImage.TYPE_4BYTE_ABGR);
1✔
429

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

432
            BufferedImageUtils.drawImage(entry.getKey().image, sprite, vertical ? 0 : entry.getValue(),
1✔
433
                    vertical ? entry.getValue() : 0);
1✔
434
        }
1✔
435

436
        return new SpriteImage(sprite, spriteImageOccurrence, spriteReplacements, spriteWidth, spriteHeight,
1✔
437
                spriteScale);
438
    }
439

440
    /**
441
     * Calculates the width/ height of "repeated" sprites.
442
     *
443
     * @param images
444
     *            the images
445
     * @param layout
446
     *            the layout
447
     *
448
     * @return the int
449
     */
450
    static int calculateLeastCommonMultiple(Map<SpriteReferenceOccurrence, BufferedImage> images,
451
            SpriteImageLayout layout) {
452
        int leastCommonMultiple = 1;
1✔
453
        for (final Map.Entry<SpriteReferenceOccurrence, BufferedImage> entry : images.entrySet()) {
1✔
454
            final BufferedImage image = entry.getValue();
1✔
455
            final SpriteReferenceOccurrence spriteReferenceOccurrence = entry.getKey();
1✔
456
            if (image != null && SpriteAlignment.REPEAT
1!
457
                    .equals(spriteReferenceOccurrence.spriteReferenceDirective.spriteLayoutProperties.alignment)) {
1✔
458
                if (SpriteImageLayout.VERTICAL.equals(layout)) {
1✔
459
                    leastCommonMultiple = ArithmeticUtils.lcm(leastCommonMultiple,
1✔
460
                            spriteReferenceOccurrence.getRequiredWidth(image, layout));
1✔
461
                } else {
462
                    leastCommonMultiple = ArithmeticUtils.lcm(leastCommonMultiple,
1✔
463
                            spriteReferenceOccurrence.getRequiredHeight(image, layout));
1✔
464
                }
465
            }
466
        }
1✔
467
        return leastCommonMultiple;
1✔
468
    }
469

470
    /**
471
     * Groups {@link SpriteReferenceReplacement}s by the line number of their corresponding directives.
472
     *
473
     * @param spriteReferenceReplacements
474
     *            the sprite reference replacements
475
     *
476
     * @return the sprite replacements by line number
477
     */
478
    static Map<Integer, SpriteReferenceReplacement> getSpriteReplacementsByLineNumber(
479
            Collection<SpriteReferenceReplacement> spriteReferenceReplacements) {
480
        final Map<Integer, SpriteReferenceReplacement> result = new HashMap<>();
1✔
481

482
        for (final SpriteReferenceReplacement spriteReferenceReplacement : spriteReferenceReplacements) {
1✔
483
            result.put(spriteReferenceReplacement.spriteReferenceOccurrence.line, spriteReferenceReplacement);
1✔
484
        }
1✔
485

486
        return result;
1✔
487
    }
488

489
    /**
490
     * Groups {@link SpriteImageOccurrence}s by the line number of their corresponding directives.
491
     *
492
     * @param spriteImageOccurrences
493
     *            the sprite image occurrences
494
     *
495
     * @return the sprite image occurrences by line number
496
     */
497
    static Map<Integer, SpriteImageOccurrence> getSpriteImageOccurrencesByLineNumber(
498
            Collection<SpriteImageOccurrence> spriteImageOccurrences) {
499
        final Map<Integer, SpriteImageOccurrence> result = new HashMap<>();
1✔
500

501
        for (final SpriteImageOccurrence spriteImageOccurrence : spriteImageOccurrences) {
1✔
502
            result.put(spriteImageOccurrence.line, spriteImageOccurrence);
1✔
503
        }
1✔
504

505
        return result;
1✔
506
    }
507

508
    /**
509
     * A Batik transcoder implementation returning a buffered image in memory.
510
     */
511
    private static final class BufferedImageTranscoder extends ImageTranscoder {
512

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

516
        @Override
517
        public BufferedImage createImage(int width, int height) {
518
            // Preserve full alpha information from SVG content for downstream sprite composition.
519
            return new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
1✔
520
        }
521

522
        @Override
523
        public void writeImage(BufferedImage image, TranscoderOutput output) {
524
            this.image = image;
1✔
525
        }
1✔
526

527
        /**
528
         * Gets the image.
529
         *
530
         * @return the image
531
         */
532
        public BufferedImage getImage() {
533
            return image;
1✔
534
        }
535
    }
536

537
    /**
538
     * A wrapper that implements content-aware {@link Object#equals(Object)} and {@link Object#hashCode()} on
539
     * {@link BufferedImage}s.
540
     */
541
    static final class BufferedImageEqualsWrapper {
542

543
        /** The image. */
544
        BufferedImage image;
545

546
        /**
547
         * Instantiates a new buffered image equals wrapper.
548
         *
549
         * @param image
550
         *            the image
551
         */
552
        BufferedImageEqualsWrapper(BufferedImage image) {
1✔
553
            this.image = image;
1✔
554
        }
1✔
555

556
        @Override
557
        public boolean equals(Object obj) {
558
            if (!(obj instanceof BufferedImageEqualsWrapper)) {
1!
559
                return false;
×
560
            }
561

562
            if (obj == this) {
1!
563
                return true;
×
564
            }
565

566
            final BufferedImage other = ((BufferedImageEqualsWrapper) obj).image;
1✔
567

568
            boolean equal = other.getWidth() == image.getWidth() && other.getHeight() == image.getHeight()
1!
569
                    && other.getType() == image.getType();
1!
570

571
            if (equal) {
1!
572
                for (int y = 0; y < image.getHeight(); y++) {
1✔
573
                    for (int x = 0; x < image.getWidth(); x++) {
1✔
574
                        if (ignoreFullTransparency(image.getRGB(x, y)) != ignoreFullTransparency(other.getRGB(x, y))) {
1✔
575
                            return false;
1✔
576
                        }
577
                    }
578
                }
579
            }
580

581
            return equal;
1✔
582
        }
583

584
        @Override
585
        public int hashCode() {
586
            if (image == null) {
1!
587
                return 0;
×
588
            }
589

590
            int hash = image.getWidth() ^ image.getHeight() << 16;
1✔
591

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

596
            for (int y = 0; y < image.getHeight(); y += yIncrement) {
1✔
597
                for (int x = 0; x < image.getWidth(); x += xIncrement) {
1✔
598
                    hash ^= ignoreFullTransparency(image.getRGB(x, y));
1✔
599
                }
600
            }
601

602
            return hash;
1✔
603
        }
604

605
        /**
606
         * If the pixel is fully transparent, returns 0. Otherwise, returns the pixel. This is useful in
607
         * {@link #equals(Object)} and {@link #hashCode()} to ignore pixels that have different colors but are invisible
608
         * anyway because of full transparency.
609
         *
610
         * @param pixel
611
         *            the pixel
612
         *
613
         * @return the int
614
         */
615
        private static int ignoreFullTransparency(int pixel) {
616
            if ((pixel & 0xff000000) == 0x00000000) {
1✔
617
                return 0;
1✔
618
            }
619
            return pixel;
1✔
620
        }
621
    }
622
}
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