diff --git a/frontend/src/pages/lib-content-view/lib-content-view.js b/frontend/src/pages/lib-content-view/lib-content-view.js index 9319f98d71..d19aa438ce 100644 --- a/frontend/src/pages/lib-content-view/lib-content-view.js +++ b/frontend/src/pages/lib-content-view/lib-content-view.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import cookie from 'react-cookies'; import moment from 'moment'; import { navigate } from '@gatsbyjs/reach-router'; -import { gettext, siteRoot, username, enableVideoThumbnail } from '../../utils/constants'; +import { gettext, siteRoot, username, enableVideoThumbnail, enablePDFThumbnail } from '../../utils/constants'; import { seafileAPI } from '../../utils/seafile-api'; import { Utils } from '../../utils/utils'; import collabServer from '../../utils/collab-server'; @@ -538,7 +538,7 @@ class LibContentView extends React.Component { getThumbnails = (repoID, path, direntList) => { let items = direntList.filter((item) => { - return (Utils.imageCheck(item.name) || (enableVideoThumbnail && Utils.videoCheck(item.name))) && !item.encoded_thumbnail_src; + return (Utils.imageCheck(item.name) || (enableVideoThumbnail && Utils.videoCheck(item.name)) || (enablePDFThumbnail && Utils.pdfCheck(item.name))) && !item.encoded_thumbnail_src; }); if (items.length == 0) { return ; diff --git a/frontend/src/pages/starred/starred.js b/frontend/src/pages/starred/starred.js index f574958734..f2783b6f72 100644 --- a/frontend/src/pages/starred/starred.js +++ b/frontend/src/pages/starred/starred.js @@ -5,7 +5,7 @@ import { Link, navigate } from '@gatsbyjs/reach-router'; import moment from 'moment'; import { seafileAPI } from '../../utils/seafile-api'; import { Utils } from '../../utils/utils'; -import { gettext, siteRoot, enableVideoThumbnail } from '../../utils/constants'; +import { gettext, siteRoot, enableVideoThumbnail, enablePDFThumbnail } from '../../utils/constants'; import EmptyTip from '../../components/empty-tip'; import Loading from '../../components/loading'; import toaster from '../../components/toast'; @@ -79,7 +79,10 @@ class TableBody extends Component { getThumbnails() { let items = this.state.items.filter((item) => { - return (Utils.imageCheck(item.obj_name) || (enableVideoThumbnail && Utils.videoCheck(item.obj_name))) && !item.repo_encrypted && !item.encoded_thumbnail_src && !item.deleted; + return (Utils.imageCheck(item.obj_name) || + (enableVideoThumbnail && Utils.videoCheck(item.obj_name)) || + (enablePDFThumbnail && Utils.pdfCheck(item.obj_name))) && + !item.repo_encrypted && !item.encoded_thumbnail_src && !item.deleted; }); if (items.length == 0) { return ; diff --git a/frontend/src/shared-dir-view.js b/frontend/src/shared-dir-view.js index e655d8933e..e8d61111f2 100644 --- a/frontend/src/shared-dir-view.js +++ b/frontend/src/shared-dir-view.js @@ -30,7 +30,7 @@ let { repoID, relativePath, mode, thumbnailSize, zipped, trafficOverLimit, canDownload, - noQuota, canUpload, enableVideoThumbnail + noQuota, canUpload, enableVideoThumbnail, enablePDFThumbnail } = window.shared.pageOptions; const showDownloadIcon = !trafficOverLimit && canDownload; @@ -111,7 +111,8 @@ class SharedDirView extends React.Component { let items = this.state.items.filter((item) => { return !item.is_dir && (Utils.imageCheck(item.file_name) || - (enableVideoThumbnail && Utils.videoCheck(item.file_name))) && + (enableVideoThumbnail && Utils.videoCheck(item.file_name)) || + (enablePDFThumbnail && Utils.pdfCheck(item.file_name))) && !item.encoded_thumbnail_src; }); if (items.length == 0) { diff --git a/frontend/src/utils/constants.js b/frontend/src/utils/constants.js index 7d3f50eed5..f74c515c53 100644 --- a/frontend/src/utils/constants.js +++ b/frontend/src/utils/constants.js @@ -91,6 +91,7 @@ export const curNoteID = window.app.pageOptions.curNoteID; export const enableTC = window.app.pageOptions.enableTC; export const enableVideoThumbnail = window.app.pageOptions.enableVideoThumbnail; +export const enablePDFThumbnail = window.app.pageOptions.enablePDFThumbnail; export const enableOnlyoffice = window.app.pageOptions.enableOnlyoffice || false; export const onlyofficeConverterExtensions = window.app.pageOptions.onlyofficeConverterExtensions || []; diff --git a/frontend/src/utils/utils.js b/frontend/src/utils/utils.js index 2e71ed2516..ef2ae025d2 100644 --- a/frontend/src/utils/utils.js +++ b/frontend/src/utils/utils.js @@ -124,6 +124,19 @@ export const Utils = { } }, + pdfCheck: function(filename) { + if (filename.lastIndexOf('.') == -1) { + return false; + } + var file_ext = filename.substr(filename.lastIndexOf('.') + 1).toLowerCase(); + var image_exts = ['pdf']; + if (image_exts.indexOf(file_ext) != -1) { + return true; + } else { + return false; + } + }, + getShareLinkPermissionList: function(itemType, permission, path, canEdit) { // itemType: library, dir, file // permission: rw, r, admin, cloud-edit, preview, custom-* diff --git a/requirements.txt b/requirements.txt index 882ca0eafd..3e43dcee4e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -26,3 +26,4 @@ Markdown==3.4.* bleach==5.0.* python-ldap==3.4.* pypinyin==0.50.* +PyMuPDF==1.24.* diff --git a/seahub/api2/endpoints/dir.py b/seahub/api2/endpoints/dir.py index cbd3c8c845..aee5ebca86 100644 --- a/seahub/api2/endpoints/dir.py +++ b/seahub/api2/endpoints/dir.py @@ -23,7 +23,7 @@ from seahub.utils import check_filename_with_rename, is_valid_dirent_name, \ normalize_dir_path, is_pro_version, FILEEXT_TYPE_MAP, get_file_type_and_ext from seahub.utils.timeutils import timestamp_to_isoformat_timestr from seahub.utils.file_tags import get_files_tags_in_dir -from seahub.utils.file_types import IMAGE, VIDEO, XMIND, SEADOC +from seahub.utils.file_types import IMAGE, VIDEO, XMIND, SEADOC, PDF from seahub.base.models import UserStarredFiles from seahub.base.templatetags.seahub_tags import email2nickname, \ email2contact_email @@ -175,7 +175,7 @@ def get_dir_file_info_list(username, request_type, repo_obj, parent_dir, fileExt = os.path.splitext(file_name)[1][1:].lower() file_type = FILEEXT_TYPE_MAP.get(fileExt) - if file_type in (IMAGE, XMIND) or \ + if file_type in (IMAGE, XMIND, PDF) or \ (file_type == VIDEO and ENABLE_VIDEO_THUMBNAIL): # if thumbnail has already been created, return its src. diff --git a/seahub/settings.py b/seahub/settings.py index 237df5b17b..305d41d6d5 100644 --- a/seahub/settings.py +++ b/seahub/settings.py @@ -747,6 +747,9 @@ THUMBNAIL_IMAGE_ORIGINAL_SIZE_LIMIT = 256 ENABLE_VIDEO_THUMBNAIL = False THUMBNAIL_VIDEO_FRAME_TIME = 5 # use the frame at 5 second as thumbnail +# pdf thumbnails +ENABLE_PDF_THUMBNAIL = True + # template for create new office file OFFICE_TEMPLATE_ROOT = os.path.join(MEDIA_ROOT, 'office-template') diff --git a/seahub/templates/base_for_react.html b/seahub/templates/base_for_react.html index 5a0f5f8cda..c725e6f22b 100644 --- a/seahub/templates/base_for_react.html +++ b/seahub/templates/base_for_react.html @@ -141,6 +141,7 @@ enableTC: {% if enable_terms_and_conditions %} true {% else %} false {% endif %}, enableSSOToThirdpartWebsite: {% if enable_sso_to_thirdpart_website %} true {% else %} false {% endif %}, enableVideoThumbnail: {% if enable_video_thumbnail %} true {% else %} false {% endif %}, + enablePDFThumbnail: {% if enable_pdf_thumbnail %} true {% else %} false {% endif %}, showLogoutIcon: {% if show_logout_icon %} true {% else %} false {% endif %}, additionalShareDialogNote: {% if additional_share_dialog_note %} {{ additional_share_dialog_note|safe }} {% else %} null {% endif %}, additionalAppBottomLinks: {% if additional_app_bottom_links %} {{ additional_app_bottom_links|safe }} {% else %} null {% endif %}, diff --git a/seahub/templates/common_file_view_react.html b/seahub/templates/common_file_view_react.html index 3bf5a1c05d..ac223584fc 100644 --- a/seahub/templates/common_file_view_react.html +++ b/seahub/templates/common_file_view_react.html @@ -24,6 +24,10 @@ thumbnailSizeForOriginal: {{ thumbnail_size_for_original }}, enableVideoThumbnail: {% if enable_video_thumbnail %} true {% else %} false {% endif %}, {% endif %} +{% if filetype == 'PDF' %} +enablePDFThumbnail: {% if enable_pdf_thumbnail %} true {% else %} false {% endif %}, +{% endif %} + {% if filetype == 'XMind' %} xmindImageSrc: '{{ xmind_image_src|escapejs }}', {% endif %} diff --git a/seahub/templates/view_shared_dir_react.html b/seahub/templates/view_shared_dir_react.html index aa562339ef..8df4fac53c 100644 --- a/seahub/templates/view_shared_dir_react.html +++ b/seahub/templates/view_shared_dir_react.html @@ -38,6 +38,7 @@ mode: '{{ mode }}', thumbnailSize: {{ thumbnail_size }}, enableVideoThumbnail: {% if enable_video_thumbnail %}true{% else %}false{% endif %}, + enablePDFThumbnail: {% if enable_pdf_thumbnail %}true{% else %}false{% endif %}, trafficOverLimit: {% if traffic_over_limit %}true{% else %}false{% endif %}, canDownload: {% if permissions.can_download %}true{% else %}false{% endif %}, noQuota: {% if no_quota %}true{% else %}false{% endif %}, diff --git a/seahub/thumbnail/utils.py b/seahub/thumbnail/utils.py index 0e69ba3084..6744a44c66 100644 --- a/seahub/thumbnail/utils.py +++ b/seahub/thumbnail/utils.py @@ -8,6 +8,7 @@ import logging import subprocess from io import BytesIO import zipfile +from fitz import open as fitz_open try: # Py2 and Py3 compatibility from urllib.request import urlretrieve except: @@ -18,7 +19,7 @@ from seaserv import get_file_id_by_path, get_repo, get_file_size, \ seafile_api from seahub.utils import gen_inner_file_get_url, get_file_type_and_ext -from seahub.utils.file_types import VIDEO, XMIND +from seahub.utils.file_types import VIDEO, XMIND, PDF from seahub.settings import THUMBNAIL_IMAGE_SIZE_LIMIT, \ THUMBNAIL_EXTENSION, THUMBNAIL_ROOT, THUMBNAIL_IMAGE_ORIGINAL_SIZE_LIMIT,\ ENABLE_VIDEO_THUMBNAIL, THUMBNAIL_VIDEO_FRAME_TIME @@ -117,7 +118,10 @@ def generate_thumbnail(request, repo_id, size, path): thumbnail_file, file_size) else: return (False, 400) - + if filetype == PDF: + # pdf thumbnails + return create_pdf_thumbnails(repo, file_id, path, size, + thumbnail_file, file_size) if filetype == XMIND: return extract_xmind_image(repo_id, path, size) @@ -181,6 +185,41 @@ def create_psd_thumbnails(repo, file_id, path, size, thumbnail_file, file_size): os.unlink(tmp_img_path) return (False, 500) + +def create_pdf_thumbnails(repo, file_id, path, size, thumbnail_file, file_size): + t1 = timeit.default_timer() + token = seafile_api.get_fileserver_access_token(repo.id, + file_id, 'view', '', use_onetime=False) + + if not token: + return (False, 500) + + inner_path = gen_inner_file_get_url(token, os.path.basename(path)) + tmp_path = str(os.path.join(tempfile.gettempdir(), '%s.png' % file_id[:8])) + pdf_file = urllib.request.urlopen(inner_path) + pdf_stream = BytesIO(pdf_file.read()) + try: + pdf_doc = fitz_open(stream=pdf_stream) + pdf_stream.close() + page = pdf_doc[0] + pix = page.get_pixmap() + pix.save(tmp_path) + pdf_doc.close() + except Exception as e: + logger.error(e) + return (False, 500) + t2 = timeit.default_timer() + logger.debug('Create PDF thumbnail of [%s](size: %s) takes: %s' % (path, file_size, (t2 - t1))) + + try: + ret = _create_thumbnail_common(tmp_path, thumbnail_file, size) + os.unlink(tmp_path) + return ret + except Exception as e: + logger.error(e) + os.unlink(tmp_path) + return (False, 500) + def create_video_thumbnails(repo, file_id, path, size, thumbnail_file, file_size): t1 = timeit.default_timer() diff --git a/seahub/views/__init__.py b/seahub/views/__init__.py index 2f7d1c6c0f..af89a386c8 100644 --- a/seahub/views/__init__.py +++ b/seahub/views/__init__.py @@ -1089,6 +1089,7 @@ def react_fake_view(request, **kwargs): 'ocm_remote_servers': OCM_REMOTE_SERVERS, 'enable_share_to_department': settings.ENABLE_SHARE_TO_DEPARTMENT, 'enable_video_thumbnail': settings.ENABLE_VIDEO_THUMBNAIL, + 'enable_pdf_thumbnail': settings.ENABLE_PDF_THUMBNAIL, 'group_import_members_extra_msg': GROUP_IMPORT_MEMBERS_EXTRA_MSG, 'request_from_onlyoffice_desktop_editor': ONLYOFFICE_DESKTOP_EDITOR_HTTP_USER_AGENT in request.headers.get('user-agent', ''), 'enable_sso_to_thirdpart_website': settings.ENABLE_SSO_TO_THIRDPART_WEBSITE, diff --git a/seahub/views/file.py b/seahub/views/file.py index c875c7bec7..8e82eab824 100644 --- a/seahub/views/file.py +++ b/seahub/views/file.py @@ -765,6 +765,8 @@ def view_lib_file(request, repo_id, path): send_file_access_msg(request, repo, path, 'web') if filetype == VIDEO: return_dict['enable_video_thumbnail'] = settings.ENABLE_VIDEO_THUMBNAIL + if filetype == PDF: + return_dict['enable_pdf_thumbnail'] = settings.ENABLE_PDF_THUMBNAIL return render(request, template, return_dict) elif filetype == XMIND: diff --git a/seahub/views/repo.py b/seahub/views/repo.py index 3c79992eb7..e808de3c96 100644 --- a/seahub/views/repo.py +++ b/seahub/views/repo.py @@ -31,7 +31,7 @@ from seahub.settings import ENABLE_UPLOAD_FOLDER, \ THUMBNAIL_ROOT, THUMBNAIL_DEFAULT_SIZE, THUMBNAIL_SIZE_FOR_GRID, \ MAX_NUMBER_OF_FILES_FOR_FILEUPLOAD, SHARE_LINK_EXPIRE_DAYS_MIN, \ SHARE_LINK_EXPIRE_DAYS_MAX, SEAFILE_COLLAB_SERVER, \ - ENABLE_SHARE_LINK_REPORT_ABUSE + ENABLE_SHARE_LINK_REPORT_ABUSE, ENABLE_PDF_THUMBNAIL from seahub.utils.file_types import IMAGE, VIDEO, XMIND from seahub.thumbnail.utils import get_share_link_thumbnail_src from seahub.group.utils import is_group_admin @@ -363,6 +363,7 @@ def view_shared_dir(request, fileshare): 'desc_for_ogp': desc_for_ogp, 'enable_share_link_report_abuse': ENABLE_SHARE_LINK_REPORT_ABUSE, 'enable_video_thumbnail': ENABLE_VIDEO_THUMBNAIL, + 'enable_pdf_thumbnail': ENABLE_PDF_THUMBNAIL, }) diff --git a/test-requirements.txt b/test-requirements.txt index fb31eab0a7..3ca8d6deab 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -7,3 +7,4 @@ splinter pytest==7.4.4 pytest-django selenium +PyMuPDF==1.24.3 \ No newline at end of file