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