From 1ca085f9c2d95faff0a42d9e46346cf065aaacb2 Mon Sep 17 00:00:00 2001 From: MAEDA Go Date: Thu, 26 Feb 2026 09:51:02 +0900 Subject: [PATCH] Add Microsoft Office and LibreOffice documents preview via MarkItDown Markdown converter --- app/controllers/admin_controller.rb | 3 +- app/controllers/attachments_controller.rb | 2 + app/models/attachment.rb | 28 ++++++ app/views/attachments/markdownized.html.erb | 4 + config/configuration.yml.example | 6 ++ config/locales/en.yml | 1 + config/locales/ja.yml | 1 + doc/INSTALL | 1 + lib/redmine/markdownizer.rb | 90 ++++++++++++++++++ test/fixtures/files/libreoffice-writer.odt | Bin 0 -> 19163 bytes test/fixtures/files/msword.docx | Bin 0 -> 6786 bytes .../functional/attachments_controller_test.rb | 44 +++++++++ test/unit/attachment_test.rb | 60 ++++++++++++ 13 files changed, 239 insertions(+), 1 deletion(-) create mode 100644 app/views/attachments/markdownized.html.erb create mode 100644 lib/redmine/markdownizer.rb create mode 100644 test/fixtures/files/libreoffice-writer.odt create mode 100644 test/fixtures/files/msword.docx diff --git a/app/controllers/admin_controller.rb b/app/controllers/admin_controller.rb index 9b45f9553..59e519edf 100644 --- a/app/controllers/admin_controller.rb +++ b/app/controllers/admin_controller.rb @@ -79,7 +79,8 @@ class AdminController < ApplicationController [:text_all_migrations_have_been_run, !ActiveRecord::Base.connection.pool.migration_context.needs_migration?], [:text_minimagick_available, Object.const_defined?(:MiniMagick)], [:text_convert_available, Redmine::Thumbnail.convert_available?], - [:text_gs_available, Redmine::Thumbnail.gs_available?] + [:text_gs_available, Redmine::Thumbnail.gs_available?], + [:text_pandoc_available, Redmine::Markdownizer.available?] ] @checklist << [:text_default_active_job_queue_changed, Rails.application.config.active_job.queue_adapter != :async] if Rails.env.production? end diff --git a/app/controllers/attachments_controller.rb b/app/controllers/attachments_controller.rb index 18795891d..c4c9e0c16 100644 --- a/app/controllers/attachments_controller.rb +++ b/app/controllers/attachments_controller.rb @@ -63,6 +63,8 @@ class AttachmentsController < ApplicationController render :action => 'image' elsif @attachment.is_pdf? render :action => 'pdf' + elsif @content = @attachment.markdownized_preview_content + render :action => 'markdownized' else render :action => 'other' end diff --git a/app/models/attachment.rb b/app/models/attachment.rb index d0dd3a71f..de23ac51d 100644 --- a/app/models/attachment.rb +++ b/app/models/attachment.rb @@ -82,6 +82,8 @@ class Attachment < ApplicationRecord cattr_accessor :thumbnails_storage_path @@thumbnails_storage_path = File.join(Rails.root, "tmp", "thumbnails") + cattr_accessor :markdownized_previews_storage_path + @@markdownized_previews_storage_path = File.join(Rails.root, "tmp", "markdownized_previews") before_create :files_to_final_location after_commit :delete_from_disk, :on => :destroy @@ -299,6 +301,31 @@ class Attachment < ApplicationRecord Redmine::MimeType.is_type?('audio', filename) end + def markdownized_previewable? + readable? && Redmine::Markdownizer.available? && Redmine::Markdownizer.supports?(filename) + end + + def markdownized_preview_content + return nil unless markdownized_previewable? + + target = markdownized_preview_cache_path + if Redmine::Markdownizer.convert(diskfile, target) + File.read(target, :mode => "rb") + end + rescue => e + if logger + logger.error( + "An error occured while generating markdownized preview for #{disk_filename} " \ + "to #{target}\nException was: #{e.message}" + ) + end + nil + end + + def markdownized_preview_cache_path + File.join(self.class.markdownized_previews_storage_path, "#{digest}_#{filesize}.md") + end + def previewable? is_text? || is_image? || is_video? || is_audio? end @@ -530,6 +557,7 @@ class Attachment < ApplicationRecord Dir[thumbnail_path("*")].each do |thumb| File.delete(thumb) end + FileUtils.rm_f(markdownized_preview_cache_path) end def thumbnail_path(size) diff --git a/app/views/attachments/markdownized.html.erb b/app/views/attachments/markdownized.html.erb new file mode 100644 index 000000000..3a158e827 --- /dev/null +++ b/app/views/attachments/markdownized.html.erb @@ -0,0 +1,4 @@ +<%= render :layout => 'layouts/file' do %> + <%= render :partial => 'common/markup', + :locals => {:markup_text_formatting => 'common_mark', :markup_text => @content} %> +<% end %> diff --git a/config/configuration.yml.example b/config/configuration.yml.example index e37f38745..cdfd516b5 100644 --- a/config/configuration.yml.example +++ b/config/configuration.yml.example @@ -237,6 +237,12 @@ default: # - example.org # - "*.example.com" + # Preview for Office documents (Microsoft Office / LibreOffice) + # + # Absolute path (e.g. /usr/local/bin/pandoc) to the Pandoc command + # used to convert supported attachments to Markdown for preview. + #pandoc_command: + # specific configuration options for production environment # that overrides the default ones production: diff --git a/config/locales/en.yml b/config/locales/en.yml index 17dfdfb6d..1ea7b4170 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1335,6 +1335,7 @@ en: text_minimagick_available: MiniMagick available (optional) text_convert_available: ImageMagick convert available (optional) text_gs_available: ImageMagick PDF support available (optional) + text_pandoc_available: Pandoc available (optional) text_default_active_job_queue_changed: Default queue adapter which is well suited only for dev/test changed text_destroy_time_entries_question: "%{hours} hours were reported on the issues you are about to delete. What do you want to do?" text_destroy_time_entries: Delete reported hours diff --git a/config/locales/ja.yml b/config/locales/ja.yml index 53bbc362f..6749d58e5 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -1103,6 +1103,7 @@ ja: notice_new_password_must_be_different: 新しいパスワードは現在のパスワードと異なるものでなければなりません setting_mail_handler_excluded_filenames: 除外する添付ファイル名 text_convert_available: ImageMagickのconvertコマンドが利用可能 (オプション) + text_pandoc_available: Pandocが利用可能 (オプション) label_link: リンク label_only: 次のもののみ label_drop_down_list: ドロップダウンリスト diff --git a/doc/INSTALL b/doc/INSTALL index 6d70f683a..d0d72f040 100644 --- a/doc/INSTALL +++ b/doc/INSTALL @@ -19,6 +19,7 @@ Optional: * SCM binaries (e.g. svn, git...), for repository browsing (must be available in PATH) * ImageMagick (to enable Gantt export to png images) +* Pandoc (to enable preview for Microsoft Office / LibreOffice documents) Supported browsers: The current version of Firefox, Safari, Chrome, Chromium and Microsoft Edge. diff --git a/lib/redmine/markdownizer.rb b/lib/redmine/markdownizer.rb new file mode 100644 index 000000000..898ef4a9b --- /dev/null +++ b/lib/redmine/markdownizer.rb @@ -0,0 +1,90 @@ +# 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 'fileutils' +require 'tempfile' +require 'timeout' + +module Redmine + module Markdownizer + extend Redmine::Utils::Shell + + COMMAND = (Redmine::Configuration['pandoc_command'] || 'pandoc').freeze + MAX_PREVIEW_SIZE = 100.kilobytes + SUPPORTED_EXTENSIONS = %w(.doc .docx .xls .xlsx .ppt .pptx .odt .ods .odp).freeze + + def self.supports?(filename) + SUPPORTED_EXTENSIONS.include?(File.extname(filename.to_s).downcase) + end + + def self.convert(source, target) + return nil unless available? + return target if File.exist?(target) + + directory = File.dirname(target) + FileUtils.mkdir_p(directory) + cmd = "#{shell_quote COMMAND} #{shell_quote source} -t gfm" + pid = nil + output = Tempfile.new('markdownized-preview') + + begin + Timeout.timeout(Redmine::Configuration['thumbnails_generation_timeout'].to_i) do + pid = Process.spawn(cmd, out: output.path) + _, status = Process.wait2(pid) + unless status.success? + logger.error("Markdown conversion failed (#{status.exitstatus}):\nCommand: #{cmd}") + return nil + end + end + rescue Timeout::Error + Process.kill('KILL', pid) if pid + logger.error("Markdown conversion timed out:\nCommand: #{cmd}") + return nil + rescue => e + logger.error("Markdown conversion failed:\nCommand: #{cmd}\nException was: #{e.message}") + return nil + ensure + output.close + end + + preview = File.binread(output.path, MAX_PREVIEW_SIZE + 1) || +"" + File.binwrite(target, preview.byteslice(0, MAX_PREVIEW_SIZE)) + target + ensure + output&.unlink + end + + def self.available? + return @available if defined?(@available) + + begin + `#{shell_quote COMMAND} --version` + @available = $?.success? + rescue + @available = false + end + logger.warn("Markdown converter command (#{COMMAND}) not available") unless @available + @available + end + + def self.logger + Rails.logger + end + end +end diff --git a/test/fixtures/files/libreoffice-writer.odt b/test/fixtures/files/libreoffice-writer.odt new file mode 100644 index 0000000000000000000000000000000000000000..32ab500ffcf80f72f35e70cc8a9f942a7450177c GIT binary patch literal 19163 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 literal 0 HcmV?d00001 diff --git a/test/fixtures/files/msword.docx b/test/fixtures/files/msword.docx new file mode 100644 index 0000000000000000000000000000000000000000..2b438db969080ac54584eb280b8584e0f8254190 GIT binary patch literal 6786 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 ph5t)+`xF28W 'No preview available' end + def test_show_msword + skip unless Redmine::Markdownizer.available? + + set_tmp_attachments_directory + a = Attachment.new( + :container => Issue.find(1), + :file => uploaded_test_file( + 'msword.docx', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' + ), + :author => User.find(1) + ) + assert a.save + + get(:show, :params => {:id => a.id}) + + assert_response :success + assert_equal 'text/html', @response.media_type + assert_select 'div.filecontent.wiki', :text => /Redmine is a flexible project management web application/ + assert_select '.nodata', :count => 0 + end + + def test_show_libreoffice_writer + skip unless Redmine::Markdownizer.available? + + set_tmp_attachments_directory + a = Attachment.new( + :container => Issue.find(1), + :file => uploaded_test_file( + 'libreoffice-writer.odt', + 'application/vnd.oasis.opendocument.text' + ), + :author => User.find(1) + ) + assert a.save + + get(:show, :params => {:id => a.id}) + + assert_response :success + assert_equal 'text/html', @response.media_type + assert_select 'div.filecontent.wiki', :text => /Redmine is a flexible project management web application/ + assert_select '.nodata', :count => 0 + end + def test_show_other_with_no_preview @request.session[:user_id] = 2 get(:show, :params => {:id => 6}) diff --git a/test/unit/attachment_test.rb b/test/unit/attachment_test.rb index 7bfa9311f..249dd6681 100644 --- a/test/unit/attachment_test.rb +++ b/test/unit/attachment_test.rb @@ -528,6 +528,66 @@ class AttachmentTest < ActiveSupport::TestCase assert_equal false, Attachment.new(:filename => 'test.txt').thumbnailable? end + def test_markdownized_previewable_should_be_true_for_supported_extensions + skip unless Redmine::Markdownizer.available? + + attachment = Attachment.new( + :container => Issue.find(1), + :file => uploaded_test_file( + "msword.docx", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document" + ), + :author => User.find(1) + ) + assert attachment.save + assert_equal true, attachment.markdownized_previewable? + end + + def test_markdownized_previewable_should_be_true_for_supported_libreoffice_extensions + skip unless Redmine::Markdownizer.available? + + attachment = Attachment.new( + :container => Issue.find(1), + :file => uploaded_test_file( + "libreoffice-writer.odt", + "application/vnd.oasis.opendocument.text" + ), + :author => User.find(1) + ) + assert attachment.save + assert_equal true, attachment.markdownized_previewable? + end + + def test_markdownized_previewable_should_be_false_for_non_supported_extensions + skip unless Redmine::Markdownizer.available? + + attachment = Attachment.new( + :container => Issue.find(1), + :file => uploaded_test_file("testfile.txt", "text/plain"), + :author => User.find(1) + ) + assert attachment.save + assert_equal false, attachment.markdownized_previewable? + end + + def test_delete_from_disk_should_delete_markdownized_preview_cache + attachment = Attachment.create!( + :container => Issue.find(1), + :file => uploaded_test_file( + "msword.docx", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document" + ), + :author => User.find(1) + ) + preview = attachment.markdownized_preview_cache_path + FileUtils.mkdir_p(File.dirname(preview)) + File.write(preview, "preview") + assert File.exist?(preview) + + attachment.send(:delete_from_disk!) + assert_not File.exist?(preview) + end + if convert_installed? def test_thumbnail_should_generate_the_thumbnail set_fixtures_attachments_directory -- 2.50.1