1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 package io.wcm.handler.mediasource.inline;
21
22 import java.io.IOException;
23 import java.io.InputStream;
24 import java.util.ArrayList;
25 import java.util.Date;
26 import java.util.List;
27 import java.util.Objects;
28
29 import javax.jcr.Node;
30 import javax.jcr.Property;
31 import javax.jcr.RepositoryException;
32
33 import org.apache.commons.io.FilenameUtils;
34 import org.apache.commons.io.IOUtils;
35 import org.apache.commons.lang3.StringUtils;
36 import org.apache.sling.api.adapter.Adaptable;
37 import org.apache.sling.api.adapter.SlingAdaptable;
38 import org.apache.sling.api.resource.Resource;
39 import org.apache.sling.api.resource.ValueMap;
40 import org.jetbrains.annotations.NotNull;
41 import org.jetbrains.annotations.Nullable;
42
43 import com.day.cq.commons.jcr.JcrConstants;
44 import com.day.image.Layer;
45
46 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
47 import io.wcm.handler.media.CropDimension;
48 import io.wcm.handler.media.Dimension;
49 import io.wcm.handler.media.Media;
50 import io.wcm.handler.media.MediaArgs;
51 import io.wcm.handler.media.MediaFileType;
52 import io.wcm.handler.media.Rendition;
53 import io.wcm.handler.media.UriTemplate;
54 import io.wcm.handler.media.UriTemplateType;
55 import io.wcm.handler.media.format.MediaFormat;
56 import io.wcm.handler.media.format.Ratio;
57 import io.wcm.handler.media.format.impl.MediaFormatSupport;
58 import io.wcm.handler.media.impl.ImageFileServlet;
59 import io.wcm.handler.media.impl.ImageFileServletSelector;
60 import io.wcm.handler.media.impl.ImageTransformation;
61 import io.wcm.handler.media.impl.JcrBinary;
62 import io.wcm.handler.media.impl.MediaFileServletConstants;
63 import io.wcm.handler.media.spi.MediaHandlerConfig;
64 import io.wcm.handler.mediasource.ngdm.impl.MediaArgsDimension;
65 import io.wcm.handler.url.UrlHandler;
66 import io.wcm.sling.commons.adapter.AdaptTo;
67 import io.wcm.wcm.commons.caching.ModificationDate;
68
69
70
71
72 final class InlineRendition extends SlingAdaptable implements Rendition {
73
74 private final Adaptable adaptable;
75 private final Resource resource;
76 private final MediaArgs mediaArgs;
77 private final MediaHandlerConfig mediaHandlerConfig;
78 private final String fileName;
79 private final String fileExtension;
80 private final String originalFileExtension;
81 private final Dimension imageDimension;
82 private final Dimension maxImageDimension;
83 private final String url;
84 private CropDimension cropDimension;
85 private final Integer rotation;
86 private MediaFormat resolvedMediaFormat;
87 private boolean fallback;
88
89
90
91
92 private static final Dimension SCALING_NOT_POSSIBLE_DIMENSION = new Dimension(-1, -1);
93
94
95
96
97
98
99
100
101 @SuppressWarnings("java:S3776")
102 InlineRendition(Resource resource, Media media, MediaArgs mediaArgs, MediaHandlerConfig mediaHandlerConfig,
103 String fileName, Adaptable adaptable) {
104 this.resource = resource;
105 this.mediaArgs = mediaArgs;
106 this.mediaHandlerConfig = mediaHandlerConfig;
107 this.adaptable = adaptable;
108
109 this.rotation = media.getRotation();
110 this.cropDimension = media.getCropDimension();
111
112
113 this.originalFileExtension = FilenameUtils.getExtension(fileName);
114
115
116 boolean isImage = MediaFileType.isImage(this.originalFileExtension);
117 boolean isVectorImage = MediaFileType.isVectorImage(this.originalFileExtension);
118
119 Dimension dimension = null;
120 Dimension maxDimension = null;
121 Dimension scaledDimension = null;
122 String processedFileName = fileName;
123 if (isImage) {
124
125 List<Dimension> dimensionCandidates = getImageOrCroppedDimensions();
126 for (int i = 0; i < dimensionCandidates.size(); i++) {
127 dimension = dimensionCandidates.get(i);
128 maxDimension = dimension;
129 if (isVectorImage && (this.rotation != null || this.cropDimension != null)) {
130
131 scaledDimension = SCALING_NOT_POSSIBLE_DIMENSION;
132 }
133 else {
134
135 scaledDimension = getScaledDimension(dimension);
136 if (scaledDimension != null && isValidScalingDimension(scaledDimension)) {
137
138 dimension = scaledDimension;
139
140 if (!isVectorImage) {
141 processedFileName = ImageFileServlet.getImageFileName(processedFileName,
142 mediaArgs.getEnforceOutputFileExtension());
143 }
144 }
145 }
146 if (isValidScalingDimension(scaledDimension)) {
147 if (i > 0) {
148
149 this.cropDimension = null;
150 this.fallback = true;
151 }
152 break;
153 }
154 }
155 if (!isValidScalingDimension(scaledDimension) && mediaArgs.isAutoCrop() && !isVectorImage && dimension != null) {
156
157 InlineAutoCropping autoCropping = new InlineAutoCropping(dimension, mediaArgs);
158 List<CropDimension> autoCropDimensions = autoCropping.calculateAutoCropDimensions();
159 for (CropDimension autoCropDimension : autoCropDimensions) {
160 scaledDimension = getScaledDimension(autoCropDimension);
161 maxDimension = autoCropDimension;
162 if (scaledDimension == null) {
163 scaledDimension = autoCropDimension;
164 }
165 if (isValidScalingDimension(scaledDimension)) {
166
167 dimension = scaledDimension;
168 this.cropDimension = autoCropDimension;
169
170 if (!isVectorImage) {
171 processedFileName = ImageFileServlet.getImageFileName(processedFileName,
172 mediaArgs.getEnforceOutputFileExtension());
173 }
174 break;
175 }
176 }
177 }
178 }
179 this.fileName = processedFileName;
180 this.fileExtension = FilenameUtils.getExtension(processedFileName);
181 this.imageDimension = dimension;
182 this.maxImageDimension = maxDimension;
183
184
185 this.url = buildMediaUrl(scaledDimension);
186
187
188 MediaFormat firstMediaFormat = MediaArgsDimension.getFirstMediaFormat(mediaArgs);
189 if (url != null && firstMediaFormat != null) {
190 this.resolvedMediaFormat = firstMediaFormat;
191 }
192 }
193
194 private boolean isValidScalingDimension(@Nullable Dimension dimension) {
195 return dimension == null || !dimension.equals(SCALING_NOT_POSSIBLE_DIMENSION);
196 }
197
198
199
200
201
202
203
204 private List<Dimension> getImageOrCroppedDimensions() {
205 List<Dimension> dimensions = new ArrayList<>();
206
207 Dimension originalDimension = getImageDimension();
208 if (originalDimension != null) {
209 if (this.cropDimension != null) {
210 dimensions.add(this.cropDimension);
211 }
212 dimensions.add(originalDimension);
213 }
214
215 return dimensions;
216 }
217
218
219
220
221
222 private Dimension getImageDimension() {
223 Dimension dimension = null;
224
225
226 Layer layer = this.resource.adaptTo(Layer.class);
227 if (layer != null) {
228 dimension = new Dimension(layer.getWidth(), layer.getHeight());
229 }
230
231 return dimension;
232 }
233
234
235
236
237
238
239
240 private @Nullable Dimension getScaledDimension(@NotNull Dimension originalDimension) {
241
242
243 Dimension requestedDimension = MediaArgsDimension.getRequestedDimension(mediaArgs);
244 double requestedRatio = MediaArgsDimension.getRequestedRatio(mediaArgs);
245 double imageRatio = Ratio.get(originalDimension);
246 if (requestedRatio > 0 && !Ratio.matches(requestedRatio, imageRatio)) {
247 return SCALING_NOT_POSSIBLE_DIMENSION;
248 }
249
250 boolean scaleWidth = (requestedDimension.getWidth() > 0
251 && requestedDimension.getWidth() != originalDimension.getWidth());
252 boolean scaleHeight = (requestedDimension.getHeight() > 0
253 && requestedDimension.getHeight() != originalDimension.getHeight());
254 if (scaleWidth || scaleHeight) {
255 long requestedWidth = requestedDimension.getWidth();
256 long requestedHeight = requestedDimension.getHeight();
257
258
259 if (requestedWidth == 0 && requestedHeight > 0) {
260 requestedWidth = Math.round(requestedHeight * imageRatio);
261 }
262 else if (requestedWidth > 0 && requestedHeight == 0) {
263 requestedHeight = Math.round(requestedWidth / imageRatio);
264 }
265
266
267 requestedRatio = Ratio.get(requestedWidth, requestedHeight);
268
269
270 if (!Ratio.matches(imageRatio, requestedRatio)
271 || (originalDimension.getWidth() < requestedWidth)
272 || (originalDimension.getHeight() < requestedHeight)) {
273 return SCALING_NOT_POSSIBLE_DIMENSION;
274 }
275 else {
276 return new Dimension(requestedWidth, requestedHeight);
277 }
278
279 }
280
281 return null;
282 }
283
284
285
286
287
288 private String buildMediaUrl(Dimension scaledDimension) {
289
290
291 if (!isMatchingFileExtension()) {
292 return null;
293 }
294
295
296 if (scaledDimension != null) {
297
298
299 if (scaledDimension.equals(SCALING_NOT_POSSIBLE_DIMENSION)) {
300 return null;
301 }
302
303
304 return buildScaledMediaUrl(scaledDimension, this.cropDimension);
305 }
306
307
308 if (this.cropDimension != null || this.rotation != null) {
309 return buildScaledMediaUrl(this.cropDimension != null ? this.cropDimension : this.imageDimension, this.cropDimension);
310 }
311
312 if (mediaArgs.isContentDispositionAttachment()) {
313
314 return buildDownloadMediaUrl();
315 }
316 else if (MediaFileType.isBrowserImage(getFileExtension()) || !MediaFileType.isImage(getFileExtension())) {
317 if (enforceVirtualRendition()) {
318
319 return buildScaledMediaUrl(this.imageDimension, null);
320 }
321 else {
322
323 return buildNativeMediaUrl();
324 }
325 }
326 else {
327
328 return buildScaledMediaUrl(this.imageDimension, null);
329 }
330 }
331
332 private boolean enforceVirtualRendition() {
333 if (MediaFileType.isImage(getFileExtension()) && !MediaFileType.isVectorImage(getFileExtension())) {
334 if (mediaHandlerConfig.enforceVirtualRenditions()) {
335 return true;
336 }
337 if (mediaArgs.getEnforceOutputFileExtension() != null) {
338 return !StringUtils.equalsIgnoreCase(getFileExtension(), mediaArgs.getEnforceOutputFileExtension());
339 }
340 }
341 return false;
342 }
343
344
345
346
347
348 private String buildNativeMediaUrl() {
349 String path = null;
350
351 Resource parentResource = this.resource.getParent();
352 if (parentResource != null && JcrBinary.isNtFile(parentResource)) {
353
354 if (StringUtils.equals(parentResource.getName(), getFileName())) {
355 path = parentResource.getPath();
356 }
357
358 else {
359 path = parentResource.getPath() + "./" + getFileName();
360 }
361 }
362 else {
363
364 path = this.resource.getPath() + "./" + getFileName();
365 }
366
367
368 UrlHandler urlHandler = AdaptTo.notNull(this.adaptable, UrlHandler.class);
369 return urlHandler.get(path).urlMode(this.mediaArgs.getUrlMode()).buildExternalResourceUrl(this.resource);
370 }
371
372
373
374
375
376 @SuppressFBWarnings("NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE")
377 @SuppressWarnings("java:S1075")
378 private String buildScaledMediaUrl(@NotNull Dimension dimension, @Nullable CropDimension mediaUrlCropDimension) {
379
380 if (isVectorImage()) {
381
382 return buildNativeMediaUrl();
383 }
384
385 String resourcePath = this.resource.getPath();
386
387
388 Resource parentResource = this.resource.getParent();
389 if (parentResource != null && JcrBinary.isNtFile(parentResource)) {
390 resourcePath = parentResource.getPath();
391 }
392
393
394 String path = resourcePath
395 + "." + ImageFileServletSelector.build(dimension.getWidth(), dimension.getHeight(),
396 mediaUrlCropDimension, this.rotation, this.mediaArgs.getImageQualityPercentage(),
397 this.mediaArgs.isContentDispositionAttachment())
398 + "." + MediaFileServletConstants.EXTENSION + "/"
399
400 + ImageFileServlet.getImageFileName(getFileName(),
401 mediaArgs.getEnforceOutputFileExtension());
402
403
404 UrlHandler urlHandler = AdaptTo.notNull(this.adaptable, UrlHandler.class);
405 return urlHandler.get(path).urlMode(this.mediaArgs.getUrlMode()).buildExternalResourceUrl(this.resource);
406 }
407
408
409
410
411
412 @SuppressWarnings("java:S1075")
413 private String buildDownloadMediaUrl() {
414 String resourcePath = this.resource.getPath();
415
416
417 Resource parentResource = this.resource.getParent();
418 if (parentResource != null && JcrBinary.isNtFile(parentResource)) {
419 resourcePath = parentResource.getPath();
420 }
421
422
423 String path = resourcePath + "." + MediaFileServletConstants.SELECTOR
424 + "." + MediaFileServletConstants.SELECTOR_DOWNLOAD
425 + "." + MediaFileServletConstants.EXTENSION + "/" + getFileName();
426
427
428 UrlHandler urlHandler = AdaptTo.notNull(this.adaptable, UrlHandler.class);
429 return urlHandler.get(path).urlMode(this.mediaArgs.getUrlMode()).buildExternalResourceUrl(this.resource);
430 }
431
432
433
434
435
436 private boolean isMatchingFileExtension() {
437 String[] extensions = MediaFormatSupport.getRequestedFileExtensions(mediaArgs);
438 if (extensions == null) {
439
440 return false;
441 }
442 if (extensions.length == 0) {
443 return true;
444 }
445 for (String extension : extensions) {
446 if (StringUtils.equalsIgnoreCase(extension, this.originalFileExtension)) {
447 return true;
448 }
449 }
450 return false;
451 }
452
453 @Override
454 public String getUrl() {
455 return this.url;
456 }
457
458 @Override
459 public String getPath() {
460 return this.resource.getPath();
461 }
462
463 @Override
464 public String getFileName() {
465 if (this.url != null) {
466 return FilenameUtils.getName(this.url);
467 }
468 return this.fileName;
469 }
470
471 @Override
472 public String getFileExtension() {
473 if (this.url != null) {
474 return FilenameUtils.getExtension(this.url);
475 }
476 return StringUtils.defaultString(this.fileExtension, this.originalFileExtension);
477 }
478
479 @Override
480 @SuppressWarnings("java:S112")
481 public long getFileSize() {
482 Node node = this.resource.adaptTo(Node.class);
483 if (node != null) {
484 try {
485 Property data = node.getProperty(JcrConstants.JCR_DATA);
486 return data.getBinary().getSize();
487 }
488 catch (RepositoryException ex) {
489 throw new RuntimeException("Unable to detect binary file size for " + this.resource.getPath(), ex);
490 }
491 }
492 else {
493
494 try {
495 InputStream is = this.resource.getValueMap().get(JcrConstants.JCR_DATA, InputStream.class);
496 return IOUtils.toByteArray(is).length;
497 }
498 catch (IOException ex) {
499 throw new RuntimeException("Unable to detect binary file size for " + this.resource.getPath(), ex);
500 }
501 }
502 }
503
504 @Override
505 public String getMimeType() {
506 return JcrBinary.getMimeType(this.resource);
507 }
508
509 @Override
510 public Date getModificationDate() {
511 return ModificationDate.get(this.resource);
512 }
513
514 @Override
515 public MediaFormat getMediaFormat() {
516 return resolvedMediaFormat;
517 }
518
519 @Override
520 @SuppressWarnings("null")
521 public ValueMap getProperties() {
522 return this.resource.getValueMap();
523 }
524
525 @Override
526 public boolean isImage() {
527 return MediaFileType.isImage(getFileExtension());
528 }
529
530 @Override
531 public boolean isBrowserImage() {
532 return MediaFileType.isBrowserImage(getFileExtension());
533 }
534
535 @Override
536 public boolean isVectorImage() {
537 return MediaFileType.isVectorImage(getFileExtension());
538 }
539
540 @Override
541 public boolean isDownload() {
542 return !isImage();
543 }
544
545 @Override
546 public long getWidth() {
547 if (imageDimension != null) {
548 return ImageTransformation.rotateMapDimension(imageDimension, rotation).getWidth();
549 }
550 else {
551 return 0;
552 }
553 }
554
555 @Override
556 public long getHeight() {
557 if (imageDimension != null) {
558 return ImageTransformation.rotateMapDimension(imageDimension, rotation).getHeight();
559 }
560 else {
561 return 0;
562 }
563 }
564
565 @Override
566 public boolean isFallback() {
567 return fallback;
568 }
569
570 @Override
571 public @NotNull UriTemplate getUriTemplate(@NotNull UriTemplateType type) {
572 if (type == UriTemplateType.CROP_CENTER) {
573 throw new IllegalArgumentException("CROP_CENTER not supported for rendition URI templates.");
574 }
575 if (!isImage() || isVectorImage()) {
576 throw new UnsupportedOperationException("Unable to build URI template for " + resource.getPath());
577 }
578 if (this.maxImageDimension == null) {
579 throw new IllegalStateException("Unable to detect dimension for inline image: " + resource.getPath());
580 }
581
582 Dimension dimension = ImageTransformation.rotateMapDimension(maxImageDimension, rotation);
583 return new InlineUriTemplate(type, dimension, this.resource, fileName,
584 this.cropDimension, this.rotation, mediaArgs, adaptable);
585 }
586
587 @Override
588 @SuppressWarnings({ "unchecked", "null" })
589 public <AdapterType> AdapterType adaptTo(Class<AdapterType> type) {
590 if (type == Resource.class) {
591 return (AdapterType)this.resource;
592 }
593 else if (type == Layer.class && isImage()) {
594 return (AdapterType)getLayer();
595 }
596 else if (type == InputStream.class) {
597 return (AdapterType)this.resource.adaptTo(InputStream.class);
598 }
599 return super.adaptTo(type);
600 }
601
602 private Layer getLayer() {
603 Layer layer = this.resource.adaptTo(Layer.class);
604 if (layer != null) {
605 if (cropDimension != null) {
606 layer.crop(cropDimension.getRectangle());
607 }
608 if (rotation != null) {
609 layer.rotate(rotation);
610 }
611 long width = getWidth();
612 long height = getHeight();
613 if (width <= layer.getWidth() && height <= layer.getHeight()) {
614 layer.resize((int)width, (int)height);
615 }
616 }
617 return layer;
618 }
619
620 @Override
621 public String toString() {
622 return Objects.toString(url, "#invalid");
623 }
624
625 }