1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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.Map;
53
54 import javax.imageio.ImageIO;
55
56 import org.apache.commons.math3.util.ArithmeticUtils;
57 import org.carrot2.labs.smartsprites.SpriteImageDirective.SpriteImageFormat;
58 import org.carrot2.labs.smartsprites.SpriteImageDirective.SpriteImageLayout;
59 import org.carrot2.labs.smartsprites.SpriteLayoutProperties.SpriteAlignment;
60 import org.carrot2.labs.smartsprites.message.Message.MessageType;
61 import org.carrot2.labs.smartsprites.message.MessageLog;
62 import org.carrot2.labs.smartsprites.resource.ResourceHandler;
63 import org.carrot2.util.BufferedImageUtils;
64 import org.carrot2.util.FileUtils;
65
66
67
68
69 public class SpriteImageBuilder {
70
71
72 public final SmartSpritesParameters parameters;
73
74
75 private final MessageLog messageLog;
76
77
78 private SpriteImageRenderer spriteImageRenderer;
79
80
81 private ResourceHandler resourceHandler;
82
83
84
85
86
87 private Instant timestamp;
88
89
90
91
92
93
94
95
96
97
98
99 SpriteImageBuilder(SmartSpritesParameters parameters, MessageLog messageLog, ResourceHandler resourceHandler) {
100 this.messageLog = messageLog;
101 this.parameters = parameters;
102 this.resourceHandler = resourceHandler;
103 spriteImageRenderer = new SpriteImageRenderer(parameters, messageLog);
104 }
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119 Multimap<String, SpriteReferenceReplacement> buildSpriteImages(
120 Map<String, SpriteImageOccurrence> spriteImageOccurrencesBySpriteId,
121 Multimap<String, SpriteReferenceOccurrence> spriteReferenceOccurrencesBySpriteId) throws IOException {
122 timestamp = Instant.now();
123
124 final Multimap<String, SpriteReferenceReplacement> spriteReplacementsByFile = LinkedListMultimap.create();
125 for (final Map.Entry<String, Collection<SpriteReferenceOccurrence>> spriteReferenceOccurrences : spriteReferenceOccurrencesBySpriteId
126 .asMap().entrySet()) {
127 final Map<SpriteReferenceOccurrence, SpriteReferenceReplacement> spriteReferenceReplacements = buildSpriteReplacements(
128 spriteImageOccurrencesBySpriteId.get(spriteReferenceOccurrences.getKey()),
129 spriteReferenceOccurrences.getValue());
130
131 for (final SpriteReferenceReplacement spriteReferenceReplacement : spriteReferenceReplacements.values()) {
132 spriteReplacementsByFile.put(spriteReferenceReplacement.spriteReferenceOccurrence.cssFile,
133 spriteReferenceReplacement);
134 }
135 }
136
137 return spriteReplacementsByFile;
138 }
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153 Map<SpriteReferenceOccurrence, SpriteReferenceReplacement> buildSpriteReplacements(
154 SpriteImageOccurrence spriteImageOccurrence,
155 Collection<SpriteReferenceOccurrence> spriteReferenceOccurrences) throws IOException {
156
157 final Map<SpriteReferenceOccurrence, BufferedImage> images = new LinkedHashMap<>();
158 for (final SpriteReferenceOccurrence spriteReferenceOccurrence : spriteReferenceOccurrences) {
159 messageLog.setCssFile(spriteReferenceOccurrence.cssFile);
160 messageLog.setLine(spriteReferenceOccurrence.line);
161
162 final String realImagePath = resourceHandler.getResourcePath(spriteReferenceOccurrence.cssFile,
163 spriteReferenceOccurrence.imagePath);
164
165 try (InputStream is = resourceHandler.getResourceAsInputStream(realImagePath)) {
166
167
168 if (is == null) {
169 messageLog.warning(MessageType.CANNOT_NOT_LOAD_IMAGE, realImagePath, "Can't read input file!");
170 continue;
171 }
172 messageLog.info(MessageType.READING_IMAGE, realImagePath);
173 final BufferedImage image = ImageIO.read(is);
174 if (image != null) {
175 images.put(spriteReferenceOccurrence, image);
176 } else {
177 messageLog.warning(MessageType.UNSUPPORTED_INDIVIDUAL_IMAGE_FORMAT, realImagePath);
178 }
179 } catch (final IOException e) {
180 messageLog.warning(MessageType.CANNOT_NOT_LOAD_IMAGE, realImagePath, "Can't read input file!");
181 continue;
182 }
183
184 messageLog.setCssFile(null);
185 }
186
187
188 final SpriteImage spriteImage = SpriteImageBuilder.buildSpriteImage(spriteImageOccurrence, images, messageLog);
189 if (spriteImage == null) {
190 return Collections.<SpriteReferenceOccurrence, SpriteReferenceReplacement>emptyMap();
191 }
192
193
194 final BufferedImage[] mergedImages = spriteImageRenderer.render(spriteImage);
195
196 writeSprite(spriteImage, mergedImages[0]);
197
198 return spriteImage.spriteReferenceReplacements;
199 }
200
201
202
203
204
205
206
207
208
209
210
211
212 private void writeSprite(SpriteImage spriteImage, final BufferedImage mergedImage) throws IOException {
213 final SpriteImageOccurrence spriteImageOccurrence = spriteImage.spriteImageOccurrence;
214 final SpriteImageDirective spriteImageDirective = spriteImageOccurrence.spriteImageDirective;
215
216
217 final ByteArrayOutputStream spriteImageByteArrayOutputStream = new ByteArrayOutputStream();
218
219
220 final BufferedImage imageToWrite;
221 if (SpriteImageFormat.JPG.equals(spriteImageDirective.format)) {
222 imageToWrite = new BufferedImage(mergedImage.getWidth(), mergedImage.getHeight(),
223 BufferedImage.TYPE_3BYTE_BGR);
224 BufferedImageUtils.drawImage(mergedImage, imageToWrite, 0, 0);
225 } else {
226 imageToWrite = mergedImage;
227 }
228
229 try {
230 ImageIO.write(imageToWrite, spriteImageDirective.format.toString(), spriteImageByteArrayOutputStream);
231 } catch (IOException e) {
232
233 messageLog.warning(MessageType.CANNOT_WRITE_SPRITE_IMAGE, spriteImageDirective.imagePath, e.getMessage());
234 }
235
236
237 byte[] spriteImageBytes = spriteImageByteArrayOutputStream.toByteArray();
238 String resolvedImagePath = spriteImage.resolveImagePath(spriteImageBytes, timestamp.toString());
239 if (resolvedImagePath.indexOf('?') >= 0) {
240 resolvedImagePath = resolvedImagePath.substring(0, resolvedImagePath.indexOf('?'));
241 }
242
243
244 final String mergedImageFile = getImageFile(spriteImageOccurrence.cssFile, resolvedImagePath);
245
246 try (OutputStream spriteImageOutputStream = resourceHandler.getResourceAsOutputStream(mergedImageFile)) {
247 messageLog.info(MessageType.WRITING_SPRITE_IMAGE, mergedImage.getWidth(), mergedImage.getHeight(),
248 spriteImageDirective.spriteId, mergedImageFile);
249
250 spriteImageOutputStream.write(spriteImageBytes);
251 } catch (final IOException e) {
252 messageLog.warning(MessageType.CANNOT_WRITE_SPRITE_IMAGE, mergedImageFile, e.getMessage());
253 }
254 }
255
256
257
258
259
260
261
262
263
264
265
266
267 String getImageFile(String cssFile, String imagePath) {
268
269 final String path = resourceHandler.getResourcePath(cssFile, imagePath);
270
271
272 if (!imagePath.startsWith("/") && parameters.hasOutputDir()) {
273 return FileUtils.changeRoot(path, parameters.getRootDir(), parameters.getOutputDir());
274 }
275 return path;
276 }
277
278
279
280
281
282
283
284
285
286
287
288
289
290 static SpriteImage buildSpriteImage(SpriteImageOccurrence spriteImageOccurrence,
291 Map<SpriteReferenceOccurrence, BufferedImage> images, MessageLog messageLog) {
292
293 final SpriteImageLayout layout = spriteImageOccurrence.spriteImageDirective.layout;
294 final float spriteScale = spriteImageOccurrence.spriteImageDirective.scaleRatio;
295 final int leastCommonMultiple = SpriteImageBuilder.calculateLeastCommonMultiple(images, layout);
296
297
298 final boolean vertical = layout.equals(SpriteImageLayout.VERTICAL);
299 int dimension = leastCommonMultiple;
300 for (final Map.Entry<SpriteReferenceOccurrence, BufferedImage> entry : images.entrySet()) {
301 final BufferedImage image = entry.getValue();
302 final SpriteReferenceOccurrence spriteReferenceOccurrence = entry.getKey();
303
304
305 dimension = Math.max(dimension, vertical ? spriteReferenceOccurrence.getRequiredWidth(image, layout)
306 : spriteReferenceOccurrence.getRequiredHeight(image, layout));
307 }
308
309
310 if (dimension % leastCommonMultiple != 0) {
311 dimension += leastCommonMultiple - dimension % leastCommonMultiple;
312 }
313
314
315 int currentOffset = 0;
316 final Map<SpriteReferenceOccurrence, SpriteReferenceReplacement> spriteReplacements = new LinkedHashMap<>();
317 final Map<BufferedImageEqualsWrapper, Integer> renderedImageToOffset = new LinkedHashMap<>();
318 for (final Map.Entry<SpriteReferenceOccurrence, BufferedImage> entry : images.entrySet()) {
319 final SpriteReferenceOccurrence spriteReferenceOccurrence = entry.getKey();
320 final BufferedImage image = entry.getValue();
321
322 final BufferedImage rendered = spriteReferenceOccurrence.render(image, layout, dimension);
323 final BufferedImageEqualsWrapper imageWrapper = new BufferedImageEqualsWrapper(rendered);
324 Integer imageOffset = renderedImageToOffset.get(imageWrapper);
325 if (imageOffset == null) {
326
327 imageOffset = currentOffset;
328 renderedImageToOffset.put(imageWrapper, imageOffset);
329 currentOffset += vertical ? rendered.getHeight() : rendered.getWidth();
330 }
331
332 final float scaledImageWidth = spriteReferenceOccurrence.getRequiredWidth(image, layout) / spriteScale;
333 final float scaledImageHeight = spriteReferenceOccurrence.getRequiredHeight(image, layout) / spriteScale;
334 if (Math.round(scaledImageWidth) != scaledImageWidth
335 || Math.round(scaledImageHeight) != scaledImageHeight) {
336 messageLog.warning(MessageType.IMAGE_FRACTIONAL_SCALE_VALUE, spriteReferenceOccurrence.imagePath,
337 scaledImageWidth, scaledImageHeight);
338 }
339
340 final int adjustedImageOffset = Math.round(imageOffset / spriteScale);
341 spriteReplacements.put(spriteReferenceOccurrence,
342 spriteReferenceOccurrence.buildReplacement(layout, adjustedImageOffset));
343 }
344
345
346 final int spriteWidth = vertical ? dimension : currentOffset;
347 final int spriteHeight = vertical ? currentOffset : dimension;
348 if (spriteWidth == 0 || spriteHeight == 0) {
349 return null;
350 }
351
352 final float scaledWidth = spriteWidth / spriteScale;
353 final float scaledHeight = spriteHeight / spriteScale;
354 if (Math.round(scaledWidth) != scaledWidth || Math.round(scaledHeight) != scaledHeight) {
355 messageLog.warning(MessageType.FRACTIONAL_SCALE_VALUE, spriteImageOccurrence.spriteImageDirective.spriteId,
356 scaledWidth, scaledHeight);
357 }
358
359 final BufferedImage sprite = new BufferedImage(spriteWidth, spriteHeight, BufferedImage.TYPE_4BYTE_ABGR);
360
361 for (final Map.Entry<BufferedImageEqualsWrapper, Integer> entry : renderedImageToOffset.entrySet()) {
362
363 BufferedImageUtils.drawImage(entry.getKey().image, sprite, vertical ? 0 : entry.getValue(),
364 vertical ? entry.getValue() : 0);
365 }
366
367 return new SpriteImage(sprite, spriteImageOccurrence, spriteReplacements, spriteWidth, spriteHeight,
368 spriteScale);
369 }
370
371
372
373
374
375
376
377
378
379
380
381 static int calculateLeastCommonMultiple(Map<SpriteReferenceOccurrence, BufferedImage> images,
382 SpriteImageLayout layout) {
383 int leastCommonMultiple = 1;
384 for (final Map.Entry<SpriteReferenceOccurrence, BufferedImage> entry : images.entrySet()) {
385 final BufferedImage image = entry.getValue();
386 final SpriteReferenceOccurrence spriteReferenceOccurrence = entry.getKey();
387 if (image != null && SpriteAlignment.REPEAT
388 .equals(spriteReferenceOccurrence.spriteReferenceDirective.spriteLayoutProperties.alignment)) {
389 if (SpriteImageLayout.VERTICAL.equals(layout)) {
390 leastCommonMultiple = ArithmeticUtils.lcm(leastCommonMultiple,
391 spriteReferenceOccurrence.getRequiredWidth(image, layout));
392 } else {
393 leastCommonMultiple = ArithmeticUtils.lcm(leastCommonMultiple,
394 spriteReferenceOccurrence.getRequiredHeight(image, layout));
395 }
396 }
397 }
398 return leastCommonMultiple;
399 }
400
401
402
403
404
405
406
407
408
409 static Map<Integer, SpriteReferenceReplacement> getSpriteReplacementsByLineNumber(
410 Collection<SpriteReferenceReplacement> spriteReferenceReplacements) {
411 final Map<Integer, SpriteReferenceReplacement> result = new HashMap<>();
412
413 for (final SpriteReferenceReplacement spriteReferenceReplacement : spriteReferenceReplacements) {
414 result.put(spriteReferenceReplacement.spriteReferenceOccurrence.line, spriteReferenceReplacement);
415 }
416
417 return result;
418 }
419
420
421
422
423
424
425
426
427
428 static Map<Integer, SpriteImageOccurrence> getSpriteImageOccurrencesByLineNumber(
429 Collection<SpriteImageOccurrence> spriteImageOccurrences) {
430 final Map<Integer, SpriteImageOccurrence> result = new HashMap<>();
431
432 for (final SpriteImageOccurrence spriteImageOccurrence : spriteImageOccurrences) {
433 result.put(spriteImageOccurrence.line, spriteImageOccurrence);
434 }
435
436 return result;
437 }
438
439
440
441
442
443 static final class BufferedImageEqualsWrapper {
444
445
446 BufferedImage image;
447
448
449
450
451
452
453
454 BufferedImageEqualsWrapper(BufferedImage image) {
455 this.image = image;
456 }
457
458 @Override
459 public boolean equals(Object obj) {
460 if (!(obj instanceof BufferedImageEqualsWrapper)) {
461 return false;
462 }
463
464 if (obj == this) {
465 return true;
466 }
467
468 final BufferedImage other = ((BufferedImageEqualsWrapper) obj).image;
469
470 boolean equal = other.getWidth() == image.getWidth() && other.getHeight() == image.getHeight()
471 && other.getType() == image.getType();
472
473 if (equal) {
474 for (int y = 0; y < image.getHeight(); y++) {
475 for (int x = 0; x < image.getWidth(); x++) {
476 if (ignoreFullTransparency(image.getRGB(x, y)) != ignoreFullTransparency(other.getRGB(x, y))) {
477 return false;
478 }
479 }
480 }
481 }
482
483 return equal;
484 }
485
486 @Override
487 public int hashCode() {
488 if (image == null) {
489 return 0;
490 }
491
492 int hash = image.getWidth() ^ image.getHeight() << 16;
493
494
495 final int xIncrement = image.getWidth() > 7 ? image.getWidth() >> 2 : 1;
496 final int yIncrement = image.getHeight() > 7 ? image.getHeight() >> 2 : 1;
497
498 for (int y = 0; y < image.getHeight(); y += yIncrement) {
499 for (int x = 0; x < image.getWidth(); x += xIncrement) {
500 hash ^= ignoreFullTransparency(image.getRGB(x, y));
501 }
502 }
503
504 return hash;
505 }
506
507
508
509
510
511
512
513
514
515
516
517 private static int ignoreFullTransparency(int pixel) {
518 if ((pixel & 0xff000000) == 0x00000000) {
519 return 0;
520 }
521 return pixel;
522 }
523 }
524 }