From 04fea711c8ed53ca42ec6ead206b920f9e43846a Mon Sep 17 00:00:00 2001 From: MAEDA Go Date: Fri, 28 Aug 2026 19:00:58 +0900 Subject: [PATCH 2/2] Reduce memory usage by not loading rbpdf until a PDF is generated. --- Gemfile | 3 +- config/initializers/zeitwerk.rb | 6 +- lib/redmine/export/pdf.rb | 121 ------------------------- lib/redmine/export/pdf/itcpdf.rb | 146 +++++++++++++++++++++++++++++++ 4 files changed, 153 insertions(+), 123 deletions(-) create mode 100644 lib/redmine/export/pdf/itcpdf.rb diff --git a/Gemfile b/Gemfile index cab7409c5..3a2478cfa 100644 --- a/Gemfile +++ b/Gemfile @@ -11,7 +11,8 @@ gem 'marcel' gem 'mail', '~> 2.9.0' gem 'nokogiri', '~> 1.19.1' gem 'i18n', '~> 1.15.2' -gem 'rbpdf', '~> 1.21.4' +# Loaded on demand by lib/redmine/export/pdf/itcpdf.rb, keep require: false +gem 'rbpdf', '~> 1.21.4', require: false gem 'addressable' gem 'rubyzip', '~> 3.5.0' gem 'propshaft', '~> 1.3.0' diff --git a/config/initializers/zeitwerk.rb b/config/initializers/zeitwerk.rb index c9f7286d6..6abbb9e61 100644 --- a/config/initializers/zeitwerk.rb +++ b/config/initializers/zeitwerk.rb @@ -21,9 +21,13 @@ Rails.autoloaders.each do |loader| 'pdf' => 'PDF', 'url' => 'URL', 'pop3' => 'POP3', - 'imap' => 'IMAP' + 'imap' => 'IMAP', + 'itcpdf' => 'ITCPDF' ) IGNORE_LIST.each do |mod| loader.ignore lib.join(mod) end + # Keep ITCPDF out of eager loading so that rbpdf is only loaded when a PDF + # is generated. + loader.do_not_eager_load lib.join('export/pdf/itcpdf.rb') end diff --git a/lib/redmine/export/pdf.rb b/lib/redmine/export/pdf.rb index f17dc437b..9f2dc2958 100644 --- a/lib/redmine/export/pdf.rb +++ b/lib/redmine/export/pdf.rb @@ -18,130 +18,9 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -require 'rbpdf' - module Redmine module Export module PDF - # The camel cased methods below follow the RBPDF API and cannot be renamed. - # rubocop:disable-next Naming/MethodName - class ITCPDF < RBPDF - include Redmine::I18n - attr_accessor :footer_date - - def initialize(lang, orientation='P') - set_language_if_valid lang - super(orientation, 'mm', 'A4') - set_print_header(false) - set_rtl(l(:direction) == 'rtl') - - @font_for_content = l(:general_pdf_fontname) - @monospaced_font_for_content = l(:general_pdf_monospaced_fontname) - @font_for_footer = l(:general_pdf_fontname) - set_creator(Redmine::Info.app_name) - set_font(@font_for_content) - - set_header_font([@font_for_content, '', 10]) - set_footer_font([@font_for_content, '', 8]) - set_default_monospaced_font(@monospaced_font_for_content) - set_display_mode('default', 'OneColumn') - end - - def SetFontStyle(style, size) - set_font(@font_for_content, style, size) - end - - def SetFont(family, style='', size=0, fontfile='') - style = +style - # FreeSerif Bold Thai font has problem. - style.delete!('B') if family.to_s.casecmp('freeserif') == 0 - # DejaVuSans Italic Arabic and Persian font has problem. - style.delete!('I') if family.to_s.casecmp('dejavusans') == 0 && current_language.to_s.casecmp("vi") != 0 - # DejaVuSansMono Italic Arabic font has problem - style.delete!('I') if family.to_s.casecmp('dejavusansmono') == 0 - super - end - alias set_font SetFont - - def fix_text_encoding(txt) - RDMPdfEncoding.rdm_from_utf8(txt, "UTF-8") - end - - def formatted_text(text) - Redmine::WikiFormatting.to_html(Setting.text_formatting, text) - end - - def RDMCell(w, h=0, txt='', border=0, ln=0, align='', fill=0, link='') - cell(w, h, txt, border, ln, align, fill, link) - end - - def RDMMultiCell(w, h=0, txt='', border=0, align='', fill=0, ln=1) - multi_cell(w, h, txt, border, align, fill, ln) - end - - def RDMwriteFormattedCell(w, h, x, y, txt='', attachments=[], border=0, ln=1, fill=0) - @attachments = attachments - - css_tag = ' ' - - # Strip {{toc}} tags - txt = txt.gsub(/

\{\{((<|<)|(>|>))?toc\}\}<\/p>/i, '') - writeHTMLCell(w, h, x, y, css_tag + txt, border, ln, fill) - end - - def RDMwriteHTMLCell(w, h, x, y, txt='', attachments=[], border=0, ln=1, fill=0) - txt = formatted_text(txt) - RDMwriteFormattedCell(w, h, x, y, txt, attachments, border, ln, fill) - end - - def get_image_filename(attrname) - atta = RDMPdfEncoding.attach(@attachments, attrname, "UTF-8") - if atta - return atta.diskfile - # rubocop:disable Lint/DuplicateBranch - elsif %r{/attachments/download/(?[^/]+)/} =~ attrname && - (atta = @attachments.find{|a| a.id.to_s == id}) && - atta.readable? && atta.visible? - return atta.diskfile - # rubocop:enable Lint/DuplicateBranch - elsif %r{/attachments/thumbnail/(?[^/]+)/(?\d+)} =~ attrname && - (atta = @attachments.find{|a| a.id.to_s == id}) && - atta.readable? && atta.visible? - return atta.thumbnail(size: size) - else - return nil - end - end - - def get_sever_url(url) - if !empty_string(url) && url.start_with?('/') - Setting.host_name.split('/')[0] + url - else - url - end - end - - def Footer - set_font(@font_for_footer, 'I', 8) - set_x(15) - if get_rtl - RDMCell(0, 5, @footer_date, 0, 0, 'R') - else - RDMCell(0, 5, @footer_date, 0, 0, 'L') - end - set_x(-30) - RDMCell(0, 5, get_alias_num_page + '/' + get_alias_nb_pages, 0, 0, 'C') - end - end - class RDMPdfEncoding def self.rdm_from_utf8(txt, encoding) txt ||= '' diff --git a/lib/redmine/export/pdf/itcpdf.rb b/lib/redmine/export/pdf/itcpdf.rb new file mode 100644 index 000000000..d7877f2d4 --- /dev/null +++ b/lib/redmine/export/pdf/itcpdf.rb @@ -0,0 +1,146 @@ +# 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 'rbpdf' + +module Redmine + module Export + module PDF + # The camel cased methods below follow the RBPDF API and cannot be renamed. + # rubocop:disable-next Naming/MethodName + class ITCPDF < RBPDF + include Redmine::I18n + attr_accessor :footer_date + + def initialize(lang, orientation='P') + set_language_if_valid lang + super(orientation, 'mm', 'A4') + set_print_header(false) + set_rtl(l(:direction) == 'rtl') + + @font_for_content = l(:general_pdf_fontname) + @monospaced_font_for_content = l(:general_pdf_monospaced_fontname) + @font_for_footer = l(:general_pdf_fontname) + set_creator(Redmine::Info.app_name) + set_font(@font_for_content) + + set_header_font([@font_for_content, '', 10]) + set_footer_font([@font_for_content, '', 8]) + set_default_monospaced_font(@monospaced_font_for_content) + set_display_mode('default', 'OneColumn') + end + + def SetFontStyle(style, size) + set_font(@font_for_content, style, size) + end + + def SetFont(family, style='', size=0, fontfile='') + style = +style + # FreeSerif Bold Thai font has problem. + style.delete!('B') if family.to_s.casecmp('freeserif') == 0 + # DejaVuSans Italic Arabic and Persian font has problem. + style.delete!('I') if family.to_s.casecmp('dejavusans') == 0 && current_language.to_s.casecmp("vi") != 0 + # DejaVuSansMono Italic Arabic font has problem + style.delete!('I') if family.to_s.casecmp('dejavusansmono') == 0 + super + end + alias set_font SetFont + + def fix_text_encoding(txt) + RDMPdfEncoding.rdm_from_utf8(txt, "UTF-8") + end + + def formatted_text(text) + Redmine::WikiFormatting.to_html(Setting.text_formatting, text) + end + + def RDMCell(w, h=0, txt='', border=0, ln=0, align='', fill=0, link='') + cell(w, h, txt, border, ln, align, fill, link) + end + + def RDMMultiCell(w, h=0, txt='', border=0, align='', fill=0, ln=1) + multi_cell(w, h, txt, border, align, fill, ln) + end + + def RDMwriteFormattedCell(w, h, x, y, txt='', attachments=[], border=0, ln=1, fill=0) + @attachments = attachments + + css_tag = ' ' + + # Strip {{toc}} tags + txt = txt.gsub(/

\{\{((<|<)|(>|>))?toc\}\}<\/p>/i, '') + writeHTMLCell(w, h, x, y, css_tag + txt, border, ln, fill) + end + + def RDMwriteHTMLCell(w, h, x, y, txt='', attachments=[], border=0, ln=1, fill=0) + txt = formatted_text(txt) + RDMwriteFormattedCell(w, h, x, y, txt, attachments, border, ln, fill) + end + + def get_image_filename(attrname) + atta = RDMPdfEncoding.attach(@attachments, attrname, "UTF-8") + if atta + return atta.diskfile + # rubocop:disable Lint/DuplicateBranch + elsif %r{/attachments/download/(?[^/]+)/} =~ attrname && + (atta = @attachments.find{|a| a.id.to_s == id}) && + atta.readable? && atta.visible? + return atta.diskfile + # rubocop:enable Lint/DuplicateBranch + elsif %r{/attachments/thumbnail/(?[^/]+)/(?\d+)} =~ attrname && + (atta = @attachments.find{|a| a.id.to_s == id}) && + atta.readable? && atta.visible? + return atta.thumbnail(size: size) + else + return nil + end + end + + def get_sever_url(url) + if !empty_string(url) && url.start_with?('/') + Setting.host_name.split('/')[0] + url + else + url + end + end + + def Footer + set_font(@font_for_footer, 'I', 8) + set_x(15) + if get_rtl + RDMCell(0, 5, @footer_date, 0, 0, 'R') + else + RDMCell(0, 5, @footer_date, 0, 0, 'L') + end + set_x(-30) + RDMCell(0, 5, get_alias_num_page + '/' + get_alias_nb_pages, 0, 0, 'C') + end + end + end + end +end -- 2.55.0