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