001    /**
002     * Copyright (c) 2000-2011 Liferay, Inc. All rights reserved.
003     *
004     * The contents of this file are subject to the terms of the Liferay Enterprise
005     * Subscription License ("License"). You may not use this file except in
006     * compliance with the License. You can obtain a copy of the License by
007     * contacting Liferay, Inc. See the License for the specific language governing
008     * permissions and limitations under the License, including but not limited to
009     * distribution rights of the Software.
010     *
011     *
012     *
013     */
014    
015    package com.liferay.portlet.documentlibrary.util;
016    
017    import com.liferay.portal.kernel.configuration.Filter;
018    import com.liferay.portal.kernel.image.ImageProcessor;
019    import com.liferay.portal.kernel.image.ImageProcessorUtil;
020    import com.liferay.portal.kernel.log.Log;
021    import com.liferay.portal.kernel.log.LogFactoryUtil;
022    import com.liferay.portal.kernel.messaging.DestinationNames;
023    import com.liferay.portal.kernel.messaging.MessageBusUtil;
024    import com.liferay.portal.kernel.repository.model.FileVersion;
025    import com.liferay.portal.kernel.util.ContentTypes;
026    import com.liferay.portal.kernel.util.FileUtil;
027    import com.liferay.portal.kernel.util.MimeTypesUtil;
028    import com.liferay.portal.kernel.util.OSDetector;
029    import com.liferay.portal.kernel.util.PropsKeys;
030    import com.liferay.portal.kernel.util.StringBundler;
031    import com.liferay.portal.kernel.util.Validator;
032    import com.liferay.portal.repository.liferayrepository.model.LiferayFileVersion;
033    import com.liferay.portal.util.PrefsPropsUtil;
034    import com.liferay.portal.util.PropsUtil;
035    import com.liferay.portal.util.PropsValues;
036    import com.liferay.portlet.documentlibrary.NoSuchFileEntryException;
037    import com.liferay.portlet.documentlibrary.store.DLStoreUtil;
038    
039    import java.awt.image.BufferedImage;
040    import java.awt.image.RenderedImage;
041    
042    import java.io.File;
043    import java.io.FileInputStream;
044    import java.io.FileOutputStream;
045    import java.io.InputStream;
046    
047    import java.util.Arrays;
048    import java.util.List;
049    import java.util.Set;
050    import java.util.Vector;
051    
052    import javax.imageio.ImageIO;
053    
054    import javax.portlet.PortletPreferences;
055    
056    import org.apache.pdfbox.pdmodel.PDDocument;
057    import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
058    import org.apache.pdfbox.pdmodel.PDPage;
059    
060    import org.im4java.core.ConvertCmd;
061    import org.im4java.core.IMOperation;
062    import org.im4java.process.ProcessStarter;
063    
064    /**
065     * @author Alexander Chow
066     * @author Mika Koivisto
067     * @author Juan González
068     */
069    public class PDFProcessor extends DefaultPreviewableProcessor {
070    
071            public static final String PREVIEW_TYPE = ImageProcessor.TYPE_PNG;
072    
073            public static final String THUMBNAIL_TYPE = ImageProcessor.TYPE_PNG;
074    
075            public static String getGlobalSearchPath() throws Exception {
076                    PortletPreferences preferences = PrefsPropsUtil.getPreferences();
077    
078                    String globalSearchPath = preferences.getValue(
079                            PropsKeys.IMAGEMAGICK_GLOBAL_SEARCH_PATH, null);
080    
081                    if (Validator.isNotNull(globalSearchPath)) {
082                            return globalSearchPath;
083                    }
084    
085                    String filterName = null;
086    
087                    if (OSDetector.isApple()) {
088                            filterName = "apple";
089                    }
090                    else if (OSDetector.isWindows()) {
091                            filterName = "windows";
092                    }
093                    else {
094                            filterName = "unix";
095                    }
096    
097                    return PropsUtil.get(
098                            PropsKeys.IMAGEMAGICK_GLOBAL_SEARCH_PATH, new Filter(filterName));
099            }
100    
101            public static void generateImages(FileVersion fileVersion)
102                    throws Exception {
103    
104                    _instance._generateImages(fileVersion);
105            }
106    
107            public static InputStream getPreviewAsStream(
108                            FileVersion fileVersion, int index)
109                    throws Exception {
110    
111                    return _instance.doGetPreviewAsStream(fileVersion, index);
112            }
113    
114            public static int getPreviewFileCount(FileVersion fileVersion) {
115                    try {
116                            return _instance.doGetPreviewFileCount(fileVersion);
117                    }
118                    catch (Exception e) {
119                            _log.error(e, e);
120                    }
121    
122                    return 0;
123            }
124    
125            public static long getPreviewFileSize(FileVersion fileVersion, int index)
126                    throws Exception {
127    
128                    return _instance.doGetPreviewFileSize(fileVersion, index);
129            }
130    
131            public static InputStream getThumbnailAsStream(FileVersion fileVersion)
132                    throws Exception {
133    
134                    return _instance.doGetThumbnailAsStream(fileVersion);
135            }
136    
137            public static long getThumbnailFileSize(FileVersion fileVersion)
138                    throws Exception {
139    
140                    return _instance.doGetThumbnailFileSize(fileVersion);
141            }
142    
143            public static boolean hasImages(FileVersion fileVersion) {
144                    boolean hasImages = false;
145    
146                    try {
147                            hasImages = _instance._hasImages(fileVersion);
148    
149                            if (!hasImages && _instance.isSupported(fileVersion)) {
150                                    _instance._queueGeneration(fileVersion);
151                            }
152                    }
153                    catch (Exception e) {
154                            _log.error(e, e);
155                    }
156    
157                    return hasImages;
158            }
159    
160            public static boolean isImageMagickEnabled() throws Exception {
161                    if (PrefsPropsUtil.getBoolean(PropsKeys.IMAGEMAGICK_ENABLED)) {
162                            return true;
163                    }
164    
165                    if (!_warned) {
166                            StringBundler sb = new StringBundler(5);
167    
168                            sb.append("Liferay is not configured to use ImageMagick for ");
169                            sb.append("generating Document Library previews and will default ");
170                            sb.append("to PDFBox. For better quality previews, install ");
171                            sb.append("ImageMagick and enable it in portal-ext.properties.");
172    
173                            _log.warn(sb.toString());
174    
175                            _warned = true;
176                    }
177    
178                    return false;
179            }
180    
181            public static void reset() throws Exception {
182                    if (isImageMagickEnabled()) {
183                            String globalSearchPath = getGlobalSearchPath();
184    
185                            ProcessStarter.setGlobalSearchPath(globalSearchPath);
186    
187                            _convertCmd = new ConvertCmd();
188                    }
189                    else {
190                            _convertCmd = null;
191                    }
192            }
193    
194            public PDFProcessor() {
195                    FileUtil.mkdirs(PREVIEW_TMP_PATH);
196                    FileUtil.mkdirs(THUMBNAIL_TMP_PATH);
197    
198                    try {
199                            reset();
200                    }
201                    catch (Exception e) {
202                            _log.warn(e, e);
203                    }
204            }
205    
206            public boolean isSupported(String mimeType) {
207                    if (mimeType.equals(ContentTypes.APPLICATION_PDF) ||
208                            mimeType.equals(ContentTypes.APPLICATION_X_PDF)) {
209    
210                            return true;
211                    }
212    
213                    if (DocumentConversionUtil.isEnabled()) {
214                            Set<String> extensions = MimeTypesUtil.getExtensions(mimeType);
215    
216                            for (String extension : extensions) {
217                                    extension = extension.substring(1);
218    
219                                    String[] targetExtensions =
220                                            DocumentConversionUtil.getConversions(extension);
221    
222                                    if (Arrays.binarySearch(targetExtensions, "pdf") >= 0) {
223                                            return true;
224                                    }
225                            }
226                    }
227    
228                    return false;
229            }
230    
231            public void trigger(FileVersion fileVersion) {
232                    _instance._queueGeneration(fileVersion);
233            }
234    
235            @Override
236            protected String getPreviewType() {
237                    return PREVIEW_TYPE;
238            }
239    
240            @Override
241            protected String getThumbnailType() {
242                    return THUMBNAIL_TYPE;
243            }
244    
245            private void _generateImages(FileVersion fileVersion)
246                    throws Exception {
247    
248                    try {
249                            if (_hasImages(fileVersion)) {
250                                    return;
251                            }
252    
253                            String extension = fileVersion.getExtension();
254    
255                            if (extension.equals("pdf")) {
256                                    if (fileVersion instanceof LiferayFileVersion) {
257                                            try {
258                                                    LiferayFileVersion liferayFileVersion =
259                                                            (LiferayFileVersion)fileVersion;
260    
261                                                    File file = liferayFileVersion.getFile(false);
262    
263                                                    _generateImages(fileVersion, file);
264    
265                                                    return;
266                                            }
267                                            catch (UnsupportedOperationException uoe) {
268                                            }
269                                    }
270    
271                                    InputStream inputStream = fileVersion.getContentStream(false);
272    
273                                    _generateImages(fileVersion, inputStream);
274                            }
275                            else if (DocumentConversionUtil.isEnabled()) {
276                                    InputStream inputStream = fileVersion.getContentStream(false);
277    
278                                    String tempFileId = DLUtil.getTempFileId(
279                                            fileVersion.getFileEntryId(), fileVersion.getVersion());
280    
281                                    File file = DocumentConversionUtil.convert(
282                                            tempFileId, inputStream, extension, "pdf");
283    
284                                    _generateImages(fileVersion, file);
285                            }
286                    }
287                    catch (NoSuchFileEntryException nsfee) {
288                    }
289                    finally {
290                            _fileVersionIds.remove(fileVersion.getFileVersionId());
291                    }
292            }
293    
294            private void _generateImages(FileVersion fileVersion, File file)
295                    throws Exception {
296    
297                    if (isImageMagickEnabled()) {
298                            _generateImagesIM(fileVersion, file);
299                    }
300                    else {
301                            _generateImagesPB(fileVersion, file);
302                    }
303            }
304    
305            private void _generateImages(
306                            FileVersion fileVersion, InputStream inputStream)
307                    throws Exception {
308    
309                    if (isImageMagickEnabled()) {
310                            _generateImagesIM(fileVersion, inputStream);
311                    }
312                    else {
313                            _generateImagesPB(fileVersion, inputStream);
314                    }
315            }
316    
317            private void _generateImagesIM(FileVersion fileVersion, File file)
318                    throws Exception {
319    
320                    if (_isGeneratePreview(fileVersion)) {
321                            _generateImagesIM(
322                                    fileVersion, file,
323                                    PropsValues.DL_FILE_ENTRY_PREVIEW_DOCUMENT_DEPTH,
324                                    PropsValues.DL_FILE_ENTRY_PREVIEW_DOCUMENT_DPI,
325                                    PropsValues.DL_FILE_ENTRY_PREVIEW_DOCUMENT_MAX_HEIGHT,
326                                    PropsValues.DL_FILE_ENTRY_PREVIEW_DOCUMENT_MAX_WIDTH, false);
327    
328                            if (_log.isInfoEnabled()) {
329                                    int previewFileCount = getPreviewFileCount(fileVersion);
330    
331                                    _log.info(
332                                            "ImageMagick generated " + previewFileCount +
333                                                    " preview pages for " +
334                                                            fileVersion.getFileVersionId());
335                            }
336                    }
337    
338                    if (_isGenerateThumbnail(fileVersion)) {
339                            _generateImagesIM(
340                                    fileVersion, file,
341                                    PropsValues.DL_FILE_ENTRY_THUMBNAIL_DOCUMENT_DEPTH,
342                                    PropsValues.DL_FILE_ENTRY_THUMBNAIL_DOCUMENT_DPI,
343                                    PrefsPropsUtil.getInteger(
344                                            PropsKeys.DL_FILE_ENTRY_THUMBNAIL_MAX_HEIGHT),
345                                    PrefsPropsUtil.getInteger(
346                                            PropsKeys.DL_FILE_ENTRY_THUMBNAIL_MAX_WIDTH),
347                                    true);
348    
349                            if (_log.isInfoEnabled()) {
350                                    _log.info(
351                                            "ImageMagick generated a thumbnail for " +
352                                                    fileVersion.getFileVersionId());
353                            }
354                    }
355            }
356    
357            private void _generateImagesIM(
358                            FileVersion fileVersion, File file, int depth, int dpi, int height,
359                            int width, boolean thumbnail)
360                    throws Exception {
361    
362                    // Generate images
363    
364                    String tempFileId = DLUtil.getTempFileId(
365                            fileVersion.getFileEntryId(), fileVersion.getVersion());
366    
367                    IMOperation imOperation = new IMOperation();
368    
369                    imOperation.alpha("off");
370    
371                    imOperation.density(dpi, dpi);
372    
373                    if (height != 0) {
374                            imOperation.adaptiveResize(width, height);
375                    }
376                    else {
377                            imOperation.adaptiveResize(width);
378                    }
379    
380                    imOperation.depth(depth);
381    
382                    if (thumbnail) {
383                            imOperation.addImage(file.getPath() + "[0]");
384                            imOperation.addImage(getThumbnailTempFilePath(tempFileId));
385                    }
386                    else {
387                            imOperation.addImage(file.getPath());
388                            imOperation.addImage(getPreviewTempFilePath(tempFileId, -1));
389                    }
390    
391                    _convertCmd.run(imOperation);
392    
393                    // Store images
394    
395                    if (thumbnail) {
396                            File thumbnailTempFile = getThumbnailTempFile(tempFileId);
397    
398                            try {
399                                    addFileToStore(
400                                            fileVersion.getCompanyId(), THUMBNAIL_PATH,
401                                            getThumbnailFilePath(fileVersion), thumbnailTempFile);
402                            }
403                            finally {
404                                    FileUtil.delete(thumbnailTempFile);
405                            }
406                    }
407                    else {
408    
409                            // ImageMagick converts single page PDFs without appending an
410                            // index. Rename file for consistency.
411    
412                            File singlePagePreviewFile = getPreviewTempFile(tempFileId, -1);
413    
414                            if (singlePagePreviewFile.exists()) {
415                                    singlePagePreviewFile.renameTo(
416                                            getPreviewTempFile(tempFileId, 1));
417                            }
418    
419                            int total = getPreviewTempFileCount(fileVersion);
420    
421                            for (int i = 0; i < total; i++) {
422                                    File previewTempFile = getPreviewTempFile(tempFileId, i + 1);
423    
424                                    try {
425                                            addFileToStore(
426                                                    fileVersion.getCompanyId(), PREVIEW_PATH,
427                                                    getPreviewFilePath(fileVersion, i + 1),
428                                                    previewTempFile);
429                                    }
430                                    finally {
431                                            FileUtil.delete(previewTempFile);
432                                    }
433                            }
434                    }
435            }
436    
437            private void _generateImagesIM(
438                            FileVersion fileVersion, InputStream inputStream)
439                    throws Exception {
440    
441                    File file = FileUtil.createTempFile(inputStream);
442    
443                    try {
444                            _generateImagesIM(fileVersion, file);
445                    }
446                    finally {
447                            FileUtil.delete(file);
448                    }
449            }
450    
451            private void _generateImagesPB(FileVersion fileVersion, File file)
452                    throws Exception {
453    
454                    _generateImagesPB(fileVersion, new FileInputStream(file));
455            }
456    
457            private void _generateImagesPB(
458                            FileVersion fileVersion, InputStream inputStream)
459                    throws Exception {
460    
461                    boolean generatePreview = _isGeneratePreview(fileVersion);
462                    boolean generateThumbnail = _isGenerateThumbnail(fileVersion);
463    
464                    PDDocument pdDocument = null;
465    
466                    try {
467                            pdDocument = PDDocument.load(inputStream);
468    
469                            PDDocumentCatalog pdDocumentCatalog =
470                                    pdDocument.getDocumentCatalog();
471    
472                            List<PDPage> pdPages = pdDocumentCatalog.getAllPages();
473    
474                            for (int i = 0; i < pdPages.size(); i++) {
475                                    PDPage pdPage = pdPages.get(i);
476    
477                                    if (generateThumbnail && (i == 0)) {
478                                            _generateImagesPB(
479                                                    fileVersion, pdPage,
480                                                    PropsValues.DL_FILE_ENTRY_THUMBNAIL_DOCUMENT_DPI,
481                                                    PrefsPropsUtil.getInteger(
482                                                            PropsKeys.DL_FILE_ENTRY_THUMBNAIL_MAX_HEIGHT),
483                                                    PrefsPropsUtil.getInteger(
484                                                            PropsKeys.DL_FILE_ENTRY_THUMBNAIL_MAX_WIDTH),
485                                                    true, 0);
486    
487                                            if (_log.isInfoEnabled()) {
488                                                    _log.info(
489                                                            "PDFBox generated a thumbnail for " +
490                                                                    fileVersion.getFileVersionId());
491                                            }
492                                    }
493    
494                                    if (!generatePreview) {
495                                            break;
496                                    }
497    
498                                    _generateImagesPB(
499                                            fileVersion, pdPage,
500                                            PropsValues.DL_FILE_ENTRY_PREVIEW_DOCUMENT_DPI,
501                                            PropsValues.DL_FILE_ENTRY_PREVIEW_DOCUMENT_MAX_HEIGHT,
502                                            PropsValues.DL_FILE_ENTRY_PREVIEW_DOCUMENT_MAX_WIDTH, false,
503                                            i + 1);
504                            }
505    
506                            if (_log.isInfoEnabled() && generatePreview) {
507                                    _log.info(
508                                            "PDFBox generated " +
509                                                    getPreviewFileCount(fileVersion) +
510                                                            " preview pages for " +
511                                                                    fileVersion.getFileVersionId());
512                            }
513                    }
514                    finally {
515                            if (pdDocument != null) {
516                                    pdDocument.close();
517                            }
518                    }
519            }
520    
521            private void _generateImagesPB(
522                            FileVersion fileVersion, PDPage pdPage, int dpi, int height,
523                            int width, boolean thumbnail, int index)
524                    throws Exception {
525    
526                    // Generate images
527    
528                    RenderedImage renderedImage = pdPage.convertToImage(
529                            BufferedImage.TYPE_INT_RGB,
530                            PropsValues.DL_FILE_ENTRY_THUMBNAIL_DOCUMENT_DPI);
531    
532                    if (height != 0) {
533                            renderedImage = ImageProcessorUtil.scale(
534                                    renderedImage, width, height);
535                    }
536                    else {
537                            renderedImage = ImageProcessorUtil.scale(renderedImage, width);
538                    }
539    
540                    // Store images
541    
542                    String tempFileId = DLUtil.getTempFileId(
543                            fileVersion.getFileEntryId(), fileVersion.getVersion());
544    
545                    File thumbnailTempFile = null;
546    
547                    try {
548                            if (thumbnail) {
549                                    thumbnailTempFile = getThumbnailTempFile(tempFileId);
550    
551                                    thumbnailTempFile.createNewFile();
552    
553                                    ImageIO.write(
554                                            renderedImage, THUMBNAIL_TYPE,
555                                            new FileOutputStream(thumbnailTempFile));
556    
557                                    addFileToStore(
558                                            fileVersion.getCompanyId(), THUMBNAIL_PATH,
559                                            getThumbnailFilePath(fileVersion), thumbnailTempFile);
560                            }
561                            else {
562                                    thumbnailTempFile = getPreviewTempFile(tempFileId, index);
563    
564                                    thumbnailTempFile.createNewFile();
565    
566                                    ImageIO.write(
567                                            renderedImage, PREVIEW_TYPE,
568                                            new FileOutputStream(thumbnailTempFile));
569    
570                                    addFileToStore(
571                                            fileVersion.getCompanyId(), PREVIEW_PATH,
572                                            getPreviewFilePath(fileVersion, index), thumbnailTempFile);
573                            }
574                    }
575                    finally {
576                            FileUtil.delete(thumbnailTempFile);
577                    }
578            }
579    
580            private boolean _hasImages(FileVersion fileVersion) throws Exception {
581                    boolean previewExists = DLStoreUtil.hasFile(
582                            fileVersion.getCompanyId(), REPOSITORY_ID,
583                            getPreviewFilePath(fileVersion, 1));
584                    boolean thumbnailExists = DLStoreUtil.hasFile(
585                            fileVersion.getCompanyId(), REPOSITORY_ID,
586                            getThumbnailFilePath(fileVersion));
587    
588                    if (PropsValues.DL_FILE_ENTRY_PREVIEW_ENABLED &&
589                            PropsValues.DL_FILE_ENTRY_THUMBNAIL_ENABLED) {
590    
591                            if (previewExists && thumbnailExists) {
592                                    return true;
593                            }
594                    }
595                    else  if (PropsValues.DL_FILE_ENTRY_PREVIEW_ENABLED && previewExists) {
596                            return true;
597                    }
598                    else if (PropsValues.DL_FILE_ENTRY_THUMBNAIL_ENABLED &&
599                                     thumbnailExists) {
600    
601                            return true;
602                    }
603    
604                    return false;
605            }
606    
607            private boolean _isGeneratePreview(FileVersion fileVersion)
608                    throws Exception {
609    
610                    String previewFilePath = getPreviewFilePath(fileVersion, 1);
611    
612                    if (PropsValues.DL_FILE_ENTRY_PREVIEW_ENABLED &&
613                            !DLStoreUtil.hasFile(
614                                    fileVersion.getCompanyId(), REPOSITORY_ID, previewFilePath)) {
615    
616                            return true;
617                    }
618                    else {
619                            return false;
620                    }
621            }
622    
623            private boolean _isGenerateThumbnail(FileVersion fileVersion)
624                    throws Exception {
625    
626                    String thumbnailFilePath = getThumbnailFilePath(fileVersion);
627    
628                    if (PropsValues.DL_FILE_ENTRY_THUMBNAIL_ENABLED &&
629                            !DLStoreUtil.hasFile(
630                                    fileVersion.getCompanyId(), REPOSITORY_ID, thumbnailFilePath)) {
631    
632                            return true;
633                    }
634                    else {
635                            return false;
636                    }
637            }
638    
639            private void _queueGeneration(FileVersion fileVersion) {
640                    if (_fileVersionIds.contains(fileVersion.getFileVersionId())) {
641                            return;
642                    }
643    
644                    boolean generateImages = false;
645    
646                    String extension = fileVersion.getExtension();
647    
648                    if (extension.equals("pdf")) {
649                            generateImages = true;
650                    }
651                    else if (DocumentConversionUtil.isEnabled()) {
652                            String[] conversions = DocumentConversionUtil.getConversions(
653                                    extension);
654    
655                            for (String conversion : conversions) {
656                                    if (conversion.equals("pdf")) {
657                                            generateImages = true;
658    
659                                            break;
660                                    }
661                            }
662                    }
663    
664                    if (generateImages) {
665                            _fileVersionIds.add(fileVersion.getFileVersionId());
666    
667                            MessageBusUtil.sendMessage(
668                                    DestinationNames.DOCUMENT_LIBRARY_PDF_PROCESSOR, fileVersion);
669                    }
670            }
671    
672            private static Log _log = LogFactoryUtil.getLog(PDFProcessor.class);
673    
674            private static PDFProcessor _instance = new PDFProcessor();
675    
676            private static ConvertCmd _convertCmd;
677            private static List<Long> _fileVersionIds = new Vector<Long>();
678            private static boolean _warned;
679    
680    }