001
014
015 package com.liferay.portlet.documentlibrary.util;
016
017 import com.liferay.portal.kernel.image.GhostscriptUtil;
018 import com.liferay.portal.kernel.lar.PortletDataContext;
019 import com.liferay.portal.kernel.log.Log;
020 import com.liferay.portal.kernel.log.LogFactoryUtil;
021 import com.liferay.portal.kernel.messaging.DestinationNames;
022 import com.liferay.portal.kernel.process.ClassPathUtil;
023 import com.liferay.portal.kernel.process.ProcessCallable;
024 import com.liferay.portal.kernel.process.ProcessException;
025 import com.liferay.portal.kernel.process.ProcessExecutor;
026 import com.liferay.portal.kernel.repository.model.FileEntry;
027 import com.liferay.portal.kernel.repository.model.FileVersion;
028 import com.liferay.portal.kernel.util.ContentTypes;
029 import com.liferay.portal.kernel.util.FileUtil;
030 import com.liferay.portal.kernel.util.GetterUtil;
031 import com.liferay.portal.kernel.util.MimeTypesUtil;
032 import com.liferay.portal.kernel.util.PropsKeys;
033 import com.liferay.portal.kernel.util.ServerDetector;
034 import com.liferay.portal.kernel.util.StreamUtil;
035 import com.liferay.portal.kernel.util.StringPool;
036 import com.liferay.portal.kernel.util.SystemEnv;
037 import com.liferay.portal.kernel.util.Validator;
038 import com.liferay.portal.kernel.xml.Element;
039 import com.liferay.portal.log.Log4jLogFactoryImpl;
040 import com.liferay.portal.repository.liferayrepository.model.LiferayFileVersion;
041 import com.liferay.portal.util.PropsUtil;
042 import com.liferay.portal.util.PropsValues;
043 import com.liferay.portlet.documentlibrary.NoSuchFileEntryException;
044 import com.liferay.portlet.documentlibrary.store.DLStoreUtil;
045 import com.liferay.util.log4j.Log4JUtil;
046
047 import java.io.File;
048 import java.io.InputStream;
049
050 import java.util.ArrayList;
051 import java.util.Arrays;
052 import java.util.List;
053 import java.util.Map;
054 import java.util.Properties;
055 import java.util.Set;
056 import java.util.Vector;
057 import java.util.concurrent.Future;
058
059 import org.apache.commons.lang.time.StopWatch;
060 import org.apache.pdfbox.pdmodel.PDDocument;
061
062
069 public class PDFProcessorImpl
070 extends DLPreviewableProcessor implements PDFProcessor {
071
072 @Override
073 public void afterPropertiesSet() throws Exception {
074 FileUtil.mkdirs(PREVIEW_TMP_PATH);
075 FileUtil.mkdirs(THUMBNAIL_TMP_PATH);
076 }
077
078 @Override
079 public void generateImages(
080 FileVersion sourceFileVersion, FileVersion destinationFileVersion)
081 throws Exception {
082
083 _generateImages(sourceFileVersion, destinationFileVersion);
084 }
085
086 @Override
087 public InputStream getPreviewAsStream(FileVersion fileVersion, int index)
088 throws Exception {
089
090 return doGetPreviewAsStream(fileVersion, index, PREVIEW_TYPE);
091 }
092
093 @Override
094 public int getPreviewFileCount(FileVersion fileVersion) {
095 try {
096 return doGetPreviewFileCount(fileVersion);
097 }
098 catch (Exception e) {
099 _log.error(e, e);
100 }
101
102 return 0;
103 }
104
105 @Override
106 public long getPreviewFileSize(FileVersion fileVersion, int index)
107 throws Exception {
108
109 return doGetPreviewFileSize(fileVersion, index);
110 }
111
112 @Override
113 public InputStream getThumbnailAsStream(FileVersion fileVersion, int index)
114 throws Exception {
115
116 return doGetThumbnailAsStream(fileVersion, index);
117 }
118
119 @Override
120 public long getThumbnailFileSize(FileVersion fileVersion, int index)
121 throws Exception {
122
123 return doGetThumbnailFileSize(fileVersion, index);
124 }
125
126 @Override
127 public boolean hasImages(FileVersion fileVersion) {
128 boolean hasImages = false;
129
130 try {
131 hasImages = _hasImages(fileVersion);
132
133 if (!hasImages && isSupported(fileVersion)) {
134 _queueGeneration(null, fileVersion);
135 }
136 }
137 catch (Exception e) {
138 _log.error(e, e);
139 }
140
141 return hasImages;
142 }
143
144 @Override
145 public boolean isDocumentSupported(FileVersion fileVersion) {
146 return isSupported(fileVersion);
147 }
148
149 @Override
150 public boolean isDocumentSupported(String mimeType) {
151 return isSupported(mimeType);
152 }
153
154 @Override
155 public boolean isSupported(String mimeType) {
156 if (Validator.isNull(mimeType)) {
157 return false;
158 }
159
160 if (mimeType.equals(ContentTypes.APPLICATION_PDF) ||
161 mimeType.equals(ContentTypes.APPLICATION_X_PDF)) {
162
163 return true;
164 }
165
166 if (DocumentConversionUtil.isEnabled()) {
167 Set<String> extensions = MimeTypesUtil.getExtensions(mimeType);
168
169 for (String extension : extensions) {
170 extension = extension.substring(1);
171
172 String[] targetExtensions =
173 DocumentConversionUtil.getConversions(extension);
174
175 if (Arrays.binarySearch(targetExtensions, "pdf") >= 0) {
176 return true;
177 }
178 }
179 }
180
181 return false;
182 }
183
184 @Override
185 public void trigger(
186 FileVersion sourceFileVersion, FileVersion destinationFileVersion) {
187
188 super.trigger(sourceFileVersion, destinationFileVersion);
189
190 _queueGeneration(sourceFileVersion, destinationFileVersion);
191 }
192
193 @Override
194 protected void copyPreviews(
195 FileVersion sourceFileVersion, FileVersion destinationFileVersion) {
196
197 if (!PropsValues.DL_FILE_ENTRY_PREVIEW_ENABLED) {
198 return;
199 }
200
201 try {
202 if (hasPreview(sourceFileVersion) &&
203 !hasPreview(destinationFileVersion)) {
204
205 int count = getPreviewFileCount(sourceFileVersion);
206
207 for (int i = 0; i < count; i++) {
208 String previewFilePath = getPreviewFilePath(
209 destinationFileVersion, i + 1);
210
211 InputStream is = doGetPreviewAsStream(
212 sourceFileVersion, i + 1, PREVIEW_TYPE);
213
214 addFileToStore(
215 destinationFileVersion.getCompanyId(), PREVIEW_PATH,
216 previewFilePath, is);
217 }
218 }
219 }
220 catch (Exception e) {
221 _log.error(e, e);
222 }
223 }
224
225 @Override
226 protected void doExportGeneratedFiles(
227 PortletDataContext portletDataContext, FileEntry fileEntry,
228 Element fileEntryElement)
229 throws Exception {
230
231 exportThumbnails(
232 portletDataContext, fileEntry, fileEntryElement, "pdf");
233
234 exportPreviews(portletDataContext, fileEntry, fileEntryElement);
235 }
236
237 @Override
238 protected void doImportGeneratedFiles(
239 PortletDataContext portletDataContext, FileEntry fileEntry,
240 FileEntry importedFileEntry, Element fileEntryElement)
241 throws Exception {
242
243 importThumbnails(
244 portletDataContext, fileEntry, importedFileEntry, fileEntryElement,
245 "pdf");
246
247 importPreviews(
248 portletDataContext, fileEntry, importedFileEntry, fileEntryElement);
249 }
250
251 protected void exportPreviews(
252 PortletDataContext portletDataContext, FileEntry fileEntry,
253 Element fileEntryElement)
254 throws Exception {
255
256 FileVersion fileVersion = fileEntry.getFileVersion();
257
258 if (!isSupported(fileVersion) || !_hasImages(fileVersion)) {
259 return;
260 }
261
262 if (!portletDataContext.isPerformDirectBinaryImport()) {
263 int previewFileCount = getPreviewFileCount(fileVersion);
264
265 fileEntryElement.addAttribute(
266 "bin-path-pdf-preview-count", String.valueOf(previewFileCount));
267
268 for (int i = 0; i < previewFileCount; i++) {
269 exportPreview(
270 portletDataContext, fileEntry, fileEntryElement, "pdf",
271 PREVIEW_TYPE, i);
272 }
273 }
274 }
275
276 @Override
277 protected List<Long> getFileVersionIds() {
278 return _fileVersionIds;
279 }
280
281 @Override
282 protected String getPreviewType(FileVersion fileVersion) {
283 return PREVIEW_TYPE;
284 }
285
286 @Override
287 protected String getThumbnailType(FileVersion fileVersion) {
288 return THUMBNAIL_TYPE;
289 }
290
291 protected boolean hasPreview(FileVersion fileVersion) throws Exception {
292 return hasPreview(fileVersion, null);
293 }
294
295 @Override
296 protected boolean hasPreview(FileVersion fileVersion, String type)
297 throws Exception {
298
299 String previewFilePath = getPreviewFilePath(fileVersion, 1);
300
301 return DLStoreUtil.hasFile(
302 fileVersion.getCompanyId(), REPOSITORY_ID, previewFilePath);
303 }
304
305 protected void importPreviews(
306 PortletDataContext portletDataContext, FileEntry fileEntry,
307 FileEntry importedFileEntry, Element fileEntryElement)
308 throws Exception {
309
310 int previewFileCount = GetterUtil.getInteger(
311 fileEntryElement.attributeValue("bin-path-pdf-preview-count"));
312
313 for (int i = 0; i < previewFileCount; i++) {
314 importPreview(
315 portletDataContext, fileEntry, importedFileEntry,
316 fileEntryElement, "pdf", PREVIEW_TYPE, i);
317 }
318 }
319
320 private void _generateImages(FileVersion fileVersion, File file)
321 throws Exception {
322
323 if (GhostscriptUtil.isEnabled()) {
324 if (!_ghostscriptInitialized) {
325 GhostscriptUtil.reset();
326
327 _ghostscriptInitialized = true;
328 }
329
330 _generateImagesGS(fileVersion, file);
331 }
332 else {
333 _generateImagesPB(fileVersion, file);
334 }
335 }
336
337 private void _generateImages(
338 FileVersion sourceFileVersion, FileVersion destinationFileVersion)
339 throws Exception {
340
341 InputStream inputStream = null;
342
343 try {
344 if (sourceFileVersion != null) {
345 copy(sourceFileVersion, destinationFileVersion);
346
347 return;
348 }
349
350 if (_hasImages(destinationFileVersion)) {
351 return;
352 }
353
354 String extension = destinationFileVersion.getExtension();
355
356 if (extension.equals("pdf")) {
357 if (destinationFileVersion instanceof LiferayFileVersion) {
358 try {
359 LiferayFileVersion liferayFileVersion =
360 (LiferayFileVersion)destinationFileVersion;
361
362 File file = liferayFileVersion.getFile(false);
363
364 _generateImages(destinationFileVersion, file);
365
366 return;
367 }
368 catch (UnsupportedOperationException uoe) {
369 }
370 }
371
372 inputStream = destinationFileVersion.getContentStream(false);
373
374 _generateImages(destinationFileVersion, inputStream);
375 }
376 else if (DocumentConversionUtil.isEnabled()) {
377 inputStream = destinationFileVersion.getContentStream(false);
378
379 String tempFileId = DLUtil.getTempFileId(
380 destinationFileVersion.getFileEntryId(),
381 destinationFileVersion.getVersion());
382
383 File file = DocumentConversionUtil.convert(
384 tempFileId, inputStream, extension, "pdf");
385
386 _generateImages(destinationFileVersion, file);
387 }
388 }
389 catch (NoSuchFileEntryException nsfee) {
390 }
391 finally {
392 StreamUtil.cleanUp(inputStream);
393
394 _fileVersionIds.remove(destinationFileVersion.getFileVersionId());
395 }
396 }
397
398 private void _generateImages(
399 FileVersion fileVersion, InputStream inputStream)
400 throws Exception {
401
402 if (GhostscriptUtil.isEnabled()) {
403 _generateImagesGS(fileVersion, inputStream);
404 }
405 else {
406 _generateImagesPB(fileVersion, inputStream);
407 }
408 }
409
410 private void _generateImagesGS(FileVersion fileVersion, File file)
411 throws Exception {
412
413 if (_isGeneratePreview(fileVersion)) {
414 StopWatch stopWatch = new StopWatch();
415
416 stopWatch.start();
417
418 _generateImagesGS(fileVersion, file, false);
419
420 if (_log.isInfoEnabled()) {
421 int previewFileCount = getPreviewFileCount(fileVersion);
422
423 _log.info(
424 "Ghostscript generated " + previewFileCount +
425 " preview pages for " + fileVersion.getTitle() +
426 " in " + stopWatch.getTime() + " ms");
427 }
428 }
429
430 if (_isGenerateThumbnail(fileVersion)) {
431 StopWatch stopWatch = new StopWatch();
432
433 stopWatch.start();
434
435 _generateImagesGS(fileVersion, file, true);
436
437 if (_log.isInfoEnabled()) {
438 _log.info(
439 "Ghostscript generated a thumbnail for " +
440 fileVersion.getTitle() + " in " + stopWatch.getTime() +
441 " ms");
442 }
443 }
444 }
445
446 private void _generateImagesGS(
447 FileVersion fileVersion, File file, boolean thumbnail)
448 throws Exception {
449
450
451
452 String tempFileId = DLUtil.getTempFileId(
453 fileVersion.getFileEntryId(), fileVersion.getVersion());
454
455 List<String> arguments = new ArrayList<String>();
456
457 arguments.add("-sDEVICE=png16m");
458
459 if (thumbnail) {
460 arguments.add(
461 "-sOutputFile=" + getThumbnailTempFilePath(tempFileId));
462 arguments.add("-dFirstPage=1");
463 arguments.add("-dLastPage=1");
464 }
465 else {
466 arguments.add(
467 "-sOutputFile=" + getPreviewTempFilePath(tempFileId, -1));
468 }
469
470 arguments.add("-dPDFFitPage");
471 arguments.add("-dTextAlphaBits=4");
472 arguments.add("-dGraphicsAlphaBits=4");
473 arguments.add("-r" + PropsValues.DL_FILE_ENTRY_PREVIEW_DOCUMENT_DPI);
474
475 if (PropsValues.DL_FILE_ENTRY_PREVIEW_DOCUMENT_MAX_WIDTH != 0) {
476 arguments.add(
477 "-dDEVICEWIDTH" +
478 PropsValues.DL_FILE_ENTRY_PREVIEW_DOCUMENT_MAX_WIDTH);
479 }
480
481 if (PropsValues.DL_FILE_ENTRY_PREVIEW_DOCUMENT_MAX_HEIGHT != 0) {
482 arguments.add(
483 "-dDEVICEHEIGHT" +
484 PropsValues.DL_FILE_ENTRY_PREVIEW_DOCUMENT_MAX_HEIGHT);
485 }
486
487 arguments.add(file.getPath());
488
489 Future<?> future = GhostscriptUtil.execute(arguments);
490
491 String processIdentity = String.valueOf(fileVersion.getFileVersionId());
492
493 futures.put(processIdentity, future);
494
495 future.get();
496
497
498
499 if (thumbnail) {
500 File thumbnailTempFile = getThumbnailTempFile(tempFileId);
501
502 try {
503 storeThumbnailImages(fileVersion, thumbnailTempFile);
504 }
505 finally {
506 FileUtil.delete(thumbnailTempFile);
507 }
508 }
509 else {
510 int total = getPreviewTempFileCount(fileVersion);
511
512 for (int i = 0; i < total; i++) {
513 File previewTempFile = getPreviewTempFile(tempFileId, i + 2);
514
515 try {
516 addFileToStore(
517 fileVersion.getCompanyId(), PREVIEW_PATH,
518 getPreviewFilePath(fileVersion, i + 1),
519 previewTempFile);
520 }
521 finally {
522 FileUtil.delete(previewTempFile);
523 }
524 }
525 }
526 }
527
528 private void _generateImagesGS(
529 FileVersion fileVersion, InputStream inputStream)
530 throws Exception {
531
532 File file = null;
533
534 try {
535 file = FileUtil.createTempFile(inputStream);
536
537 _generateImagesGS(fileVersion, file);
538 }
539 finally {
540 FileUtil.delete(file);
541 }
542 }
543
544 private void _generateImagesPB(FileVersion fileVersion, File file)
545 throws Exception {
546
547 String tempFileId = DLUtil.getTempFileId(
548 fileVersion.getFileEntryId(), fileVersion.getVersion());
549
550 File thumbnailFile = getThumbnailTempFile(tempFileId);
551
552 int previewFilesCount = 0;
553
554 PDDocument pdDocument = null;
555
556 try {
557 pdDocument = PDDocument.load(file);
558
559 previewFilesCount = pdDocument.getNumberOfPages();
560 }
561 finally {
562 if (pdDocument != null) {
563 pdDocument.close();
564 }
565 }
566
567 File[] previewFiles = new File[previewFilesCount];
568
569 for (int i = 0; i < previewFilesCount; i++) {
570 previewFiles[i] = getPreviewTempFile(tempFileId, i);
571 }
572
573 boolean generatePreview = _isGeneratePreview(fileVersion);
574 boolean generateThumbnail = _isGenerateThumbnail(fileVersion);
575
576 if (PropsValues.DL_FILE_ENTRY_PREVIEW_FORK_PROCESS_ENABLED) {
577 ProcessCallable<String> processCallable =
578 new LiferayPDFBoxProcessCallable(
579 ServerDetector.getServerId(),
580 PropsUtil.get(PropsKeys.LIFERAY_HOME),
581 Log4JUtil.getCustomLogSettings(), file, thumbnailFile,
582 previewFiles, getThumbnailType(fileVersion),
583 getPreviewType(fileVersion),
584 PropsValues.DL_FILE_ENTRY_PREVIEW_DOCUMENT_DPI,
585 PropsValues.DL_FILE_ENTRY_PREVIEW_DOCUMENT_MAX_HEIGHT,
586 PropsValues.DL_FILE_ENTRY_PREVIEW_DOCUMENT_MAX_WIDTH,
587 generatePreview, generateThumbnail);
588
589 Future<String> future = ProcessExecutor.execute(
590 ClassPathUtil.getPortalClassPath(), processCallable);
591
592 String processIdentity = String.valueOf(
593 fileVersion.getFileVersionId());
594
595 futures.put(processIdentity, future);
596
597 future.get();
598 }
599 else {
600 LiferayPDFBoxConverter liferayConverter =
601 new LiferayPDFBoxConverter(
602 file, thumbnailFile, previewFiles,
603 getPreviewType(fileVersion), getThumbnailType(fileVersion),
604 PropsValues.DL_FILE_ENTRY_PREVIEW_DOCUMENT_DPI,
605 PropsValues.DL_FILE_ENTRY_PREVIEW_DOCUMENT_MAX_HEIGHT,
606 PropsValues.DL_FILE_ENTRY_PREVIEW_DOCUMENT_MAX_WIDTH,
607 generatePreview, generateThumbnail);
608
609 liferayConverter.generateImagesPB();
610 }
611
612 if (generateThumbnail) {
613 try {
614 storeThumbnailImages(fileVersion, thumbnailFile);
615 }
616 finally {
617 FileUtil.delete(thumbnailFile);
618 }
619
620 if (_log.isInfoEnabled()) {
621 _log.info(
622 "PDFBox generated a thumbnail for " +
623 fileVersion.getFileVersionId());
624 }
625 }
626
627 if (generatePreview) {
628 int index = 0;
629
630 for (File previewFile : previewFiles) {
631 try {
632 addFileToStore(
633 fileVersion.getCompanyId(), PREVIEW_PATH,
634 getPreviewFilePath(fileVersion, index +1), previewFile);
635 }
636 finally {
637 FileUtil.delete(previewFile);
638 }
639
640 index++;
641 }
642
643 if (_log.isInfoEnabled()) {
644 _log.info(
645 "PDFBox generated " +
646 getPreviewFileCount(fileVersion) +
647 " preview pages for " +
648 fileVersion.getFileVersionId());
649 }
650 }
651 }
652
653 private void _generateImagesPB(
654 FileVersion fileVersion, InputStream inputStream)
655 throws Exception {
656
657 File file = null;
658
659 try {
660 file = FileUtil.createTempFile(inputStream);
661
662 _generateImagesPB(fileVersion, file);
663 }
664 finally {
665 FileUtil.delete(file);
666 }
667 }
668
669 private boolean _hasImages(FileVersion fileVersion) throws Exception {
670 if (PropsValues.DL_FILE_ENTRY_PREVIEW_ENABLED) {
671 if (!hasPreview(fileVersion)) {
672 return false;
673 }
674 }
675
676 return hasThumbnails(fileVersion);
677 }
678
679 private boolean _isGeneratePreview(FileVersion fileVersion)
680 throws Exception {
681
682 if (PropsValues.DL_FILE_ENTRY_PREVIEW_ENABLED &&
683 !hasPreview(fileVersion)) {
684
685 return true;
686 }
687 else {
688 return false;
689 }
690 }
691
692 private boolean _isGenerateThumbnail(FileVersion fileVersion)
693 throws Exception {
694
695 if (PropsValues.DL_FILE_ENTRY_THUMBNAIL_ENABLED &&
696 !hasThumbnail(fileVersion, THUMBNAIL_INDEX_DEFAULT)) {
697
698 return true;
699 }
700 else {
701 return false;
702 }
703 }
704
705 private void _queueGeneration(
706 FileVersion sourceFileVersion, FileVersion destinationFileVersion) {
707
708 if (_fileVersionIds.contains(
709 destinationFileVersion.getFileVersionId())) {
710
711 return;
712 }
713
714 boolean generateImages = false;
715
716 String extension = destinationFileVersion.getExtension();
717
718 if (extension.equals("pdf")) {
719 generateImages = true;
720 }
721 else if (DocumentConversionUtil.isEnabled()) {
722 String[] conversions = DocumentConversionUtil.getConversions(
723 extension);
724
725 for (String conversion : conversions) {
726 if (conversion.equals("pdf")) {
727 generateImages = true;
728
729 break;
730 }
731 }
732 }
733
734 if (generateImages) {
735 _fileVersionIds.add(destinationFileVersion.getFileVersionId());
736
737 sendGenerationMessage(
738 DestinationNames.DOCUMENT_LIBRARY_PDF_PROCESSOR,
739 sourceFileVersion, destinationFileVersion);
740 }
741 }
742
743 private static Log _log = LogFactoryUtil.getLog(PDFProcessorImpl.class);
744
745 private List<Long> _fileVersionIds = new Vector<Long>();
746 private boolean _ghostscriptInitialized = false;
747
748 private static class LiferayPDFBoxProcessCallable
749 implements ProcessCallable<String> {
750
751 public LiferayPDFBoxProcessCallable(
752 String serverId, String liferayHome,
753 Map<String, String> customLogSettings, File inputFile,
754 File thumbnailFile, File[] previewFiles, String extension,
755 String thumbnailExtension, int dpi, int height, int width,
756 boolean generatePreview, boolean generateThumbnail) {
757
758 _serverId = serverId;
759 _liferayHome = liferayHome;
760 _customLogSettings = customLogSettings;
761 _inputFile = inputFile;
762 _thumbnailFile = thumbnailFile;
763 _previewFiles = previewFiles;
764 _extension = extension;
765 _thumbnailExtension = thumbnailExtension;
766 _dpi = dpi;
767 _height = height;
768 _width = width;
769 _generatePreview = generatePreview;
770 _generateThumbnail = generateThumbnail;
771 }
772
773 @Override
774 public String call() throws ProcessException {
775 Properties systemProperties = System.getProperties();
776
777 SystemEnv.setProperties(systemProperties);
778
779 Class<?> clazz = getClass();
780
781 ClassLoader classLoader = clazz.getClassLoader();
782
783 Log4JUtil.initLog4J(
784 _serverId, _liferayHome, classLoader, new Log4jLogFactoryImpl(),
785 _customLogSettings);
786
787 try {
788 LiferayPDFBoxConverter liferayConverter =
789 new LiferayPDFBoxConverter(
790 _inputFile, _thumbnailFile, _previewFiles, _extension,
791 _thumbnailExtension, _dpi, _height, _width,
792 _generatePreview, _generateThumbnail);
793
794 liferayConverter.generateImagesPB();
795 }
796 catch (Exception e) {
797 throw new ProcessException(e);
798 }
799
800 return StringPool.BLANK;
801 }
802
803 private static final long serialVersionUID = 1L;
804
805 private Map<String, String> _customLogSettings;
806 private int _dpi;
807 private String _extension;
808 private boolean _generatePreview;
809 private boolean _generateThumbnail;
810 private int _height;
811 private File _inputFile;
812 private String _liferayHome;
813 private File[] _previewFiles;
814 private String _serverId;
815 private String _thumbnailExtension;
816 private File _thumbnailFile;
817 private int _width;
818
819 }
820
821 }