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