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