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.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    
034    import com.sun.media.jai.codec.ImageCodec;
035    import com.sun.media.jai.codec.ImageDecoder;
036    import com.sun.media.jai.codec.ImageEncoder;
037    
038    import java.awt.Graphics;
039    import java.awt.Graphics2D;
040    import java.awt.image.BufferedImage;
041    import java.awt.image.DataBuffer;
042    import java.awt.image.IndexColorModel;
043    import java.awt.image.RenderedImage;
044    import java.awt.image.SampleModel;
045    import java.awt.image.WritableRaster;
046    
047    import java.io.File;
048    import java.io.IOException;
049    import java.io.InputStream;
050    import java.io.OutputStream;
051    
052    import java.util.Arrays;
053    import java.util.Enumeration;
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    
061    import javax.media.jai.RenderedImageAdapter;
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                    RenderedImageAdapter adapter = new RenderedImageAdapter(renderedImage);
302    
303                    return adapter.getAsBufferedImage();
304            }
305    
306            @Override
307            public byte[] getBytes(RenderedImage renderedImage, String contentType)
308                    throws IOException {
309    
310                    UnsyncByteArrayOutputStream baos = new UnsyncByteArrayOutputStream();
311    
312                    write(renderedImage, contentType, baos);
313    
314                    return baos.toByteArray();
315            }
316    
317            @Override
318            public Image getDefaultCompanyLogo() {
319                    return _defaultCompanyLogo;
320            }
321    
322            @Override
323            public Image getDefaultOrganizationLogo() {
324                    return _defaultOrganizationLogo;
325            }
326    
327            @Override
328            public Image getDefaultSpacer() {
329                    return _defaultSpacer;
330            }
331    
332            @Override
333            public Image getDefaultUserFemalePortrait() {
334                    return _defaultUserFemalePortrait;
335            }
336    
337            @Override
338            public Image getDefaultUserMalePortrait() {
339                    return _defaultUserMalePortrait;
340            }
341    
342            @Override
343            public Image getImage(byte[] bytes) throws IOException {
344                    if (bytes == null) {
345                            return null;
346                    }
347    
348                    ImageBag imageBag = read(bytes);
349    
350                    RenderedImage renderedImage = imageBag.getRenderedImage();
351    
352                    if (renderedImage == null) {
353                            throw new IOException("Unable to decode image");
354                    }
355    
356                    String type = imageBag.getType();
357    
358                    int height = renderedImage.getHeight();
359                    int width = renderedImage.getWidth();
360                    int size = bytes.length;
361    
362                    Image image = new ImageImpl();
363    
364                    image.setTextObj(bytes);
365                    image.setType(type);
366                    image.setHeight(height);
367                    image.setWidth(width);
368                    image.setSize(size);
369    
370                    return image;
371            }
372    
373            @Override
374            public Image getImage(File file) throws IOException {
375                    byte[] bytes = _fileUtil.getBytes(file);
376    
377                    return getImage(bytes);
378            }
379    
380            @Override
381            public Image getImage(InputStream is) throws IOException {
382                    byte[] bytes = _fileUtil.getBytes(is, -1, true);
383    
384                    return getImage(bytes);
385            }
386    
387            @Override
388            public Image getImage(InputStream is, boolean cleanUpStream)
389                    throws IOException {
390    
391                    byte[] bytes = _fileUtil.getBytes(is, -1, cleanUpStream);
392    
393                    return getImage(bytes);
394            }
395    
396            @Override
397            public boolean isNullOrDefaultSpacer(byte[] bytes) {
398                    if (ArrayUtil.isEmpty(bytes) ||
399                            Arrays.equals(bytes, getDefaultSpacer().getTextObj())) {
400    
401                            return true;
402                    }
403                    else {
404                            return false;
405                    }
406            }
407    
408            @Override
409            public ImageBag read(byte[] bytes) {
410                    RenderedImage renderedImage = null;
411                    String type = TYPE_NOT_AVAILABLE;
412    
413                    Enumeration<ImageCodec> enu = ImageCodec.getCodecs();
414    
415                    while (enu.hasMoreElements()) {
416                            ImageCodec codec = enu.nextElement();
417    
418                            if (codec.isFormatRecognized(bytes)) {
419                                    type = codec.getFormatName();
420    
421                                    renderedImage = read(bytes, type);
422    
423                                    break;
424                            }
425                    }
426    
427                    if (type.equals("jpeg")) {
428                            type = TYPE_JPEG;
429                    }
430    
431                    return new ImageBag(renderedImage, type);
432            }
433    
434            @Override
435            public ImageBag read(File file) throws IOException {
436                    return read(_fileUtil.getBytes(file));
437            }
438    
439            @Override
440            public ImageBag read(InputStream inputStream) throws IOException {
441                    return read(_fileUtil.getBytes(inputStream));
442            }
443    
444            @Override
445            public RenderedImage scale(RenderedImage renderedImage, int width) {
446                    if (width <= 0) {
447                            return renderedImage;
448                    }
449    
450                    int imageHeight = renderedImage.getHeight();
451                    int imageWidth = renderedImage.getWidth();
452    
453                    double factor = (double)width / imageWidth;
454    
455                    int scaledHeight = (int)(factor * imageHeight);
456                    int scaledWidth = width;
457    
458                    BufferedImage bufferedImage = getBufferedImage(renderedImage);
459    
460                    int type = bufferedImage.getType();
461    
462                    if (type == 0) {
463                            type = BufferedImage.TYPE_INT_ARGB;
464                    }
465    
466                    BufferedImage scaledBufferedImage = new BufferedImage(
467                            scaledWidth, scaledHeight, type);
468    
469                    Graphics graphics = scaledBufferedImage.getGraphics();
470    
471                    java.awt.Image scaledImage = bufferedImage.getScaledInstance(
472                            scaledWidth, scaledHeight, java.awt.Image.SCALE_SMOOTH);
473    
474                    graphics.drawImage(scaledImage, 0, 0, null);
475    
476                    return scaledBufferedImage;
477            }
478    
479            @Override
480            public RenderedImage scale(
481                    RenderedImage renderedImage, int maxHeight, int maxWidth) {
482    
483                    int imageHeight = renderedImage.getHeight();
484                    int imageWidth = renderedImage.getWidth();
485    
486                    if (maxHeight == 0) {
487                            maxHeight = imageHeight;
488                    }
489    
490                    if (maxWidth == 0) {
491                            maxWidth = imageWidth;
492                    }
493    
494                    if ((imageHeight <= maxHeight) && (imageWidth <= maxWidth)) {
495                            return renderedImage;
496                    }
497    
498                    double factor = Math.min(
499                            (double)maxHeight / imageHeight, (double)maxWidth / imageWidth);
500    
501                    int scaledHeight = Math.max(1, (int)(factor * imageHeight));
502                    int scaledWidth = Math.max(1, (int)(factor * imageWidth));
503    
504                    BufferedImage bufferedImage = getBufferedImage(renderedImage);
505    
506                    int type = bufferedImage.getType();
507    
508                    if (type == 0) {
509                            type = BufferedImage.TYPE_INT_ARGB;
510                    }
511    
512                    BufferedImage scaledBufferedImage = null;
513    
514                    if ((type == BufferedImage.TYPE_BYTE_BINARY) ||
515                            (type == BufferedImage.TYPE_BYTE_INDEXED)) {
516    
517                            IndexColorModel indexColorModel =
518                                    (IndexColorModel)bufferedImage.getColorModel();
519    
520                            BufferedImage tempBufferedImage = new BufferedImage(
521                                    1, 1, type, indexColorModel);
522    
523                            int bits = indexColorModel.getPixelSize();
524                            int size = indexColorModel.getMapSize();
525    
526                            byte[] reds = new byte[size];
527    
528                            indexColorModel.getReds(reds);
529    
530                            byte[] greens = new byte[size];
531    
532                            indexColorModel.getGreens(greens);
533    
534                            byte[] blues = new byte[size];
535    
536                            indexColorModel.getBlues(blues);
537    
538                            WritableRaster writableRaster = tempBufferedImage.getRaster();
539    
540                            int pixel = writableRaster.getSample(0, 0, 0);
541    
542                            IndexColorModel scaledIndexColorModel = new IndexColorModel(
543                                    bits, size, reds, greens, blues, pixel);
544    
545                            scaledBufferedImage = new BufferedImage(
546                                    scaledWidth, scaledHeight, type, scaledIndexColorModel);
547                    }
548                    else {
549                            scaledBufferedImage = new BufferedImage(
550                                    scaledWidth, scaledHeight, type);
551                    }
552    
553                    Graphics graphics = scaledBufferedImage.getGraphics();
554    
555                    java.awt.Image scaledImage = bufferedImage.getScaledInstance(
556                            scaledWidth, scaledHeight, java.awt.Image.SCALE_SMOOTH);
557    
558                    graphics.drawImage(scaledImage, 0, 0, null);
559    
560                    return scaledBufferedImage;
561            }
562    
563            @Override
564            public void write(
565                            RenderedImage renderedImage, String contentType, OutputStream os)
566                    throws IOException {
567    
568                    if (contentType.contains(TYPE_BMP)) {
569                            ImageEncoder imageEncoder = ImageCodec.createImageEncoder(
570                                    TYPE_BMP, os, null);
571    
572                            imageEncoder.encode(renderedImage);
573                    }
574                    else if (contentType.contains(TYPE_GIF)) {
575                            encodeGIF(renderedImage, os);
576                    }
577                    else if (contentType.contains(TYPE_JPEG) ||
578                                     contentType.contains("jpeg")) {
579    
580                            ImageIO.write(renderedImage, "jpeg", os);
581                    }
582                    else if (contentType.contains(TYPE_PNG)) {
583                            ImageIO.write(renderedImage, TYPE_PNG, os);
584                    }
585                    else if (contentType.contains(TYPE_TIFF) ||
586                                     contentType.contains("tif")) {
587    
588                            ImageEncoder imageEncoder = ImageCodec.createImageEncoder(
589                                    TYPE_TIFF, os, null);
590    
591                            imageEncoder.encode(renderedImage);
592                    }
593            }
594    
595            protected ImageMagick getImageMagick() {
596                    if (_imageMagick == null) {
597                            _imageMagick = ImageMagickImpl.getInstance();
598    
599                            _imageMagick.reset();
600                    }
601    
602                    return _imageMagick;
603            }
604    
605            protected RenderedImage read(byte[] bytes, String type) {
606                    RenderedImage renderedImage = null;
607    
608                    try {
609                            if (type.equals(TYPE_JPEG)) {
610                                    type = "jpeg";
611                            }
612    
613                            ImageDecoder imageDecoder = ImageCodec.createImageDecoder(
614                                    type, new UnsyncByteArrayInputStream(bytes), null);
615    
616                            renderedImage = imageDecoder.decodeAsRenderedImage();
617                    }
618                    catch (IOException ioe) {
619                            if (_log.isDebugEnabled()) {
620                                    _log.debug(type + ": " + ioe.getMessage());
621                            }
622                    }
623    
624                    return renderedImage;
625            }
626    
627            protected byte[] toMultiByte(int intValue) {
628                    int numBits = 32;
629                    int mask = 0x80000000;
630    
631                    while ((mask != 0) && ((intValue & mask) == 0)) {
632                            numBits--;
633                            mask >>>= 1;
634                    }
635    
636                    int numBitsLeft = numBits;
637                    byte[] multiBytes = new byte[(numBitsLeft + 6) / 7];
638    
639                    int maxIndex = multiBytes.length - 1;
640    
641                    for (int b = 0; b <= maxIndex; b++) {
642                            multiBytes[b] = (byte)((intValue >>> ((maxIndex - b) * 7)) & 0x7f);
643    
644                            if (b != maxIndex) {
645                                    multiBytes[b] |= (byte)0x80;
646                            }
647                    }
648    
649                    return multiBytes;
650            }
651    
652            private static Log _log = LogFactoryUtil.getLog(ImageToolImpl.class);
653    
654            private static ImageTool _instance = new ImageToolImpl();
655    
656            private static FileImpl _fileUtil = FileImpl.getInstance();
657            private static ImageMagick _imageMagick;
658    
659            private Image _defaultCompanyLogo;
660            private Image _defaultOrganizationLogo;
661            private Image _defaultSpacer;
662            private Image _defaultUserFemalePortrait;
663            private Image _defaultUserMalePortrait;
664    
665            private class RenderedImageFuture implements Future<RenderedImage> {
666    
667                    public RenderedImageFuture(
668                            Future<?> future, File outputFile, String type) {
669    
670                            _future = future;
671                            _outputFile = outputFile;
672                            _type = type;
673                    }
674    
675                    @Override
676                    public boolean cancel(boolean mayInterruptIfRunning) {
677                            if (_future.isCancelled() || _future.isDone()) {
678                                    return false;
679                            }
680    
681                            _future.cancel(true);
682    
683                            return true;
684                    }
685    
686                    @Override
687                    public RenderedImage get()
688                            throws ExecutionException, InterruptedException {
689    
690                            _future.get();
691    
692                            byte[] bytes = new byte[0];
693    
694                            try {
695                                    bytes = _fileUtil.getBytes(_outputFile);
696                            }
697                            catch (IOException ioe) {
698                                    throw new ExecutionException(ioe);
699                            }
700    
701                            return read(bytes, _type);
702                    }
703    
704                    @Override
705                    public RenderedImage get(long timeout, TimeUnit timeUnit)
706                            throws ExecutionException, InterruptedException, TimeoutException {
707    
708                            _future.get(timeout, timeUnit);
709    
710                            byte[] bytes = new byte[0];
711    
712                            try {
713                                    bytes = _fileUtil.getBytes(_outputFile);
714                            }
715                            catch (IOException ioe) {
716                                    throw new ExecutionException(ioe);
717                            }
718    
719                            return read(bytes, _type);
720                    }
721    
722                    @Override
723                    public boolean isCancelled() {
724                            return _future.isCancelled();
725                    }
726    
727                    @Override
728                    public boolean isDone() {
729                            return _future.isDone();
730                    }
731    
732                    private final Future<?> _future;
733                    private final File _outputFile;
734                    private final String _type;
735    
736            }
737    
738    }