001    /**
002     * Copyright (c) 2000-present Liferay, Inc. All rights reserved.
003     *
004     * This library is free software; you can redistribute it and/or modify it under
005     * the terms of the GNU Lesser General Public License as published by the Free
006     * Software Foundation; either version 2.1 of the License, or (at your option)
007     * any later version.
008     *
009     * This library is distributed in the hope that it will be useful, but WITHOUT
010     * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
011     * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
012     * details.
013     */
014    
015    package com.liferay.portal.image;
016    
017    import com.liferay.portal.kernel.concurrent.FutureConverter;
018    import com.liferay.portal.kernel.image.ImageBag;
019    import com.liferay.portal.kernel.image.ImageMagick;
020    import com.liferay.portal.kernel.image.ImageTool;
021    import com.liferay.portal.kernel.io.unsync.UnsyncByteArrayOutputStream;
022    import com.liferay.portal.kernel.log.Log;
023    import com.liferay.portal.kernel.log.LogFactoryUtil;
024    import com.liferay.portal.kernel.security.pacl.DoPrivileged;
025    import com.liferay.portal.kernel.util.ArrayUtil;
026    import com.liferay.portal.kernel.util.JavaDetector;
027    import com.liferay.portal.kernel.util.PropsKeys;
028    import com.liferay.portal.kernel.util.StringUtil;
029    import com.liferay.portal.model.Image;
030    import com.liferay.portal.model.impl.ImageImpl;
031    import com.liferay.portal.util.FileImpl;
032    import com.liferay.portal.util.PropsUtil;
033    import com.liferay.portal.util.PropsValues;
034    
035    import java.awt.AlphaComposite;
036    import java.awt.Graphics;
037    import java.awt.Graphics2D;
038    import java.awt.GraphicsConfiguration;
039    import java.awt.Rectangle;
040    import java.awt.image.BufferedImage;
041    import java.awt.image.ColorModel;
042    import java.awt.image.DataBuffer;
043    import java.awt.image.IndexColorModel;
044    import java.awt.image.RenderedImage;
045    import java.awt.image.SampleModel;
046    import java.awt.image.WritableRaster;
047    
048    import java.io.ByteArrayInputStream;
049    import java.io.File;
050    import java.io.IOException;
051    import java.io.InputStream;
052    import java.io.OutputStream;
053    
054    import java.util.Arrays;
055    import java.util.Hashtable;
056    import java.util.Iterator;
057    import java.util.LinkedList;
058    import java.util.Queue;
059    import java.util.concurrent.Future;
060    
061    import javax.imageio.ImageIO;
062    import javax.imageio.ImageReader;
063    import javax.imageio.stream.ImageInputStream;
064    
065    import net.jmge.gif.Gif89Encoder;
066    
067    import org.im4java.core.IMOperation;
068    
069    /**
070     * @author Brian Wing Shun Chan
071     * @author Alexander Chow
072     * @author Shuyang Zhou
073     */
074    @DoPrivileged
075    public class ImageToolImpl implements ImageTool {
076    
077            public static ImageTool getInstance() {
078                    return _instance;
079            }
080    
081            public void afterPropertiesSet() {
082                    ClassLoader classLoader = getClass().getClassLoader();
083    
084                    try {
085                            InputStream is = classLoader.getResourceAsStream(
086                                    PropsUtil.get(PropsKeys.IMAGE_DEFAULT_SPACER));
087    
088                            if (is == null) {
089                                    _log.error("Default spacer is not available");
090                            }
091    
092                            _defaultSpacer = getImage(is);
093                    }
094                    catch (Exception e) {
095                            _log.error(
096                                    "Unable to configure the default spacer: " + e.getMessage());
097                    }
098    
099                    try {
100                            InputStream is = classLoader.getResourceAsStream(
101                                    PropsUtil.get(PropsKeys.IMAGE_DEFAULT_COMPANY_LOGO));
102    
103                            if (is == null) {
104                                    _log.error("Default company logo is not available");
105                            }
106    
107                            _defaultCompanyLogo = getImage(is);
108                    }
109                    catch (Exception e) {
110                            _log.error(
111                                    "Unable to configure the default company logo: " +
112                                            e.getMessage());
113                    }
114    
115                    try {
116                            InputStream is = classLoader.getResourceAsStream(
117                                    PropsUtil.get(PropsKeys.IMAGE_DEFAULT_ORGANIZATION_LOGO));
118    
119                            if (is == null) {
120                                    _log.error("Default organization logo is not available");
121                            }
122    
123                            _defaultOrganizationLogo = getImage(is);
124                    }
125                    catch (Exception e) {
126                            _log.error(
127                                    "Unable to configure the default organization logo: " +
128                                            e.getMessage());
129                    }
130    
131                    try {
132                            InputStream is = classLoader.getResourceAsStream(
133                                    PropsUtil.get(PropsKeys.IMAGE_DEFAULT_USER_FEMALE_PORTRAIT));
134    
135                            if (is == null) {
136                                    _log.error("Default user female portrait is not available");
137                            }
138    
139                            _defaultUserFemalePortrait = getImage(is);
140                    }
141                    catch (Exception e) {
142                            _log.error(
143                                    "Unable to configure the default user female portrait: " +
144                                            e.getMessage());
145                    }
146    
147                    try {
148                            InputStream is = classLoader.getResourceAsStream(
149                                    PropsUtil.get(PropsKeys.IMAGE_DEFAULT_USER_MALE_PORTRAIT));
150    
151                            if (is == null) {
152                                    _log.error("Default user male portrait is not available");
153                            }
154    
155                            _defaultUserMalePortrait = getImage(is);
156                    }
157                    catch (Exception e) {
158                            _log.error(
159                                    "Unable to configure the default user male portrait: " +
160                                            e.getMessage());
161                    }
162            }
163    
164            @Override
165            public Future<RenderedImage> convertCMYKtoRGB(
166                    byte[] bytes, final String type) {
167    
168                    ImageMagick imageMagick = getImageMagick();
169    
170                    if (!imageMagick.isEnabled()) {
171                            return null;
172                    }
173    
174                    File inputFile = _fileUtil.createTempFile(type);
175                    final File outputFile = _fileUtil.createTempFile(type);
176    
177                    try {
178                            _fileUtil.write(inputFile, bytes);
179    
180                            IMOperation imOperation = new IMOperation();
181    
182                            imOperation.addRawArgs("-format", "%[colorspace]");
183                            imOperation.addImage(inputFile.getPath());
184    
185                            String[] output = imageMagick.identify(imOperation.getCmdArgs());
186    
187                            if ((output.length == 1) &&
188                                    StringUtil.equalsIgnoreCase(output[0], "CMYK")) {
189    
190                                    if (_log.isInfoEnabled()) {
191                                            _log.info("The image is in the CMYK colorspace");
192                                    }
193    
194                                    imOperation = new IMOperation();
195    
196                                    imOperation.addRawArgs("-colorspace", "RGB");
197                                    imOperation.addImage(inputFile.getPath());
198                                    imOperation.addImage(outputFile.getPath());
199    
200                                    Future<Object> future = (Future<Object>)imageMagick.convert(
201                                            imOperation.getCmdArgs());
202    
203                                    return new FutureConverter<RenderedImage, Object>(future) {
204    
205                                            @Override
206                                            protected RenderedImage convert(Object obj) {
207                                                    RenderedImage renderedImage = null;
208    
209                                                    try {
210                                                            ImageBag imageBag = read(
211                                                                    _fileUtil.getBytes(outputFile));
212    
213                                                            renderedImage = imageBag.getRenderedImage();
214                                                    }
215                                                    catch (IOException ioe) {
216                                                            if (_log.isDebugEnabled()) {
217                                                                    _log.debug("Unable to convert " + type, ioe);
218                                                            }
219                                                    }
220    
221                                                    return renderedImage;
222                                            }
223    
224                                    };
225                            }
226                    }
227                    catch (Exception e) {
228                            if (_log.isErrorEnabled()) {
229                                    _log.error(e, e);
230                            }
231                    }
232                    finally {
233                            _fileUtil.delete(inputFile);
234                            _fileUtil.delete(outputFile);
235                    }
236    
237                    return null;
238            }
239    
240            @Override
241            public BufferedImage convertImageType(BufferedImage sourceImage, int type) {
242                    BufferedImage targetImage = new BufferedImage(
243                            sourceImage.getWidth(), sourceImage.getHeight(), type);
244    
245                    Graphics2D graphics = targetImage.createGraphics();
246    
247                    graphics.drawRenderedImage(sourceImage, null);
248    
249                    graphics.dispose();
250    
251                    return targetImage;
252            }
253    
254            @Override
255            public RenderedImage crop(
256                    RenderedImage renderedImage, int height, int width, int x, int y) {
257    
258                    Rectangle rectangle = new Rectangle(x, y, width, height);
259    
260                    Rectangle croppedRectangle = rectangle.intersection(
261                            new Rectangle(renderedImage.getWidth(), renderedImage.getHeight()));
262    
263                    BufferedImage bufferedImage = getBufferedImage(renderedImage);
264    
265                    return bufferedImage.getSubimage(
266                            croppedRectangle.x, croppedRectangle.y, croppedRectangle.width,
267                            croppedRectangle.height);
268            }
269    
270            @Override
271            public void encodeGIF(RenderedImage renderedImage, OutputStream os)
272                    throws IOException {
273    
274                    if (JavaDetector.isJDK6()) {
275                            ImageIO.write(renderedImage, TYPE_GIF, os);
276                    }
277                    else {
278                            BufferedImage bufferedImage = getBufferedImage(renderedImage);
279    
280                            if (!(bufferedImage.getColorModel() instanceof IndexColorModel)) {
281                                    bufferedImage = convertImageType(
282                                            bufferedImage, BufferedImage.TYPE_BYTE_INDEXED);
283                            }
284    
285                            Gif89Encoder encoder = new Gif89Encoder(bufferedImage);
286    
287                            encoder.encode(os);
288                    }
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                            // See http://www.jguru.com/faq/view.jsp?EID=127723
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<String, Object>();
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) throws IOException {
405                    if (bytes == null) {
406                            return null;
407                    }
408    
409                    ImageBag imageBag = read(bytes);
410    
411                    RenderedImage renderedImage = imageBag.getRenderedImage();
412    
413                    if (renderedImage == null) {
414                            throw new IOException("Unable to decode image");
415                    }
416    
417                    String type = imageBag.getType();
418    
419                    int height = renderedImage.getHeight();
420                    int width = renderedImage.getWidth();
421                    int size = bytes.length;
422    
423                    Image image = new ImageImpl();
424    
425                    image.setTextObj(bytes);
426                    image.setType(type);
427                    image.setHeight(height);
428                    image.setWidth(width);
429                    image.setSize(size);
430    
431                    return image;
432            }
433    
434            @Override
435            public Image getImage(File file) throws IOException {
436                    byte[] bytes = _fileUtil.getBytes(file);
437    
438                    return getImage(bytes);
439            }
440    
441            @Override
442            public Image getImage(InputStream is) throws IOException {
443                    byte[] bytes = _fileUtil.getBytes(is, -1, true);
444    
445                    return getImage(bytes);
446            }
447    
448            @Override
449            public Image getImage(InputStream is, boolean cleanUpStream)
450                    throws IOException {
451    
452                    byte[] bytes = _fileUtil.getBytes(is, -1, cleanUpStream);
453    
454                    return getImage(bytes);
455            }
456    
457            @Override
458            public boolean isNullOrDefaultSpacer(byte[] bytes) {
459                    if (ArrayUtil.isEmpty(bytes) ||
460                            Arrays.equals(bytes, getDefaultSpacer().getTextObj())) {
461    
462                            return true;
463                    }
464                    else {
465                            return false;
466                    }
467            }
468    
469            @Override
470            public ImageBag read(byte[] bytes) throws IOException {
471                    String formatName = null;
472                    ImageInputStream imageInputStream = null;
473                    Queue<ImageReader> imageReaders = new LinkedList<ImageReader>();
474                    RenderedImage renderedImage = null;
475    
476                    try {
477                            boolean firstImageReader = true;
478    
479                            imageInputStream = ImageIO.createImageInputStream(
480                                    new ByteArrayInputStream(bytes));
481    
482                            Iterator<ImageReader> iterator = ImageIO.getImageReaders(
483                                    imageInputStream);
484    
485                            while (iterator.hasNext()) {
486                                    ImageReader imageReader = iterator.next();
487    
488                                    imageReaders.offer(imageReader);
489    
490                                    if (firstImageReader) {
491                                            imageReader.setInput(imageInputStream);
492    
493                                            renderedImage = imageReader.read(0);
494    
495                                            formatName = imageReader.getFormatName();
496    
497                                            firstImageReader = false;
498                                    }
499                            }
500                    }
501                    finally {
502                            while (!imageReaders.isEmpty()) {
503                                    ImageReader imageReader = imageReaders.poll();
504    
505                                    imageReader.dispose();
506                            }
507    
508                            if (imageInputStream != null) {
509                                    imageInputStream.close();
510                            }
511                    }
512    
513                    formatName = StringUtil.toLowerCase(formatName);
514    
515                    String type = TYPE_JPEG;
516    
517                    if (formatName.contains(TYPE_BMP)) {
518                            type = TYPE_BMP;
519                    }
520                    else if (formatName.contains(TYPE_GIF)) {
521                            type = TYPE_GIF;
522                    }
523                    else if (formatName.contains("jpeg") || type.equals("jpeg")) {
524                            type = TYPE_JPEG;
525                    }
526                    else if (formatName.contains(TYPE_PNG)) {
527                            type = TYPE_PNG;
528                    }
529                    else if (formatName.contains(TYPE_TIFF)) {
530                            type = TYPE_TIFF;
531                    }
532                    else {
533                            throw new IllegalArgumentException(type + " is not supported");
534                    }
535    
536                    return new ImageBag(renderedImage, type);
537            }
538    
539            @Override
540            public ImageBag read(File file) throws IOException {
541                    return read(_fileUtil.getBytes(file));
542            }
543    
544            @Override
545            public ImageBag read(InputStream inputStream) throws IOException {
546                    return read(_fileUtil.getBytes(inputStream));
547            }
548    
549            @Override
550            public RenderedImage scale(RenderedImage renderedImage, int width) {
551                    if (width <= 0) {
552                            return renderedImage;
553                    }
554    
555                    int imageHeight = renderedImage.getHeight();
556                    int imageWidth = renderedImage.getWidth();
557    
558                    double factor = (double)width / imageWidth;
559    
560                    int scaledHeight = (int)Math.round(factor * imageHeight);
561                    int scaledWidth = width;
562    
563                    return doScale(renderedImage, scaledHeight, scaledWidth);
564            }
565    
566            @Override
567            public RenderedImage scale(
568                    RenderedImage renderedImage, int maxHeight, int maxWidth) {
569    
570                    int imageHeight = renderedImage.getHeight();
571                    int imageWidth = renderedImage.getWidth();
572    
573                    if (maxHeight == 0) {
574                            maxHeight = imageHeight;
575                    }
576    
577                    if (maxWidth == 0) {
578                            maxWidth = imageWidth;
579                    }
580    
581                    if ((imageHeight <= maxHeight) && (imageWidth <= maxWidth)) {
582                            return renderedImage;
583                    }
584    
585                    double factor = Math.min(
586                            (double)maxHeight / imageHeight, (double)maxWidth / imageWidth);
587    
588                    int scaledHeight = Math.max(1, (int)Math.round(factor * imageHeight));
589                    int scaledWidth = Math.max(1, (int)Math.round(factor * imageWidth));
590    
591                    return doScale(renderedImage, scaledHeight, scaledWidth);
592            }
593    
594            @Override
595            public void write(
596                            RenderedImage renderedImage, String contentType, OutputStream os)
597                    throws IOException {
598    
599                    if (contentType.contains(TYPE_BMP)) {
600                            ImageIO.write(renderedImage, "bmp", os);
601                    }
602                    else if (contentType.contains(TYPE_GIF)) {
603                            encodeGIF(renderedImage, os);
604                    }
605                    else if (contentType.contains(TYPE_JPEG) ||
606                                     contentType.contains("jpeg")) {
607    
608                            ImageIO.write(renderedImage, "jpeg", os);
609                    }
610                    else if (contentType.contains(TYPE_PNG)) {
611                            ImageIO.write(renderedImage, TYPE_PNG, os);
612                    }
613                    else if (contentType.contains(TYPE_TIFF) ||
614                                     contentType.contains("tif")) {
615    
616                            ImageIO.write(renderedImage, "tiff", os);
617                    }
618            }
619    
620            protected RenderedImage doScale(
621                    RenderedImage renderedImage, int scaledHeight, int scaledWidth) {
622    
623                    // See http://www.oracle.com/technetwork/java/index-137037.html
624    
625                    BufferedImage originalBufferedImage = getBufferedImage(renderedImage);
626    
627                    ColorModel originalColorModel = originalBufferedImage.getColorModel();
628    
629                    Graphics2D originalGraphics2D = originalBufferedImage.createGraphics();
630    
631                    if (originalColorModel.hasAlpha()) {
632                            originalGraphics2D.setComposite(AlphaComposite.Src);
633                    }
634    
635                    GraphicsConfiguration originalGraphicsConfiguration =
636                            originalGraphics2D.getDeviceConfiguration();
637    
638                    BufferedImage scaledBufferedImage =
639                            originalGraphicsConfiguration.createCompatibleImage(
640                                    scaledWidth, scaledHeight,
641                                    originalBufferedImage.getTransparency());
642    
643                    Graphics scaledGraphics = scaledBufferedImage.getGraphics();
644    
645                    scaledGraphics.drawImage(
646                            originalBufferedImage.getScaledInstance(
647                                    scaledWidth, scaledHeight, java.awt.Image.SCALE_SMOOTH),
648                            0, 0, null);
649    
650                    originalGraphics2D.dispose();
651    
652                    return scaledBufferedImage;
653            }
654    
655            protected ImageMagick getImageMagick() {
656                    if (_imageMagick == null) {
657                            _imageMagick = ImageMagickImpl.getInstance();
658    
659                            _imageMagick.reset();
660                    }
661    
662                    return _imageMagick;
663            }
664    
665            protected byte[] toMultiByte(int intValue) {
666                    int numBits = 32;
667                    int mask = 0x80000000;
668    
669                    while ((mask != 0) && ((intValue & mask) == 0)) {
670                            numBits--;
671                            mask >>>= 1;
672                    }
673    
674                    int numBitsLeft = numBits;
675                    byte[] multiBytes = new byte[(numBitsLeft + 6) / 7];
676    
677                    int maxIndex = multiBytes.length - 1;
678    
679                    for (int b = 0; b <= maxIndex; b++) {
680                            multiBytes[b] = (byte)((intValue >>> ((maxIndex - b) * 7)) & 0x7f);
681    
682                            if (b != maxIndex) {
683                                    multiBytes[b] |= (byte)0x80;
684                            }
685                    }
686    
687                    return multiBytes;
688            }
689    
690            private ImageToolImpl() {
691                    ImageIO.setUseCache(PropsValues.IMAGE_IO_USE_DISK_CACHE);
692            }
693    
694            private static Log _log = LogFactoryUtil.getLog(ImageToolImpl.class);
695    
696            private static ImageTool _instance = new ImageToolImpl();
697    
698            private static FileImpl _fileUtil = FileImpl.getInstance();
699            private static ImageMagick _imageMagick;
700    
701            private Image _defaultCompanyLogo;
702            private Image _defaultOrganizationLogo;
703            private Image _defaultSpacer;
704            private Image _defaultUserFemalePortrait;
705            private Image _defaultUserMalePortrait;
706    
707    }