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