diff --git a/lib/redmine/thumbnail.rb b/lib/redmine/thumbnail.rb index cd617487a..556680a5a 100644 --- a/lib/redmine/thumbnail.rb +++ b/lib/redmine/thumbnail.rb @@ -41,13 +41,15 @@ module Redmine # Make sure we only invoke Imagemagick if the file type is allowed mime_type = File.open(source) {|f| Marcel::MimeType.for(f)} return nil unless ALLOWED_TYPES.include? mime_type - return nil if mime_type == 'application/pdf' && !gs_available? directory = File.dirname(target) FileUtils.mkdir_p directory size_option = "#{size}x#{size}>" if mime_type == 'application/pdf' + return nil unless gs_available? + return nil unless valid_pdf_magic?(source) + cmd = "#{shell_quote CONVERT_BIN} #{shell_quote "#{source}[0]"} -thumbnail #{shell_quote size_option} #{shell_quote "png:#{target}"}" else cmd = "#{shell_quote CONVERT_BIN} #{shell_quote source} -auto-orient -thumbnail #{shell_quote size_option} #{shell_quote target}" @@ -98,6 +100,21 @@ module Redmine @gs_available end + # Check PDF magic bytes to make sure the file looks like a PDF, not + # PostScript. + # + # This method treats the file as PostScript instead of PDF and returns + # false if PostScript magic bytes appear before the PDF magic bytes. + # This behavior is based on the detection logic used by Ghostscript in + # the redefined `run` operator in pdf_main.ps. + def self.valid_pdf_magic?(filename) + head_data = File.binread(filename, 1024) + pdf_magic_pos = head_data.index('%PDF-') + ps_magic_pos = head_data.index('%!PS') + + !pdf_magic_pos.nil? && (ps_magic_pos.nil? || pdf_magic_pos < ps_magic_pos) + end + def self.logger Rails.logger end diff --git a/test/fixtures/files/hello.pdf b/test/fixtures/files/hello.pdf new file mode 100644 index 000000000..e114decd5 --- /dev/null +++ b/test/fixtures/files/hello.pdf @@ -0,0 +1,32 @@ +%PDF-2.0 +1 0 obj +<< /Type /Catalog /Pages 2 0 R >> +endobj +2 0 obj +<< /Type /Pages /Kids [3 0 R] /Count 1 >> +endobj +3 0 obj +<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >> +endobj +4 0 obj +<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >> +endobj +5 0 obj +<< /Length 43 >> +stream +BT /F1 24 Tf 100 700 Td (Hello World) Tj ET +endstream +endobj +xref +0 6 +0000000000 65535 f +0000000009 00000 n +0000000058 00000 n +0000000115 00000 n +0000000241 00000 n +0000000311 00000 n +trailer +<< /Size 6 /Root 1 0 R >> +startxref +404 +%%EOF \ No newline at end of file diff --git a/test/fixtures/files/hello.ps b/test/fixtures/files/hello.ps new file mode 100644 index 000000000..b4e6589c8 --- /dev/null +++ b/test/fixtures/files/hello.ps @@ -0,0 +1,7 @@ +%!PS +/Helvetica findfont +24 scalefont +setfont +100 700 moveto +(Hello World) show +showpage diff --git a/test/fixtures/files/with_pdf_magic.ps b/test/fixtures/files/with_pdf_magic.ps new file mode 100644 index 000000000..01b13cc64 --- /dev/null +++ b/test/fixtures/files/with_pdf_magic.ps @@ -0,0 +1,10 @@ + +%!PS +%PDF-2.0 +% ***** The above line looks like PDF magic bytes, but just a comment ***** +/Helvetica findfont +24 scalefont +setfont +100 700 moveto +(Hello World) show +showpage diff --git a/test/unit/lib/redmine/thumbnail_test.rb b/test/unit/lib/redmine/thumbnail_test.rb new file mode 100644 index 000000000..67fd72123 --- /dev/null +++ b/test/unit/lib/redmine/thumbnail_test.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +# Redmine - project management software +# Copyright (C) 2006- Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require_relative '../../../test_helper' + +class Redmine::ThumbnailTest < ActiveSupport::TestCase + def test_valid_pdf_magic_returns_false_when_postscript_magic_comes_first + # This PostScript file has a string '%PDF-' after '%!PS'. + # Marcel currently misclassifies it as application/pdf, so we use + # valid_pdf_magic? to avoid treating it as PDF. + # TODO: + # Consider removing `valid_pdf_magic?` once Marcel correctly + # returns application/postscript. + file = file_fixture('with_pdf_magic.ps') + assert_equal 'application/pdf', file.open {|f| Marcel::MimeType.for(f)} + assert_equal false, Redmine::Thumbnail.valid_pdf_magic?(file.to_s) + end + + def test_valid_pdf_magic_returns_false_for_postscript + file = file_fixture('hello.ps') + assert_equal 'application/postscript', file.open {|f| Marcel::MimeType.for(f)} + assert_equal false, Redmine::Thumbnail.valid_pdf_magic?(file.to_s) + end + + def test_valid_pdf_magic_returns_true_for_pdf + file = file_fixture('hello.pdf') + assert_equal 'application/pdf', file.open {|f| Marcel::MimeType.for(f)} + assert_equal true, Redmine::Thumbnail.valid_pdf_magic?(file.to_s) + end + + def test_thumbnail_returns_nil_for_postscript + skip unless Redmine::Thumbnail.convert_available? && Redmine::Thumbnail.gs_available? + + set_tmp_attachments_directory + file = file_fixture('with_pdf_magic.ps') + target = File.join(Attachment.storage_path, "#{SecureRandom.hex(8)}.thumb.png") + assert_nil Redmine::Thumbnail.generate(file.to_s, target, 100) + assert_not File.exist?(target) + ensure + FileUtils.rm_f(target) if target + end + + def test_thumbnail_returns_thumbnail_filename_for_pdf + skip unless Redmine::Thumbnail.convert_available? && Redmine::Thumbnail.gs_available? + + set_tmp_attachments_directory + file = file_fixture('hello.pdf') + target = File.join(Attachment.storage_path, "#{SecureRandom.hex(8)}.thumb.png") + result = Redmine::Thumbnail.generate(file.to_s, target, 100) + assert_equal target, result + assert File.exist?(target) + ensure + FileUtils.rm_f(target) if target + end +end