1 package org.sentrysoftware.maven.skin;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 import java.awt.Image;
24 import java.awt.image.BufferedImage;
25 import java.io.File;
26 import java.io.IOException;
27 import java.nio.file.Path;
28 import java.nio.file.Paths;
29 import java.util.ArrayList;
30 import java.util.List;
31 import java.util.regex.Pattern;
32 import java.util.stream.Collectors;
33
34 import javax.imageio.IIOImage;
35 import javax.imageio.ImageIO;
36 import javax.imageio.ImageWriteParam;
37 import javax.imageio.ImageWriter;
38 import javax.imageio.spi.IIORegistry;
39 import javax.imageio.stream.FileImageOutputStream;
40
41 import org.apache.velocity.tools.config.DefaultKey;
42 import org.jsoup.nodes.Element;
43
44 import com.luciad.imageio.webp.WebPImageReaderSpi;
45 import com.luciad.imageio.webp.WebPImageWriterSpi;
46 import com.luciad.imageio.webp.WebPWriteParam;
47
48
49
50
51
52 @DefaultKey("imageTool")
53 public class ImageTool {
54
55 static {
56
57
58
59 IIORegistry iioRegistry = IIORegistry.getDefaultInstance();
60 iioRegistry.registerServiceProvider(new WebPImageWriterSpi());
61 iioRegistry.registerServiceProvider(new WebPImageReaderSpi());
62
63 }
64
65
66
67
68
69
70
71
72 private static final Pattern ABSOLUTE_URL_PATTERN = Pattern.compile("^(?:[a-z]+:)?//", Pattern.CASE_INSENSITIVE);
73
74
75
76
77 public ImageTool() {
78
79 }
80
81
82
83
84
85
86
87
88
89
90
91
92 protected static boolean isAbsoluteUrl(final String path) {
93 return ABSOLUTE_URL_PATTERN.matcher(path).find();
94 }
95
96
97
98
99
100
101
102
103
104
105
106
107 public Element checkImageLinks(
108 final Element body,
109 final String basedir,
110 final String currentDocument
111 ) throws IOException {
112
113
114 List<String> errorList = new ArrayList<String>();
115
116
117 Path basedirPath = Paths.get(basedir).toAbsolutePath();
118
119
120 Path documentPath = Paths.get(basedir, currentDocument);
121
122 Path parentPath = documentPath.getParent();
123 if (parentPath == null) {
124 throw new IOException("Couldn't get the parent path of " + currentDocument);
125 }
126
127
128 List<Element> elements = body.select("img");
129
130
131 for (Element element : elements) {
132
133
134 String imageSrc = element.attr("src");
135 if (imageSrc.isEmpty()) {
136 continue;
137 }
138
139
140 if (isAbsoluteUrl(imageSrc)) {
141 continue;
142 }
143
144
145 Path sourcePath = documentPath.resolveSibling(imageSrc);
146 File sourceFile = sourcePath.toFile();
147
148
149 if (!sourcePath.toAbsolutePath().startsWith(basedirPath)) {
150 continue;
151 }
152
153
154
155 Path recalculatedPath = parentPath.toRealPath().relativize(sourcePath.toRealPath());
156 String sourcePathSlashString = sourcePath.toString().replace('\\', '/');
157 String recalculatedPathSlashString = recalculatedPath.toString().replace('\\', '/');
158 if (!recalculatedPathSlashString.endsWith(sourcePathSlashString) && !sourcePathSlashString.endsWith(recalculatedPathSlashString)) {
159 errorList.add("Referenced image " + imageSrc + " in " + currentDocument + " doesn't match case of actual file " + recalculatedPath);
160 }
161
162
163 if (!sourceFile.isFile()) {
164 errorList.add("Referenced image " + imageSrc + " in " + currentDocument + " doesn't exist");
165 }
166
167 }
168
169
170 if (!errorList.isEmpty()) {
171 throw new IOException(errorList.stream().collect(Collectors.joining("\n")));
172 }
173
174 return body;
175
176 }
177
178
179
180
181
182
183
184
185 protected static String getExtension(final File file) {
186 String name = file.getName();
187 int dotIndex = name.lastIndexOf('.');
188 if (dotIndex > -1) {
189 return name.substring(dotIndex + 1);
190 }
191 return "";
192 }
193
194
195
196
197
198
199
200 protected static String getNameWithoutExtension(final File file) {
201 String name = file.getName();
202 int dotIndex = name.lastIndexOf('.');
203 if (dotIndex > -1) {
204 return name.substring(0, dotIndex);
205 }
206 return name;
207 }
208
209
210
211
212
213
214
215
216
217
218
219 protected static File createThumbnail(
220 final File sourceFile,
221 final String thumbnailMark,
222 final int maxWidth,
223 final int maxHeight
224 ) throws IOException {
225
226
227 if (!sourceFile.isFile()) {
228 throw new IOException(sourceFile.getAbsolutePath() + " does not exist");
229 }
230
231
232 File destination = new File(sourceFile.getParent(), getNameWithoutExtension(sourceFile) + thumbnailMark + ".jpg");
233
234
235 if (Helper.getLastModifiedTime(sourceFile) < Helper.getLastModifiedTime(destination)) {
236 return destination;
237 }
238
239
240 BufferedImage sourceImage = ImageIO.read(sourceFile);
241 String imageType = getExtension(sourceFile).toLowerCase();
242
243
244 int targetWidth = sourceImage.getWidth();
245 int targetHeight = sourceImage.getHeight();
246
247 if (maxWidth > 0 && targetWidth > maxWidth) {
248 targetHeight = targetHeight * maxWidth / targetWidth;
249 targetWidth = maxWidth;
250 }
251 if (maxHeight > 0 && targetHeight > maxHeight) {
252 targetWidth = targetWidth * maxHeight / targetHeight;
253 targetHeight = maxHeight;
254 }
255
256
257 Image resultingImage = sourceImage.getScaledInstance(targetWidth, targetHeight, Image.SCALE_SMOOTH);
258 BufferedImage outputImage = new BufferedImage(targetWidth, targetHeight, BufferedImage.TYPE_INT_RGB);
259 outputImage.getGraphics().drawImage(resultingImage, 0, 0, null);
260
261
262 ImageIO.write(outputImage, imageType, destination);
263
264 return destination;
265
266 }
267
268
269
270
271
272
273
274
275 protected static File saveImageFileAsWebp(final File sourceFile) throws IOException {
276
277
278 if (!sourceFile.isFile()) {
279 throw new IOException(sourceFile.getAbsolutePath() + " does not exist");
280 }
281
282
283 String webpImagePath = getNameWithoutExtension(sourceFile) + ".webp";
284 File webpFile = new File(sourceFile.getParent(), webpImagePath);
285
286
287 if (Helper.getLastModifiedTime(sourceFile) < Helper.getLastModifiedTime(webpFile)) {
288 return webpFile;
289 }
290
291
292 BufferedImage sourceImage = ImageIO.read(sourceFile);
293 if (sourceImage == null) {
294 return null;
295 }
296
297
298 String imageType = getExtension(sourceFile).toLowerCase();
299 if ("webp".equals(imageType)) {
300 return null;
301 }
302
303
304 ImageWriter writer = ImageIO.getImageWritersBySuffix("webp").next();
305
306
307 WebPWriteParam writeParam = new WebPWriteParam(writer.getLocale());
308 writeParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
309 if ("jpeg".equals(imageType) || "jpg".equals(imageType)) {
310 writeParam.setCompressionType(writeParam.getCompressionTypes()[WebPWriteParam.LOSSY_COMPRESSION]);
311 } else {
312 writeParam.setCompressionType(writeParam.getCompressionTypes()[WebPWriteParam.LOSSLESS_COMPRESSION]);
313 }
314
315
316 writer.setOutput(new FileImageOutputStream(webpFile));
317
318
319 writer.write(null, new IIOImage(sourceImage, null, null), writeParam);
320
321
322 return webpFile;
323
324 }
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339 public Element convertImagesToWebp(
340 final Element body,
341 final String selector,
342 final String basedir,
343 final String currentDocument
344 ) throws IOException {
345
346
347 Path basedirPath = Paths.get(basedir).toAbsolutePath();
348
349
350 Path documentPath = Paths.get(basedir, currentDocument);
351
352 Path parentPath = documentPath.getParent();
353 if (parentPath == null) {
354 throw new IOException("Couldn't get parent path of " + currentDocument);
355 }
356
357
358 List<Element> elements = body.select(selector);
359
360
361 for (Element element : elements) {
362
363
364 String imageSrc = element.attr("src");
365 if (imageSrc.isEmpty()) {
366 continue;
367 }
368
369
370 if (isAbsoluteUrl(imageSrc)) {
371 continue;
372 }
373
374
375 Path sourcePath = documentPath.resolveSibling(imageSrc);
376 File sourceFile = sourcePath.toFile();
377
378
379 if (!sourcePath.toAbsolutePath().startsWith(basedirPath)) {
380 continue;
381 }
382
383
384 if (!sourceFile.isFile()) {
385 throw new IOException(sourceFile.getAbsolutePath() + " (referenced as " + imageSrc + ") does not exist");
386 }
387
388
389 File webpFile = saveImageFileAsWebp(sourceFile);
390 if (webpFile == null) {
391 continue;
392 }
393
394
395 String webpSrc = parentPath.relativize(webpFile.toPath()).toString().replace('\\', '/');
396
397
398 element
399 .wrap("<picture>")
400 .parent()
401 .prependElement("source")
402 .attr("srcset", webpSrc)
403 .attr("type", "image/webp");
404
405 }
406
407 return body;
408
409 }
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425 public Element explicitImageSize(
426 final Element body,
427 final String selector,
428 final String basedir,
429 final String currentDocument
430 ) throws IOException {
431
432
433 Path basedirPath = Paths.get(basedir).toAbsolutePath();
434
435
436 Path documentPath = Paths.get(basedir, currentDocument);
437
438
439 List<Element> elements = body.select(selector);
440
441
442 for (Element element : elements) {
443
444
445 String imageSrc = element.attr("src");
446 if (imageSrc.isEmpty()) {
447 continue;
448 }
449
450
451 if (isAbsoluteUrl(imageSrc)) {
452 continue;
453 }
454
455
456 if (element.attr("style").matches("(^|\\b)(width:|height:)")
457 || !element.attr("height").isEmpty()
458 || !element.attr("width").isEmpty()) {
459 continue;
460 }
461
462
463 Path sourcePath = documentPath.resolveSibling(imageSrc);
464 File sourceFile = sourcePath.toFile();
465
466
467 if (!sourcePath.toAbsolutePath().startsWith(basedirPath)) {
468 continue;
469 }
470
471
472 if (!sourceFile.isFile()) {
473 throw new IOException(sourceFile.getAbsolutePath() + " (referenced as " + imageSrc + ") does not exist");
474 }
475
476
477 BufferedImage sourceImage = ImageIO.read(sourceFile);
478 if (sourceImage == null) {
479 continue;
480 }
481
482
483 element
484 .attr("width", String.valueOf(sourceImage.getWidth()))
485 .attr("height", String.valueOf(sourceImage.getHeight()))
486 .attr("style", String.format(
487 "width: %dpx; height: %dpx;%s",
488 sourceImage.getWidth(),
489 sourceImage.getHeight(),
490 element.attr("style")
491 ));
492
493 }
494
495 return body;
496
497 }
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528 public Element convertImagesToThumbnails(
529 final Element body,
530 final String selector,
531 final String basedir,
532 final String currentDocument,
533 final int maxWidth,
534 final int maxHeight,
535 final String wrapTemplate
536 ) throws IOException {
537
538
539 Path basedirPath = Paths.get(basedir).toAbsolutePath();
540
541
542 Path documentPath = Paths.get(basedir, currentDocument);
543
544 Path parentPath = documentPath.getParent();
545 if (parentPath == null) {
546 throw new IOException("Couldn't get parent path of " + currentDocument);
547 }
548
549
550 List<Element> elements = body.select(selector);
551
552
553 for (Element element : elements) {
554
555
556 String imageSrc = element.attr("src");
557 if (imageSrc.isEmpty()) {
558 continue;
559 }
560
561
562 if (isAbsoluteUrl(imageSrc)) {
563 continue;
564 }
565
566
567 String imageAlt = element.attr("alt");
568
569
570 Path sourcePath = documentPath.resolveSibling(imageSrc);
571 File sourceFile = sourcePath.toFile();
572
573
574 if (!sourcePath.toAbsolutePath().startsWith(basedirPath)) {
575 continue;
576 }
577
578
579 if (!sourceFile.isFile()) {
580 throw new IOException(sourceFile.getAbsolutePath() + " (referenced as " + imageSrc + ") does not exist");
581 }
582
583
584 BufferedImage sourceImage = ImageIO.read(sourceFile);
585 int sourceWidth = sourceImage.getWidth();
586 int sourceHeight = sourceImage.getHeight();
587
588
589 File thumbnailFile = createThumbnail(sourceFile, "-thumbnail", maxWidth, maxHeight);
590
591
592 BufferedImage thumbnailImage = ImageIO.read(thumbnailFile);
593 int thumbnailWidth = thumbnailImage.getWidth();
594 int thumbnailHeight = thumbnailImage.getHeight();
595
596
597 String thumbnailSrc = parentPath.relativize(thumbnailFile.toPath()).toString().replace('\\', '/');
598
599
600 String wrapHtml = wrapTemplate
601 .replaceAll("%imgWidth%", String.valueOf(sourceWidth))
602 .replaceAll("%imgHeight%", String.valueOf(sourceHeight))
603 .replaceAll("%thumbWidth%", String.valueOf(thumbnailWidth))
604 .replaceAll("%thumbHeight%", String.valueOf(thumbnailHeight))
605 .replaceAll("%thumbSrc%", thumbnailSrc)
606 .replaceAll("%imgAlt%", imageAlt);
607
608
609
610 Element pictureElement = element;
611 if ("PICTURE".equalsIgnoreCase(element.parent().tagName())) {
612 pictureElement = element.parent();
613 }
614 pictureElement.wrap(wrapHtml);
615
616 }
617
618 return body;
619
620 }
621
622
623 }