diff --git a/Gemfile b/Gemfile index 8fcf249a3..98c83bd76 100644 --- a/Gemfile +++ b/Gemfile @@ -32,8 +32,8 @@ end platforms :mri, :mingw, :x64_mingw do # Optional gem for exporting the gantt to a PNG file, not supported with jruby - group :rmagick do - gem "rmagick", ">= 2.14.0" + group :minimagick do + gem "mini_magick" end # Optional Markdown support, not for JRuby diff --git a/app/controllers/admin_controller.rb b/app/controllers/admin_controller.rb index 885c3a169..e46f52166 100644 --- a/app/controllers/admin_controller.rb +++ b/app/controllers/admin_controller.rb @@ -74,7 +74,7 @@ class AdminController < ApplicationController [:text_default_administrator_account_changed, User.default_admin_account_changed?], [:text_file_repository_writable, File.writable?(Attachment.storage_path)], ["#{l :text_plugin_assets_writable} (./public/plugin_assets)", File.writable?(Redmine::Plugin.public_directory)], - [:text_rmagick_available, Object.const_defined?(:Magick)], + [:text_minimagick_available, Object.const_defined?(:MiniMagick)], [:text_convert_available, Redmine::Thumbnail.convert_available?] ] end diff --git a/appveyor.yml b/appveyor.yml index d5ea687bb..c5818a60e 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -13,7 +13,7 @@ install: build: off test_script: - - bundle install --without rmagick + - bundle install --without minimagick - set SCMS=mercurial - set RUN_ON_NOT_OFFICIAL= - set RUBY_VER=1.9 diff --git a/config/configuration.yml.example b/config/configuration.yml.example index bff4c9740..b175492eb 100644 --- a/config/configuration.yml.example +++ b/config/configuration.yml.example @@ -179,10 +179,10 @@ default: # the ImageMagick's `convert` binary. Used to generate attachment thumbnails. #imagemagick_convert_command: - # Configuration of RMagick font. + # Configuration of MiniMagick font. # - # Redmine uses RMagick in order to export gantt png. - # You don't need this setting if you don't install RMagick. + # Redmine uses MiniMagick in order to export gantt png. + # You don't need this setting if you don't install MiniMagick. # # In CJK (Chinese, Japanese and Korean), # in order to show CJK characters correctly, @@ -195,11 +195,11 @@ default: # # Examples for Japanese: # Windows: - # rmagick_font_path: C:\windows\fonts\msgothic.ttc + # minimagick_font_path: C:\windows\fonts\msgothic.ttc # Linux: - # rmagick_font_path: /usr/share/fonts/ipa-mincho/ipam.ttf + # minimagick_font_path: /usr/share/fonts/ipa-mincho/ipam.ttf # - rmagick_font_path: + minimagick_font_path: # Maximum number of simultaneous AJAX uploads #max_concurrent_ajax_uploads: 2 diff --git a/config/locales/en.yml b/config/locales/en.yml index 74e03d34e..9904c3e5e 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1152,7 +1152,7 @@ en: text_default_administrator_account_changed: Default administrator account changed text_file_repository_writable: Attachments directory writable text_plugin_assets_writable: Plugin assets directory writable - text_rmagick_available: RMagick available (optional) + text_minimagick_available: MiniMagick available (optional) text_convert_available: ImageMagick convert available (optional) 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/lib/redmine.rb b/lib/redmine.rb index 9bf00f366..67c393133 100644 --- a/lib/redmine.rb +++ b/lib/redmine.rb @@ -18,9 +18,9 @@ require 'redmine/core_ext' begin - require 'rmagick' unless Object.const_defined?(:Magick) + require 'mini_magick' unless Object.const_defined?(:MiniMagick) rescue LoadError - # RMagick is not available + # MiniMagick is not available end begin require 'redcarpet' unless Object.const_defined?(:Redcarpet) diff --git a/lib/redmine/helpers/gantt.rb b/lib/redmine/helpers/gantt.rb index 312a3133a..8a49cb81b 100644 --- a/lib/redmine/helpers/gantt.rb +++ b/lib/redmine/helpers/gantt.rb @@ -335,7 +335,7 @@ module Redmine end # Generates a gantt image - # Only defined if RMagick is avalaible + # Only defined if MiniMagick is avalaible def to_image(format='PNG') date_to = (@date_from >> @months) - 1 show_weeks = @zoom > 1 @@ -348,98 +348,122 @@ module Redmine g_height = 20 * number_of_rows + 30 headers_height = (show_weeks ? 2 * header_height : header_height) height = g_height + headers_height - imgl = Magick::ImageList.new - imgl.new_image(subject_width + g_width + 1, height) - gc = Magick::Draw.new - gc.font = Redmine::Configuration['rmagick_font_path'] || "" - # Subjects - gc.stroke('transparent') - subjects(:image => gc, :top => (headers_height + 20), :indent => 4, :format => :image) - # Months headers - month_f = @date_from - left = subject_width - @months.times do - width = ((month_f >> 1) - month_f) * zoom - gc.fill('white') - gc.stroke('grey') - gc.stroke_width(1) - gc.rectangle(left, 0, left + width, height) - gc.fill('black') + Rails.logger.warn('rmagick_font_path option is deprecated. use minimagick_font_path instead.') \ + unless Redmine::Configuration['rmagick_font_path'].nil? + font_path = Redmine::Configuration['minimagick_font_path'].presence || Redmine::Configuration['rmagick_font_path'].presence + img = MiniMagick::Image.create(".#{format}", false) + MiniMagick::Tool::Convert.new do |gc| + gc.size('%dx%d' % [subject_width + g_width + 1, height]) + gc.xc('white') + gc.font(font_path) if font_path.present? + # Subjects gc.stroke('transparent') - gc.stroke_width(1) - gc.text(left.round + 8, 14, "#{month_f.year}-#{month_f.month}") - left = left + width - month_f = month_f >> 1 - end - # Weeks headers - if show_weeks + subjects(:image => gc, :top => (headers_height + 20), :indent => 4, :format => :image) + # Months headers + month_f = @date_from left = subject_width - height = header_height - if @date_from.cwday == 1 - # date_from is monday - week_f = date_from - else - # find next monday after date_from - week_f = @date_from + (7 - @date_from.cwday + 1) - width = (7 - @date_from.cwday + 1) * zoom - gc.fill('white') - gc.stroke('grey') - gc.stroke_width(1) - gc.rectangle(left, header_height, left + width, 2 * header_height + g_height - 1) - left = left + width - end - while week_f <= date_to - width = (week_f + 6 <= date_to) ? 7 * zoom : (date_to - week_f + 1) * zoom + @months.times do + width = ((month_f >> 1) - month_f) * zoom gc.fill('white') gc.stroke('grey') - gc.stroke_width(1) - gc.rectangle(left.round, header_height, left.round + width, 2 * header_height + g_height - 1) + gc.strokewidth(1) + gc.draw('rectangle %d,%d %d,%d' % [ + left, 0, left + width, height + ]) gc.fill('black') gc.stroke('transparent') - gc.stroke_width(1) - gc.text(left.round + 2, header_height + 14, week_f.cweek.to_s) + gc.strokewidth(1) + gc.draw('text %d,%d %s' % [ + left.round + 8, 14, Redmine::Utils::Shell::shell_quote("#{month_f.year}-#{month_f.month}") + ]) left = left + width - week_f = week_f + 7 + month_f = month_f >> 1 + end + # Weeks headers + if show_weeks + left = subject_width + height = header_height + if @date_from.cwday == 1 + # date_from is monday + week_f = date_from + else + # find next monday after date_from + week_f = @date_from + (7 - @date_from.cwday + 1) + width = (7 - @date_from.cwday + 1) * zoom + gc.fill('white') + gc.stroke('grey') + gc.strokewidth(1) + gc.draw('rectangle %d,%d %d,%d' % [ + left, header_height, left + width, 2 * header_height + g_height - 1 + ]) + left = left + width + end + while week_f <= date_to + width = (week_f + 6 <= date_to) ? 7 * zoom : (date_to - week_f + 1) * zoom + gc.fill('white') + gc.stroke('grey') + gc.strokewidth(1) + gc.draw('rectangle %d,%d %d,%d' % [ + left.round, header_height, left.round + width, 2 * header_height + g_height - 1 + ]) + gc.fill('black') + gc.stroke('transparent') + gc.strokewidth(1) + gc.draw('text %d,%d %s' % [ + left.round + 2, header_height + 14, Redmine::Utils::Shell::shell_quote(week_f.cweek.to_s) + ]) + left = left + width + week_f = week_f + 7 + end end - end - # Days details (week-end in grey) - if show_days - left = subject_width - height = g_height + header_height - 1 - wday = @date_from.cwday - (date_to - @date_from + 1).to_i.times do - width = zoom - gc.fill(non_working_week_days.include?(wday) ? '#eee' : 'white') - gc.stroke('#ddd') - gc.stroke_width(1) - gc.rectangle(left, 2 * header_height, left + width, 2 * header_height + g_height - 1) - left = left + width - wday = wday + 1 - wday = 1 if wday > 7 + # Days details (week-end in grey) + if show_days + left = subject_width + height = g_height + header_height - 1 + wday = @date_from.cwday + (date_to - @date_from + 1).to_i.times do + width = zoom + gc.fill(non_working_week_days.include?(wday) ? '#eee' : 'white') + gc.stroke('#ddd') + gc.strokewidth(1) + gc.draw('rectangle %d,%d %d,%d' % [ + left, 2 * header_height, left + width, 2 * header_height + g_height - 1 + ]) + left = left + width + wday = wday + 1 + wday = 1 if wday > 7 + end end - end - # border - gc.fill('transparent') - gc.stroke('grey') - gc.stroke_width(1) - gc.rectangle(0, 0, subject_width + g_width, headers_height) - gc.stroke('black') - gc.rectangle(0, 0, subject_width + g_width, g_height + headers_height - 1) - # content - top = headers_height + 20 - gc.stroke('transparent') - lines(:image => gc, :top => top, :zoom => zoom, - :subject_width => subject_width, :format => :image) - # today red line - if User.current.today >= @date_from and User.current.today <= date_to - gc.stroke('red') - x = (User.current.today - @date_from + 1) * zoom + subject_width - gc.line(x, headers_height, x, headers_height + g_height - 1) - end - gc.draw(imgl) - imgl.format = format - imgl.to_blob - end if Object.const_defined?(:Magick) + # border + gc.fill('transparent') + gc.stroke('grey') + gc.strokewidth(1) + gc.draw('rectangle %d,%d %d,%d' % [ + 0, 0, subject_width + g_width, headers_height + ]) + gc.stroke('black') + gc.draw('rectangle %d,%d %d,%d' % [ + 0, 0, subject_width + g_width, g_height + headers_height - 1 + ]) + # content + top = headers_height + 20 + gc.stroke('transparent') + lines(:image => gc, :top => top, :zoom => zoom, + :subject_width => subject_width, :format => :image) + # today red line + if User.current.today >= @date_from and User.current.today <= date_to + gc.stroke('red') + x = (User.current.today - @date_from + 1) * zoom + subject_width + gc.draw('line %g,%g %g,%g' % [ + x, headers_height, x, headers_height + g_height - 1 + ]) + end + gc << img.path + end + img.to_blob + ensure + img.destroy! if img + end if Object.const_defined?(:MiniMagick) def to_pdf pdf = ::Redmine::Export::PDF::ITCPDF.new(current_language) @@ -735,8 +759,10 @@ module Redmine def image_subject(params, subject, options={}) params[:image].fill('black') params[:image].stroke('transparent') - params[:image].stroke_width(1) - params[:image].text(params[:indent], params[:top] + 2, subject) + params[:image].strokewidth(1) + params[:image].draw('text %d,%d %s' % [ + params[:indent], params[:top] + 2, Redmine::Utils::Shell::shell_quote(subject) + ]) end def issue_relations(issue) @@ -912,23 +938,29 @@ module Redmine # Renders the task bar, with progress and late if coords[:bar_start] && coords[:bar_end] params[:image].fill('#aaa') - params[:image].rectangle(params[:subject_width] + coords[:bar_start], - params[:top], - params[:subject_width] + coords[:bar_end], - params[:top] - height) + params[:image].draw('rectangle %d,%d %d,%d' % [ + params[:subject_width] + coords[:bar_start], + params[:top], + params[:subject_width] + coords[:bar_end], + params[:top] - height + ]) if coords[:bar_late_end] params[:image].fill('#f66') - params[:image].rectangle(params[:subject_width] + coords[:bar_start], - params[:top], - params[:subject_width] + coords[:bar_late_end], - params[:top] - height) + params[:image].draw('rectangle %d,%d %d,%d' % [ + params[:subject_width] + coords[:bar_start], + params[:top], + params[:subject_width] + coords[:bar_late_end], + params[:top] - height + ]) end if coords[:bar_progress_end] params[:image].fill('#00c600') - params[:image].rectangle(params[:subject_width] + coords[:bar_start], - params[:top], - params[:subject_width] + coords[:bar_progress_end], - params[:top] - height) + params[:image].draw('rectangle %d,%d %d,%d' % [ + params[:subject_width] + coords[:bar_start], + params[:top], + params[:subject_width] + coords[:bar_progress_end], + params[:top] - height + ]) end end # Renders the markers @@ -937,21 +969,31 @@ module Redmine x = params[:subject_width] + coords[:start] y = params[:top] - height / 2 params[:image].fill('blue') - params[:image].polygon(x - 4, y, x, y - 4, x + 4, y, x, y + 4) + params[:image].draw('polygon %d,%d %d,%d %d,%d %d,%d' % [ + x - 4, y, + x, y - 4, + x + 4, y, + x, y + 4 + ]) end if coords[:end] x = params[:subject_width] + coords[:end] + params[:zoom] y = params[:top] - height / 2 params[:image].fill('blue') - params[:image].polygon(x - 4, y, x, y - 4, x + 4, y, x, y + 4) + params[:image].draw('polygon %d,%d %d,%d %d,%d %d,%d' % [ + x - 4, y, + x, y - 4, + x + 4, y, + x, y + 4 + ]) end end # Renders the label on the right if label params[:image].fill('black') - params[:image].text(params[:subject_width] + (coords[:bar_end] || 0) + 5, - params[:top] + 1, - label) + params[:image].draw('text %d,%d %s' % [ + params[:subject_width] + (coords[:bar_end] || 0) + 5, params[:top] + 1, Redmine::Utils::Shell::shell_quote(label) + ]) end end end diff --git a/test/functional/gantts_controller_test.rb b/test/functional/gantts_controller_test.rb index cf11f9d66..67742a770 100644 --- a/test/functional/gantts_controller_test.rb +++ b/test/functional/gantts_controller_test.rb @@ -149,7 +149,7 @@ class GanttsControllerTest < Redmine::ControllerTest assert @response.body.starts_with?('%PDF') end - if Object.const_defined?(:Magick) + if Object.const_defined?(:MiniMagick) def test_gantt_should_export_to_png get :show, :params => { :project_id => 1,