原田です。今回はお客様のお問い合わせから当社サービスが改善できたことについて書きます。
My Redmineではお客様からwebサポート窓口よりご契約に関することやMy Redmineの利用方法など多岐にわたってお問い合わせを頂いております。お問い合わせの内容によってはサポート担当からRedmine/RedMicaの内部コードを調べる依頼を受けることもあります。
先日お客様より「チケットをPDF形式でエクスポートしましたが、チケットに埋め込んでいる画像(インライン画像)がPDFに表示されません。」というお問い合わせを頂きました。
通常はサポート担当がお問い合わせ内容から現象を確認します。サポート担当には数多くのお問い合わせに対する知識(やり取り)が蓄積されておりそれらを参考にしてお客様にご回答いたします。ただし前例の無いお問い合わせについては社内の見識者に調査を依頼します。今回のお問い合わせも前例が無かったため私に調査依頼がありました。
サポート担当からは上記お問い合わせ内容と以下の情報を提供して頂きました。
My Redmineで提供しているソフトウェアは「RedMica」、現在Gen.1とGen.2でお客様にご提供しているバージョンはRedMica 1.1(Redmine4.1をベースにしRedmine4.2で追加予定の新機能を加えたもの) です。よってインライン画像はPDF内に表示できます。気になったのは上記2・3のGen.2とGen.1で挙動が異なる点でした。
My Redmineは従来サービスの「Gen.1」と現行サービスの「Gen.2」 の2つを提供しています。Gen.1とGen.2の違いはGen.2(現行)とGen.1(従来)のサービス内容の違いに記載しております。この違いの一つにストレージ容量を上げていますが、Gen.2はマルチテナントで構成されておりストレージにはAmazon S3(以下S3と称します)を使用し添付ファイルを管理しております(参考資料: My Redmine Gen.2を支えるインフラストラクチャー)。
Visual Studio Codeを用いてデバッグを実施したところS3への添付ファイルの読み書きは適切に動作していますが、PDF形式でエクスポートした時のみS3から該当の添付ファイル(画像ファイル)を読み込んでいないことが分かりました。
ただしPDFエクスポート時に添付ファイルを読み込んでいるのはRedMica本体やS3上のファイルへの読み書きを行っているredmica_s3プラグインではなく、PDFを作成しているGemライブラリrbpdfでした。rbpdfは自サーバー上に存在する画像ファイルかHTTP-GETリクエストでアクセス可能な画像ファイルのみを読み込んでいます。ちなみにredmica_s3プラグインではセキュリティ上の理由からS3へのアクセスはREST API以外を禁止しております。
原因がはっきりしましたので、これを解決するために以下2つのいずれかを実施すべきと判断しました。
最終的には2つとも実施しました。まずredmica_s3プラグインにプルリクエストを投稿し、その後rbpdfにもプルリクエストを提案として投稿しました。これはrbpdfの管理者は当社ではないためプルリクエストのパッチがいつリリースされるか不明であること、そして改善すべき処理の一部はredmica_s3にも追加する必要があるためです。さらにredmica_s3プラグインの管理は当社が行っているのでプルリクエストのパッチをリリースする時期はある程度調整することが可能です。
今回はredmica_s3プラグインにrbpdf側で見直すべき処理を一時的に当プラグインに追加した上で、添付ファイルをインライン画像として扱うためS3オブジェクトの生データを取得するよう改善しました。
diff --git a/lib/redmica_s3/pdf_patch.rb b/lib/redmica_s3/pdf_patch.rb new file mode 100644 index 0000000..47a0693 --- /dev/null +++ b/lib/redmica_s3/pdf_patch.rb @@ -0,0 +1,180 @@ +require 'open-uri' + +module RedmicaS3 + module PdfPatch + extend ActiveSupport::Concern + + included do + prepend PrependMethods + end + + class_methods do + end + + module PrependMethods + def self.prepended(base) + class << base + self.prepend(ClassMethods) + end + end + + module ClassMethods + end + + def get_image_filename(attrname) + Redmine::Export::PDF::RDMPdfEncoding.attach(@attachments, attrname, 'UTF-8') + end + + protected + + def openHTMLTagHandler(dom, key, cell) + tag = dom[key] + unless tag['value'] == 'img' + return super + end + + if !tag['attribute']['src'].nil? + tag['attribute']['src'].gsub!(/%([0-9a-fA-F]{2})/){$1.hex.chr} + + img_name = tag['attribute']['src'] + tag['attribute']['src'] = get_image_filename(img_name) + + tag['width'] ||= 0 + tag['height'] ||= 0 + tag['attribute']['align'] = 'bottom' + align = 'B' + + prevy = @y + xpos = @x + xpos += + # eliminate marker spaces + if !dom[key - 1].nil? + if (dom[key - 1]['value'] == ' ') or !dom[key - 1]['trimmed_space'].nil? + -GetStringWidth(32.chr) + elsif @rtl and (dom[key - 1]['value'] == ' ') + 2 * GetStringWidth(32.chr) + else + 0 + end + else + 0 + end + + imglink = '' + if !@href['url'].nil? and !empty_string(@href['url']) + imglink = @href['url'] + if imglink[0, 1] == '#' + # convert url to internal link + page = imglink.sub(/^#/, "").to_i + imglink = AddLink() + SetLink(imglink, 0, page) + end + end + border = + if !tag['attribute']['border'].nil? and !tag['attribute']['border'].empty? + # currently only support 1 (frame) or a combination of 'LTRB' + case tag['attribute']['border'] + when '0' + 0 + when '1' + 1 + else + tag['attribute']['border'] + end + else + 0 + end + + iw = tag['width'] ? getHTMLUnitToUnits(tag['width'], 1, 'px', false) : 0 + ih = tag['height'] ? getHTMLUnitToUnits(tag['height'], 1, 'px', false) : 0 + + # store original margin values + l_margin = @l_margin + r_margin = @r_margin + + SetLeftMargin(@l_margin + @c_margin) + SetRightMargin(@r_margin + @c_margin) + + result_img = + proc_image_file(tag['attribute']['src']) do |img_file| + Image(img_file, xpos, @y, iw, ih, '', imglink, align, false, 300, '', false, false, border, false, false, true) + end + + @y = + if result_img or ih != 0 + case align + when 'T' + prevy + when 'M' + (@img_rb_y + prevy - (tag['fontsize'] / @k)) / 2 + when 'B' + @img_rb_y - (tag['fontsize'] / @k) + else + prevy + end + else + prevy + end + + # restore original margin values + SetLeftMargin(l_margin) + SetRightMargin(r_margin) + + if result_img == false && !img_name.nil? + Write(@lasth, File::basename(img_name) + ' ', '', false, '', false, 0, false) + end + end + + if dom[key]['self'] and dom[key]['attribute']['pagebreakafter'] + pba = dom[key]['attribute']['pagebreakafter'] + # check for pagebreak + if (pba == 'true') or (pba == 'left') or (pba == 'right') + # add a page (or trig AcceptPageBreak() for multicolumn mode) + checkPageBreak(@page_break_trigger + 1) + end + if ((pba == 'left') and ((!@rtl and (@page % 2 == 0)) or (@rtl and (@page % 2 != 0)))) or ((pba == 'right') and ((!@rtl and (@page % 2 != 0)) or (@rtl and (@page % 2 == 0)))) + # add a page (or trig AcceptPageBreak() for multicolumn mode) + checkPageBreak(@page_break_trigger + 1) + end + end + dom + end + + end + + def get_image_file(image_uri) + #use a temporary file.... + tmpFile = Tempfile.new(['tmp_', '.img'], self.class.k_path_cache) + tmpFile.binmode + if image_uri.is_a?(Attachment) + tmpFile.write(image_uri.raw_data) + else + open(image_uri, 'rb') do |read_file| + tmpFile.write(read_file.read) + end + end + tmpFile.fsync + tmpFile + end + + private + + def proc_image_file(src, &block) + tmpFile = nil + img_file = + if src.is_a?(Attachment) || /^http/.match?(src) + tmpFile = get_image_file(src) + tmpFile.path + else + src + end + yield img_file + rescue => err + logger.error "pdf: Image: error: #{err.message}" + false + ensure + # remove temp files + tmpFile.close(true) if tmpFile + end + end +end
redmica_s3プラグインへのプルリクエストはすでにリリースされておりお客様にも動作をご確認頂いております。お客様からのご報告→サポート担当の調査→当方の(開発者による)機能改善という流れが出来ていることでスムーズに問題に対処できたと思います。
不具合は本来あってはならないですけど、どうしても人の手から作り出されるものなので開発者が気付かない不具合が存在する可能性はあり得ます。今後もお客様から今回のようなご報告を頂いた際は、引き続き関係者各位と連携し当社サービスを改善していきます。
![]() |
My Redmine Gen.2をリリース!クラウドへの対応を進めより良いサービスになりました。 |
![]() |
想像していた以上に簡単に室温管理・分析のための仕組みを構築することができました。 |
![]() |
Googleの提供するサービスの活用方法を学べるオンラインサービス「スキルショップ」で学習しています。 |
![]() |
フレックスタイム制の導入で従業員の満足度向上・労務管理の簡素化などが実現できました。 |
![]() |
マインドマップ作成ツール「XMind」を使用。マインドマップを用いることで頭の中が整理されより理解を深めることができます。 |
![]() |
社員研修に伴うサポート体制変更・休業のお知らせ(5/20〜23) 社員研修に伴い、5月20日〜23日はサポート体制の変更および休業とさせていただきます。 |
![]() |
オープンソースカンファレンス2025 Nagoyaに弊社代表の前田が登壇(ブース出展あり) オープンソースカンファレンス(OSC)2025 Nagoyaに弊社代表の前田が登壇。『Redmineの意外と知らない便利な機能(Redmine 6.0 対応版)』をテーマに発表します。 |
![]() |
エンタープライズプラン向け「優先サポート」を開始 My Redmineでは、エンタープライズプランをご契約のお客様向けにサポート対応を優先的に行う「優先サポート(プライオリティサポート)」を開始いたしました。 |
![]() |
プロジェクト管理ツール「RedMica」バージョン 3.1.0をリリース Redmine互換のオープンソースソフトウェア ファーエンドテクノロジー株式会社は、2024年11月19日(日本時間)、Redmine互換のプロジェクト管理ソフトウェア「RedMica 3.1.0」をリリースしました。 |
![]() |
Redmineの最新情報をメールでお知らせする「Redmine News」配信中 新バージョンやセキュリティ修正のリリース情報、そのほか最新情報を迅速にお届け |