001
014
015 package com.liferay.portal.image;
016
017 import com.liferay.portal.kernel.concurrent.FutureConverter;
018 import com.liferay.portal.kernel.exception.ImageResolutionException;
019 import com.liferay.portal.kernel.image.ImageBag;
020 import com.liferay.portal.kernel.image.ImageMagick;
021 import com.liferay.portal.kernel.image.ImageTool;
022 import com.liferay.portal.kernel.io.unsync.UnsyncByteArrayOutputStream;
023 import com.liferay.portal.kernel.log.Log;
024 import com.liferay.portal.kernel.log.LogFactoryUtil;
025 import com.liferay.portal.kernel.model.Image;
026 import com.liferay.portal.kernel.security.pacl.DoPrivileged;
027 import com.liferay.portal.kernel.util.ArrayUtil;
028 import com.liferay.portal.kernel.util.PropsKeys;
029 import com.liferay.portal.kernel.util.StringBundler;
030 import com.liferay.portal.kernel.util.StringUtil;
031 import com.liferay.portal.model.impl.ImageImpl;
032 import com.liferay.portal.util.FileImpl;
033 import com.liferay.portal.util.PropsUtil;
034 import com.liferay.portal.util.PropsValues;
035
036 import java.awt.AlphaComposite;
037 import java.awt.Graphics;
038 import java.awt.Graphics2D;
039 import java.awt.Rectangle;
040 import java.awt.color.ColorSpace;
041 import java.awt.image.BufferedImage;
042 import java.awt.image.ColorModel;
043 import java.awt.image.DataBuffer;
044 import java.awt.image.IndexColorModel;
045 import java.awt.image.RenderedImage;
046 import java.awt.image.SampleModel;
047 import java.awt.image.WritableRaster;
048
049 import java.io.ByteArrayInputStream;
050 import java.io.File;
051 import java.io.IOException;
052 import java.io.InputStream;
053 import java.io.OutputStream;
054
055 import java.util.Arrays;
056 import java.util.Hashtable;
057 import java.util.Iterator;
058 import java.util.LinkedList;
059 import java.util.Queue;
060 import java.util.concurrent.Future;
061
062 import javax.imageio.ImageIO;
063 import javax.imageio.ImageReader;
064 import javax.imageio.spi.IIORegistry;
065 import javax.imageio.spi.ImageReaderSpi;
066 import javax.imageio.stream.ImageInputStream;
067
068 import net.jmge.gif.Gif89Encoder;
069
070 import org.im4java.core.IMOperation;
071
072 import org.monte.media.jpeg.CMYKJPEGImageReaderSpi;
073
074
079 @DoPrivileged
080 public class ImageToolImpl implements ImageTool {
081
082 public static ImageTool getInstance() {
083 return _instance;
084 }
085
086 public void afterPropertiesSet() {
087 Class<?> clazz = getClass();
088
089 ClassLoader classLoader = clazz.getClassLoader();
090
091 try {
092 InputStream is = classLoader.getResourceAsStream(
093 PropsUtil.get(PropsKeys.IMAGE_DEFAULT_SPACER));
094
095 if (is == null) {
096 _log.error("Default spacer is not available");
097 }
098
099 _defaultSpacer = getImage(is);
100 }
101 catch (Exception e) {
102 _log.error(
103 "Unable to configure the default spacer: " + e.getMessage());
104 }
105
106 try {
107 InputStream is = classLoader.getResourceAsStream(
108 PropsUtil.get(PropsKeys.IMAGE_DEFAULT_COMPANY_LOGO));
109
110 if (is == null) {
111 _log.error("Default company logo is not available");
112 }
113
114 _defaultCompanyLogo = getImage(is);
115 }
116 catch (Exception e) {
117 _log.error(
118 "Unable to configure the default company logo: " +
119 e.getMessage());
120 }
121
122 try {
123 InputStream is = classLoader.getResourceAsStream(
124 PropsUtil.get(PropsKeys.IMAGE_DEFAULT_ORGANIZATION_LOGO));
125
126 if (is == null) {
127 _log.error("Default organization logo is not available");
128 }
129
130 _defaultOrganizationLogo = getImage(is);
131 }
132 catch (Exception e) {
133 _log.error(
134 "Unable to configure the default organization logo: " +
135 e.getMessage());
136 }
137
138 try {
139 InputStream is = classLoader.getResourceAsStream(
140 PropsUtil.get(PropsKeys.IMAGE_DEFAULT_USER_FEMALE_PORTRAIT));
141
142 if (is == null) {
143 _log.error("Default user female portrait is not available");
144 }
145
146 _defaultUserFemalePortrait = getImage(is);
147 }
148 catch (Exception e) {
149 _log.error(
150 "Unable to configure the default user female portrait: " +
151 e.getMessage());
152 }
153
154 try {
155 InputStream is = classLoader.getResourceAsStream(
156 PropsUtil.get(PropsKeys.IMAGE_DEFAULT_USER_MALE_PORTRAIT));
157
158 if (is == null) {
159 _log.error("Default user male portrait is not available");
160 }
161
162 _defaultUserMalePortrait = getImage(is);
163 }
164 catch (Exception e) {
165 _log.error(
166 "Unable to configure the default user male portrait: " +
167 e.getMessage());
168 }
169 }
170
171 @Override
172 public Future<RenderedImage> convertCMYKtoRGB(
173 byte[] bytes, final String type) {
174
175 ImageMagick imageMagick = getImageMagick();
176
177 if (!imageMagick.isEnabled()) {
178 return null;
179 }
180
181 File inputFile = _fileUtil.createTempFile(type);
182 final File outputFile = _fileUtil.createTempFile(type);
183
184 try {
185 _fileUtil.write(inputFile, bytes);
186
187 IMOperation imOperation = new IMOperation();
188
189 imOperation.addRawArgs("-format", "%[colorspace]");
190 imOperation.addImage(inputFile.getPath());
191
192 String[] output = imageMagick.identify(imOperation.getCmdArgs());
193
194 if ((output.length == 1) &&
195 StringUtil.equalsIgnoreCase(output[0], "CMYK")) {
196
197 if (_log.isInfoEnabled()) {
198 _log.info("The image is in the CMYK colorspace");
199 }
200
201 imOperation = new IMOperation();
202
203 imOperation.addRawArgs("-colorspace", "RGB");
204 imOperation.addImage(inputFile.getPath());
205 imOperation.addImage(outputFile.getPath());
206
207 Future<Object> future = (Future<Object>)imageMagick.convert(
208 imOperation.getCmdArgs());
209
210 return new FutureConverter<RenderedImage, Object>(future) {
211
212 @Override
213 protected RenderedImage convert(Object obj) {
214 RenderedImage renderedImage = null;
215
216 try {
217 ImageBag imageBag = read(
218 _fileUtil.getBytes(outputFile));
219
220 renderedImage = imageBag.getRenderedImage();
221 }
222 catch (ImageResolutionException | IOException e) {
223 if (_log.isDebugEnabled()) {
224 _log.debug("Unable to convert " + type, e);
225 }
226 }
227
228 return renderedImage;
229 }
230
231 };
232 }
233 }
234 catch (Exception e) {
235 _log.error(e, e);
236 }
237 finally {
238 _fileUtil.delete(inputFile);
239 _fileUtil.delete(outputFile);
240 }
241
242 return null;
243 }
244
245 @Override
246 public BufferedImage convertImageType(BufferedImage sourceImage, int type) {
247 BufferedImage targetImage = new BufferedImage(
248 sourceImage.getWidth(), sourceImage.getHeight(), type);
249
250 Graphics2D graphics = targetImage.createGraphics();
251
252 graphics.drawRenderedImage(sourceImage, null);
253
254 graphics.dispose();
255
256 return targetImage;
257 }
258
259 @Override
260 public RenderedImage crop(
261 RenderedImage renderedImage, int height, int width, int x, int y) {
262
263 Rectangle rectangle = new Rectangle(x, y, width, height);
264
265 Rectangle croppedRectangle = rectangle.intersection(
266 new Rectangle(renderedImage.getWidth(), renderedImage.getHeight()));
267
268 BufferedImage bufferedImage = getBufferedImage(renderedImage);
269
270 return bufferedImage.getSubimage(
271 croppedRectangle.x, croppedRectangle.y, croppedRectangle.width,
272 croppedRectangle.height);
273 }
274
275 @Override
276 public void encodeGIF(RenderedImage renderedImage, OutputStream os)
277 throws IOException {
278
279 BufferedImage bufferedImage = getBufferedImage(renderedImage);
280
281 if (!(bufferedImage.getColorModel() instanceof IndexColorModel)) {
282 bufferedImage = convertImageType(
283 bufferedImage, BufferedImage.TYPE_BYTE_INDEXED);
284 }
285
286 Gif89Encoder encoder = new Gif89Encoder(bufferedImage);
287
288 encoder.encode(os);
289 }
290
291 @Override
292 public void encodeWBMP(RenderedImage renderedImage, OutputStream os)
293 throws IOException {
294
295 BufferedImage bufferedImage = getBufferedImage(renderedImage);
296
297 SampleModel sampleModel = bufferedImage.getSampleModel();
298
299 int type = sampleModel.getDataType();
300
301 if ((bufferedImage.getType() != BufferedImage.TYPE_BYTE_BINARY) ||
302 (type < DataBuffer.TYPE_BYTE) || (type > DataBuffer.TYPE_INT) ||
303 (sampleModel.getNumBands() != 1) ||
304 (sampleModel.getSampleSize(0) != 1)) {
305
306 BufferedImage binaryImage = new BufferedImage(
307 bufferedImage.getWidth(), bufferedImage.getHeight(),
308 BufferedImage.TYPE_BYTE_BINARY);
309
310 Graphics graphics = binaryImage.getGraphics();
311
312 graphics.drawImage(bufferedImage, 0, 0, null);
313
314 renderedImage = binaryImage;
315 }
316
317 if (!ImageIO.write(renderedImage, "wbmp", os)) {
318
319
320
321 os.write(0);
322 os.write(0);
323 os.write(toMultiByte(bufferedImage.getWidth()));
324 os.write(toMultiByte(bufferedImage.getHeight()));
325
326 DataBuffer dataBuffer = bufferedImage.getData().getDataBuffer();
327
328 int size = dataBuffer.getSize();
329
330 for (int i = 0; i < size; i++) {
331 os.write((byte)dataBuffer.getElem(i));
332 }
333 }
334 }
335
336 @Override
337 public BufferedImage getBufferedImage(RenderedImage renderedImage) {
338 if (renderedImage instanceof BufferedImage) {
339 return (BufferedImage)renderedImage;
340 }
341
342 ColorModel colorModel = renderedImage.getColorModel();
343
344 WritableRaster writableRaster =
345 colorModel.createCompatibleWritableRaster(
346 renderedImage.getWidth(), renderedImage.getHeight());
347
348 Hashtable<String, Object> properties = new Hashtable<>();
349
350 String[] keys = renderedImage.getPropertyNames();
351
352 if (!ArrayUtil.isEmpty(keys)) {
353 for (String key : keys) {
354 properties.put(key, renderedImage.getProperty(key));
355 }
356 }
357
358 BufferedImage bufferedImage = new BufferedImage(
359 colorModel, writableRaster, colorModel.isAlphaPremultiplied(),
360 properties);
361
362 renderedImage.copyData(writableRaster);
363
364 return bufferedImage;
365 }
366
367 @Override
368 public byte[] getBytes(RenderedImage renderedImage, String contentType)
369 throws IOException {
370
371 UnsyncByteArrayOutputStream baos = new UnsyncByteArrayOutputStream();
372
373 write(renderedImage, contentType, baos);
374
375 return baos.toByteArray();
376 }
377
378 @Override
379 public Image getDefaultCompanyLogo() {
380 return _defaultCompanyLogo;
381 }
382
383 @Override
384 public Image getDefaultOrganizationLogo() {
385 return _defaultOrganizationLogo;
386 }
387
388 @Override
389 public Image getDefaultSpacer() {
390 return _defaultSpacer;
391 }
392
393 @Override
394 public Image getDefaultUserFemalePortrait() {
395 return _defaultUserFemalePortrait;
396 }
397
398 @Override
399 public Image getDefaultUserMalePortrait() {
400 return _defaultUserMalePortrait;
401 }
402
403 @Override
404 public Image getImage(byte[] bytes)
405 throws ImageResolutionException, IOException {
406
407 if (bytes == null) {
408 return null;
409 }
410
411 ImageBag imageBag = read(bytes);
412
413 RenderedImage renderedImage = imageBag.getRenderedImage();
414
415 if (renderedImage == null) {
416 throw new IOException("Unable to decode image");
417 }
418
419 String type = imageBag.getType();
420
421 int height = renderedImage.getHeight();
422 int width = renderedImage.getWidth();
423 int size = bytes.length;
424
425 Image image = new ImageImpl();
426
427 image.setTextObj(bytes);
428 image.setType(type);
429 image.setHeight(height);
430 image.setWidth(width);
431 image.setSize(size);
432
433 return image;
434 }
435
436 @Override
437 public Image getImage(File file)
438 throws ImageResolutionException, IOException {
439
440 byte[] bytes = _fileUtil.getBytes(file);
441
442 return getImage(bytes);
443 }
444
445 @Override
446 public Image getImage(InputStream is)
447 throws ImageResolutionException, IOException {
448
449 byte[] bytes = _fileUtil.getBytes(is, -1, true);
450
451 return getImage(bytes);
452 }
453
454 @Override
455 public Image getImage(InputStream is, boolean cleanUpStream)
456 throws ImageResolutionException, IOException {
457
458 byte[] bytes = _fileUtil.getBytes(is, -1, cleanUpStream);
459
460 return getImage(bytes);
461 }
462
463 @Override
464 public boolean isNullOrDefaultSpacer(byte[] bytes) {
465 if (ArrayUtil.isEmpty(bytes) ||
466 Arrays.equals(bytes, getDefaultSpacer().getTextObj())) {
467
468 return true;
469 }
470 else {
471 return false;
472 }
473 }
474
475 @Override
476 public ImageBag read(byte[] bytes)
477 throws ImageResolutionException, IOException {
478
479 String formatName = null;
480 ImageInputStream imageInputStream = null;
481 Queue<ImageReader> imageReaders = new LinkedList<>();
482 RenderedImage renderedImage = null;
483
484 try {
485 imageInputStream = ImageIO.createImageInputStream(
486 new ByteArrayInputStream(bytes));
487
488 Iterator<ImageReader> iterator = ImageIO.getImageReaders(
489 imageInputStream);
490
491 while ((renderedImage == null) && iterator.hasNext()) {
492 ImageReader imageReader = iterator.next();
493
494 imageReaders.offer(imageReader);
495
496 try {
497 imageReader.setInput(imageInputStream);
498
499 int height = imageReader.getHeight(0);
500 int width = imageReader.getWidth(0);
501
502 if ((height > PropsValues.IMAGE_TOOL_IMAGE_MAX_HEIGHT) ||
503 (width > PropsValues.IMAGE_TOOL_IMAGE_MAX_WIDTH)) {
504
505 StringBundler sb = new StringBundler(9);
506
507 sb.append("Image's dimensions (");
508 sb.append(height);
509 sb.append(" px high and ");
510 sb.append(width);
511 sb.append(" px wide) exceed max dimensions (");
512 sb.append(PropsValues.IMAGE_TOOL_IMAGE_MAX_HEIGHT);
513 sb.append(" px high and ");
514 sb.append(PropsValues.IMAGE_TOOL_IMAGE_MAX_WIDTH);
515 sb.append(" px wide)");
516
517 throw new ImageResolutionException(sb.toString());
518 }
519
520 renderedImage = imageReader.read(0);
521 }
522 catch (IOException ioe) {
523 continue;
524 }
525
526 formatName = StringUtil.toLowerCase(
527 imageReader.getFormatName());
528 }
529
530 if (renderedImage == null) {
531 throw new IOException("Unsupported image type");
532 }
533 }
534 finally {
535 while (!imageReaders.isEmpty()) {
536 ImageReader imageReader = imageReaders.poll();
537
538 imageReader.dispose();
539 }
540
541 if (imageInputStream != null) {
542 imageInputStream.close();
543 }
544 }
545
546 String type = TYPE_JPEG;
547
548 if (formatName.contains(TYPE_BMP)) {
549 type = TYPE_BMP;
550 }
551 else if (formatName.contains(TYPE_GIF)) {
552 type = TYPE_GIF;
553 }
554 else if (formatName.contains("jpeg") ||
555 StringUtil.equalsIgnoreCase(type, "jpeg")) {
556
557 type = TYPE_JPEG;
558 }
559 else if (formatName.contains(TYPE_PNG)) {
560 type = TYPE_PNG;
561 }
562 else if (formatName.contains(TYPE_TIFF)) {
563 type = TYPE_TIFF;
564 }
565 else {
566 throw new IllegalArgumentException(type + " is not supported");
567 }
568
569 return new ImageBag(renderedImage, type);
570 }
571
572 @Override
573 public ImageBag read(File file)
574 throws ImageResolutionException, IOException {
575
576 return read(_fileUtil.getBytes(file));
577 }
578
579 @Override
580 public ImageBag read(InputStream inputStream)
581 throws ImageResolutionException, IOException {
582
583 return read(_fileUtil.getBytes(inputStream));
584 }
585
586 @Override
587 public RenderedImage scale(RenderedImage renderedImage, int width) {
588 if (width <= 0) {
589 return renderedImage;
590 }
591
592 int imageHeight = renderedImage.getHeight();
593 int imageWidth = renderedImage.getWidth();
594
595 double factor = (double)width / imageWidth;
596
597 int scaledHeight = (int)Math.round(factor * imageHeight);
598 int scaledWidth = width;
599
600 return doScale(renderedImage, scaledHeight, scaledWidth);
601 }
602
603 @Override
604 public RenderedImage scale(
605 RenderedImage renderedImage, int maxHeight, int maxWidth) {
606
607 int imageHeight = renderedImage.getHeight();
608 int imageWidth = renderedImage.getWidth();
609
610 if (maxHeight == 0) {
611 maxHeight = imageHeight;
612 }
613
614 if (maxWidth == 0) {
615 maxWidth = imageWidth;
616 }
617
618 if ((imageHeight <= maxHeight) && (imageWidth <= maxWidth)) {
619 return renderedImage;
620 }
621
622 double factor = Math.min(
623 (double)maxHeight / imageHeight, (double)maxWidth / imageWidth);
624
625 int scaledHeight = Math.max(1, (int)Math.round(factor * imageHeight));
626 int scaledWidth = Math.max(1, (int)Math.round(factor * imageWidth));
627
628 return doScale(renderedImage, scaledHeight, scaledWidth);
629 }
630
631 @Override
632 public void write(
633 RenderedImage renderedImage, String contentType, OutputStream os)
634 throws IOException {
635
636 if (contentType.contains(TYPE_BMP)) {
637 ImageIO.write(renderedImage, "bmp", os);
638 }
639 else if (contentType.contains(TYPE_GIF)) {
640 encodeGIF(renderedImage, os);
641 }
642 else if (contentType.contains(TYPE_JPEG) ||
643 contentType.contains("jpeg")) {
644
645 ImageIO.write(renderedImage, "jpeg", os);
646 }
647 else if (contentType.contains(TYPE_PNG)) {
648 ImageIO.write(renderedImage, TYPE_PNG, os);
649 }
650 else if (contentType.contains(TYPE_TIFF) ||
651 contentType.contains("tif")) {
652
653 ImageIO.write(renderedImage, "tiff", os);
654 }
655 }
656
657 protected RenderedImage doScale(
658 RenderedImage renderedImage, int scaledHeight, int scaledWidth) {
659
660
661
662 BufferedImage originalBufferedImage = getBufferedImage(renderedImage);
663
664 ColorModel originalColorModel = originalBufferedImage.getColorModel();
665
666 ColorSpace colorSpace = originalColorModel.getColorSpace();
667
668 BufferedImage scaledBufferedImage = new BufferedImage(
669 scaledWidth, scaledHeight, colorSpace.getType());
670
671 Graphics2D scaledGraphics2D = scaledBufferedImage.createGraphics();
672
673 if (originalColorModel.hasAlpha()) {
674 scaledGraphics2D.setComposite(AlphaComposite.Src);
675 }
676
677 scaledGraphics2D.drawImage(
678 originalBufferedImage, 0, 0, scaledWidth, scaledHeight, null);
679
680 scaledGraphics2D.dispose();
681
682 return scaledBufferedImage;
683 }
684
685 protected ImageMagick getImageMagick() {
686 if (_imageMagick == null) {
687 _imageMagick = ImageMagickImpl.getInstance();
688
689 _imageMagick.reset();
690 }
691
692 return _imageMagick;
693 }
694
695 protected void orderImageReaderSpis() {
696 IIORegistry defaultIIORegistry = IIORegistry.getDefaultInstance();
697
698 ImageReaderSpi firstImageReaderSpi = null;
699 ImageReaderSpi secondImageReaderSpi = null;
700
701 Iterator<ImageReaderSpi> imageReaderSpis =
702 defaultIIORegistry.getServiceProviders(ImageReaderSpi.class, true);
703
704 while (imageReaderSpis.hasNext()) {
705 ImageReaderSpi imageReaderSpi = imageReaderSpis.next();
706
707 if (imageReaderSpi instanceof CMYKJPEGImageReaderSpi) {
708 secondImageReaderSpi = imageReaderSpi;
709 }
710 else {
711 String[] formatNames = imageReaderSpi.getFormatNames();
712
713 if (ArrayUtil.contains(formatNames, TYPE_JPEG, true) ||
714 ArrayUtil.contains(formatNames, "jpeg", true)) {
715
716 firstImageReaderSpi = imageReaderSpi;
717 }
718 }
719 }
720
721 if ((firstImageReaderSpi != null) && (secondImageReaderSpi != null)) {
722 defaultIIORegistry.setOrdering(
723 ImageReaderSpi.class, firstImageReaderSpi,
724 secondImageReaderSpi);
725 }
726 }
727
728 protected byte[] toMultiByte(int intValue) {
729 int numBits = 32;
730 int mask = 0x80000000;
731
732 while ((mask != 0) && ((intValue & mask) == 0)) {
733 numBits--;
734 mask >>>= 1;
735 }
736
737 int numBitsLeft = numBits;
738 byte[] multiBytes = new byte[(numBitsLeft + 6) / 7];
739
740 int maxIndex = multiBytes.length - 1;
741
742 for (int b = 0; b <= maxIndex; b++) {
743 multiBytes[b] = (byte)((intValue >>> ((maxIndex - b) * 7)) & 0x7f);
744
745 if (b != maxIndex) {
746 multiBytes[b] |= (byte)0x80;
747 }
748 }
749
750 return multiBytes;
751 }
752
753 private ImageToolImpl() {
754 ImageIO.setUseCache(PropsValues.IMAGE_IO_USE_DISK_CACHE);
755
756 orderImageReaderSpis();
757 }
758
759 private static final Log _log = LogFactoryUtil.getLog(ImageToolImpl.class);
760
761 private static final ImageTool _instance = new ImageToolImpl();
762
763 private static final FileImpl _fileUtil = FileImpl.getInstance();
764 private static ImageMagick _imageMagick;
765
766 private Image _defaultCompanyLogo;
767 private Image _defaultOrganizationLogo;
768 private Image _defaultSpacer;
769 private Image _defaultUserFemalePortrait;
770 private Image _defaultUserMalePortrait;
771
772 }