Index: test/unit/lib/redmine/export/pdf_test.rb =================================================================== --- test/unit/lib/redmine/export/pdf_test.rb (revision 7697) +++ test/unit/lib/redmine/export/pdf_test.rb (working copy) @@ -34,9 +34,22 @@ assert_equal '', Redmine::Export::PDF::RDMPdfEncoding::rdm_pdf_iconv(ic, nil) end - def test_rdm_pdf_iconv_cannot_convert_ja_cp932 + def test_fix_text_decoding_nil set_language_if_valid 'ja' assert_equal 'CP932', l(:general_pdf_encoding) + if RUBY_VERSION < '1.9' + if RUBY_PLATFORM == 'java' + ic = Iconv.new("UTF-8", 'SJIS') + else + ic = Iconv.new('UTF-8', l(:general_pdf_encoding)) + end + end + assert_equal '', Redmine::Export::PDF::RDMPdfEncoding::rdm_pdf_iconv(ic, nil, true) + end + + def test_rdm_pdf_iconv_cannot_convert_ja_to_cp932 + set_language_if_valid 'ja' + assert_equal 'CP932', l(:general_pdf_encoding) if RUBY_VERSION < '1.9' if RUBY_PLATFORM == 'java' ic = Iconv.new("SJIS", 'UTF-8') @@ -44,6 +57,8 @@ ic = Iconv.new(l(:general_pdf_encoding), 'UTF-8') end end + # \xe7\x8b\x80 : invalid UTF-8 character + # \xe6\x85\x8b : valid UTF-8 character => \x91\xd4 : valid CP932 character utf8_txt_1 = "\xe7\x8b\x80\xe6\x85\x8b" utf8_txt_2 = "\xe7\x8b\x80\xe6\x85\x8b\xe7\x8b\x80" utf8_txt_3 = "\xe7\x8b\x80\xe7\x8b\x80\xe6\x85\x8b\xe7\x8b\x80" @@ -54,6 +69,9 @@ assert_equal "?\x91\xd4", txt_1 assert_equal "?\x91\xd4?", txt_2 assert_equal "??\x91\xd4?", txt_3 + assert_equal "UTF-8", utf8_txt_1.encoding.to_s + assert_equal "UTF-8", utf8_txt_2.encoding.to_s + assert_equal "UTF-8", utf8_txt_3.encoding.to_s assert_equal "ASCII-8BIT", txt_1.encoding.to_s assert_equal "ASCII-8BIT", txt_2.encoding.to_s assert_equal "ASCII-8BIT", txt_3.encoding.to_s @@ -74,6 +92,52 @@ end end + def test_rdm_pdf_iconv_cannot_convert_from_cp932 + set_language_if_valid 'ja' + assert_equal 'CP932', l(:general_pdf_encoding) + if RUBY_VERSION < '1.9' + if RUBY_PLATFORM == 'java' + ic = Iconv.new("UTF-8", 'SJIS') + else + ic = Iconv.new('UTF-8', l(:general_pdf_encoding)) + end + end + # \x90\xff : invalid CP932 character + # \x91\xe4 : valid CP932 character => \xe5\x8f\xb0 : valid UTF-8 character + + cp932_txt_1 = "\x90\xff\x91\xe4" + cp932_txt_2 = "\x90\xff\x91\xe4\x90\xff" + cp932_txt_3 = "\x90\xff\x90\xff\x91\xe4\x90\xff" + if cp932_txt_1.respond_to?(:force_encoding) + txt_1 = Redmine::Export::PDF::RDMPdfEncoding::rdm_pdf_iconv(ic, cp932_txt_1, true) + txt_2 = Redmine::Export::PDF::RDMPdfEncoding::rdm_pdf_iconv(ic, cp932_txt_2, true) + txt_3 = Redmine::Export::PDF::RDMPdfEncoding::rdm_pdf_iconv(ic, cp932_txt_3, true) + assert_equal "ASCII-8BIT", cp932_txt_1.encoding.to_s + assert_equal "ASCII-8BIT", cp932_txt_2.encoding.to_s + assert_equal "ASCII-8BIT", cp932_txt_3.encoding.to_s + assert_equal "UTF-8", txt_1.encoding.to_s + assert_equal "UTF-8", txt_2.encoding.to_s + assert_equal "UTF-8", txt_3.encoding.to_s + assert_equal "??\xe5\x8f\xb0", txt_1.force_encoding('ASCII-8BIT') + assert_equal "??\xe5\x8f\xb0??", txt_2.force_encoding('ASCII-8BIT') + assert_equal "????\xe5\x8f\xb0??", txt_3.force_encoding('ASCII-8BIT') + elsif RUBY_PLATFORM == 'java' + assert_equal "????", + Redmine::Export::PDF::RDMPdfEncoding::rdm_pdf_iconv(ic, cp932_txt_1, true) + assert_equal "??????", + Redmine::Export::PDF::RDMPdfEncoding::rdm_pdf_iconv(ic, cp932_txt_2, true) + assert_equal "????????", + Redmine::Export::PDF::RDMPdfEncoding::rdm_pdf_iconv(ic, cp932_txt_3, true) + else + assert_equal "??\xe5\x8f\xb0", + Redmine::Export::PDF::RDMPdfEncoding::rdm_pdf_iconv(ic, cp932_txt_1, true) + assert_equal "??\xe5\x8f\xb0??", + Redmine::Export::PDF::RDMPdfEncoding::rdm_pdf_iconv(ic, cp932_txt_2, true) + assert_equal "????\xe5\x8f\xb0??", + Redmine::Export::PDF::RDMPdfEncoding::rdm_pdf_iconv(ic, cp932_txt_3, true) + end + end + def test_rdm_pdf_iconv_invalid_utf8_should_be_replaced_en set_language_if_valid 'en' assert_equal 'UTF-8', l(:general_pdf_encoding) @@ -86,12 +150,20 @@ end txt_1 = Redmine::Export::PDF::RDMPdfEncoding::rdm_pdf_iconv(ic, str1) txt_2 = Redmine::Export::PDF::RDMPdfEncoding::rdm_pdf_iconv(ic, str2) + txt_3 = Redmine::Export::PDF::RDMPdfEncoding::rdm_pdf_iconv(ic, str1, true) + txt_4 = Redmine::Export::PDF::RDMPdfEncoding::rdm_pdf_iconv(ic, str2, true) if txt_1.respond_to?(:force_encoding) + assert_equal "UTF-8", str1.encoding.to_s + assert_equal "UTF-8", str2.encoding.to_s assert_equal "ASCII-8BIT", txt_1.encoding.to_s assert_equal "ASCII-8BIT", txt_2.encoding.to_s + assert_equal "ASCII-8BIT", txt_3.encoding.to_s + assert_equal "ASCII-8BIT", txt_4.encoding.to_s end assert_equal "Texte encod? en ISO-8859-1", txt_1 assert_equal "?a?b?c?d?e test", txt_2 + assert_equal "Texte encod? en ISO-8859-1", txt_3 + assert_equal "?a?b?c?d?e test", txt_4 end def test_rdm_pdf_iconv_invalid_utf8_should_be_replaced_ja @@ -111,6 +183,8 @@ txt_1 = Redmine::Export::PDF::RDMPdfEncoding::rdm_pdf_iconv(ic, str1) txt_2 = Redmine::Export::PDF::RDMPdfEncoding::rdm_pdf_iconv(ic, str2) if txt_1.respond_to?(:force_encoding) + assert_equal "UTF-8", str1.encoding.to_s + assert_equal "UTF-8", str2.encoding.to_s assert_equal "ASCII-8BIT", txt_1.encoding.to_s assert_equal "ASCII-8BIT", txt_2.encoding.to_s end Index: vendor/plugins/rfpdf/lib/tcpdf.rb =================================================================== --- vendor/plugins/rfpdf/lib/tcpdf.rb (revision 7697) +++ vendor/plugins/rfpdf/lib/tcpdf.rb (working copy) @@ -72,6 +72,10 @@ include Core::RFPDF include RFPDF::Math + def logger + Rails.logger + end + cattr_accessor :k_cell_height_ratio @@k_cell_height_ratio = 1.25 @@ -2045,17 +2049,27 @@ if (@images[file].nil?) #First use of image, get info if (type == '') - pos = file.rindex('.'); - if (pos == 0) + pos = File::basename(file).rindex('.'); + if (pos.nil? or pos == 0) Error('Image file has no extension and no type was specified: ' + file); end + pos = file.rindex('.'); type = file[pos+1..-1]; end type.downcase! if (type == 'jpg' or type == 'jpeg') info=parsejpg(file); - elsif (type == 'png') - info=parsepng(file); + elsif (type == 'png' or type == 'gif') + img = Magick::ImageList.new(file) + img.format = "PNG" # convert to PNG from gif + img.opacity = 0 # PNG alpha channel delete + File.open( @@k_path_cache + File::basename(file), 'w'){|f| + f.binmode + f.print img.to_blob + f.close + } + info=parsepng( @@k_path_cache + File::basename(file)); + File.delete( @@k_path_cache + File::basename(file)) else #Allow for additional formats mtd='parse' + type; @@ -2071,15 +2085,37 @@ end #Automatic width and height calculation if needed if ((w == 0) and (h == 0)) + rescale_x = (@w - @r_margin - x) / (info['w'] / (@img_scale * @k)) + rescale_x = 1 if rescale_x >= 1 + if (y + info['h'] * rescale_x / (@img_scale * @k) > @page_break_trigger and !@in_footer and AcceptPageBreak()) + #Automatic page break + if @pages[@page+1].nil? + ws = @ws; + if (ws > 0) + @ws = 0; + out('0 Tw'); + end + AddPage(@cur_orientation); + if (ws > 0) + @ws = ws; + out(sprintf('%.3f Tw', ws * @k)); + end + else + @page += 1; + end + y=@t_margin; + end + rescale_y = (@page_break_trigger - y) / (info['h'] / (@img_scale * @k)) + rescale_y = 1 if rescale_y >= 1 + rescale = rescale_y >= rescale_x ? rescale_x : rescale_y + #Put image at 72 dpi # 2004-06-14 :: Nicola Asuni, scale factor where added - w = info['w'] / (@img_scale * @k); - h = info['h'] / (@img_scale * @k); - end - if (w == 0) + w = info['w'] * rescale / (@img_scale * @k); + h = info['h'] * rescale / (@img_scale * @k); + elsif (w == 0) w = h * info['w'] / info['h']; - end - if (h == 0) + elsif (h == 0) h = w * info['h'] / info['w']; end out(sprintf('q %.2f 0 0 %.2f %.2f %.2f cm /I%d Do Q', w*@k, h*@k, x*@k, (@h-(y+h))*@k, info['i'])); @@ -2842,7 +2878,7 @@ if (a.empty?) Error('Missing or incorrect image file: ' + file); end - if (a[2]!='JPEG') + if (!a[2].nil? and a[2]!='JPEG') Error('Not a JPEG file: ' + file); end if (a['channels'].nil? or a['channels']==3) @@ -2855,9 +2891,12 @@ bpc=!a['bits'].nil? ? a['bits'] : 8; #Read whole file data=''; - open(file,'rb') do |f| + + open( @@k_path_cache + File::basename(file),'rb') do |f| data< a[0],'h' => a[1],'cs' => colspace,'bpc' => bpc,'f'=>'DCTDecode','data' => data} end @@ -2878,11 +2917,11 @@ end w=freadint(f); h=freadint(f); - bpc=f.read(1)[0]; + bpc=f.read(1).unpack('C')[0]; if (bpc>8) Error('16-bit depth not supported: ' + file); end - ct=f.read(1)[0]; + ct=f.read(1).unpack('C')[0]; if (ct==0) colspace='DeviceGray'; elsif (ct==2) @@ -2892,13 +2931,13 @@ else Error('Alpha channel not supported: ' + file); end - if (f.read(1)[0] != 0) + if (f.read(1).unpack('C')[0] != 0) Error('Unknown compression method: ' + file); end - if (f.read(1)[0]!=0) + if (f.read(1).unpack('C')[0] != 0) Error('Unknown filter method: ' + file); end - if (f.read(1)[0]!=0) + if (f.read(1).unpack('C')[0] != 0) Error('Interlacing not supported: ' + file); end f.read(4); @@ -2918,9 +2957,9 @@ #Read transparency info t=f.read( n); if (ct==0) - trns = t[1][0] + trns = t[1].unpack('C')[0] elsif (ct==2) - trns = t[[1][0], t[3][0], t[5][0]] + trns = t[[1].unpack('C')[0], t[3].unpack('C')[0], t[5].unpack('C')[0]] else pos=t.include?(0.chr); if (pos!=false) @@ -3762,6 +3801,20 @@ end # + # Convert to accessible file path + # @param string :attrname image file name + # @access private + # + def getImageFilename( attrname ) + # check of unsafe URI + if img.to_s =~ /[^-_.!~*'()a-zA-Z\d;\/?:@&=+$,\[\]%]/n + URI.encode(attrname) + else + attrname + end + end + + # # Process opening tags. # @param string :tag tag name (in upcase) # @param string :attr tag attribute (in upcase) @@ -3874,10 +3927,17 @@ when 'img' if (!attrs['src'].nil?) - Write(@lasth, '!' + attrs['src'] + '!', '', fill); -=begin Comment out. Because not implement image output yet. - # replace relative path with real server path - attrs['src'] = attrs['src'].gsub(@@k_path_url_cache, @@k_path_cache); + # Only generates image include a pdf if RMagick is avalaible + unless Object.const_defined?(:Magick) + Write(@lasth, attrs['src'], '', fill); + return + end + file = getImageFilename(attrs['src']) + if (file.nil?) + Write(@lasth, attrs['src'], '', fill); + return + end + if (attrs['width'].nil?) attrs['width'] = 0; end @@ -3885,10 +3945,17 @@ attrs['height'] = 0; end - Image(attrs['src'], GetX(),GetY(), pixelsToMillimeters(attrs['width']), pixelsToMillimeters(attrs['height'])); - #SetX(@img_rb_x); - SetY(@img_rb_y); -=end + begin + Image(file, GetX(),GetY(), pixelsToMillimeters(attrs['width']), pixelsToMillimeters(attrs['height'])); + #SetX(@img_rb_x); + SetY(@img_rb_y); + rescue => err + logger.error "pdf: Image: error: #{err.message}" + Write(@lasth, attrs['src'], '', fill); + if File.file?( @@k_path_cache + File::basename(file)) + File.delete( @@k_path_cache + File::basename(file)) + end + end end when 'ul', 'ol' Index: vendor/plugins/rfpdf/lib/core/rmagick.rb =================================================================== --- vendor/plugins/rfpdf/lib/core/rmagick.rb (revision 7697) +++ vendor/plugins/rfpdf/lib/core/rmagick.rb (working copy) @@ -57,8 +57,13 @@ end out['bits'] = image.channel_depth + File.open( TCPDF.k_path_cache + File::basename(filename), 'w'){|f| + f.binmode + f.print image.to_blob + f.close + } out end -end \ No newline at end of file +end Index: lib/redmine/export/pdf.rb =================================================================== --- lib/redmine/export/pdf.rb (revision 7697) +++ lib/redmine/export/pdf.rb (working copy) @@ -18,10 +18,10 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. require 'iconv' -require 'rfpdf/fpdf' require 'fpdf/chinese' require 'fpdf/japanese' require 'fpdf/korean' +require 'core/rmagick' module Redmine module Export @@ -34,10 +34,13 @@ attr_accessor :footer_date def initialize(lang) + @@k_path_cache = Rails.root.join('tmp', 'pdf') + FileUtils.mkdir_p @@k_path_cache unless File::exist?(@@k_path_cache) set_language_if_valid lang pdf_encoding = l(:general_pdf_encoding).upcase if RUBY_VERSION < '1.9' - @ic = Iconv.new(pdf_encoding, 'UTF-8') + @ic_encode = Iconv.new(pdf_encoding, 'UTF-8') + @ic_decode = Iconv.new('UTF-8', pdf_encoding) end super('P', 'mm', 'A4', (pdf_encoding == 'UTF-8'), pdf_encoding) case current_language.to_s.downcase @@ -104,9 +107,13 @@ end def fix_text_encoding(txt) - RDMPdfEncoding::rdm_pdf_iconv(@ic, txt) + RDMPdfEncoding::rdm_pdf_iconv(@ic_encode, txt) end + def fix_text_decoding(txt) + RDMPdfEncoding::rdm_pdf_iconv(@ic_decode, txt, true) + end + def RDMCell(w ,h=0, txt='', border=0, ln=0, align='', fill=0, link='') Cell(w, h, fix_text_encoding(txt), border, ln, align, fill, link) end @@ -115,10 +122,37 @@ MultiCell(w, h, fix_text_encoding(txt), border, align, fill, ln) end - def RDMwriteHTMLCell(w, h, x, y, html='', border=0, ln=1, fill=0) - writeHTMLCell(w, h, x, y, fix_text_encoding(html), border, ln, fill) + def RDMwriteHTMLCell(w, h, x, y, txt='', attachments=[], border=0, ln=1, fill=0) + @attachments = attachments.sort_by(&:created_on).reverse + + writeHTMLCell(w, h, x, y, + fix_text_encoding(Redmine::WikiFormatting.to_html(Setting.text_formatting, txt)), + border, ln, fill) end + def getImageFilename(attrname) + # attrname : general_pdf_encoding string file/uri name + if /^https?:\/\//i =~ attrname + uri = fix_text_decoding(attrname) # get original URI + + # check of unsafe URI + if uri =~ /[^-_.!~*'()a-zA-Z\d;\/?:@&=+$,\[\]%]/n + return URI.encode(uri) + else + return uri + end + else + for attachment in @attachments + # attachment.filename : UTF-8 string name + # attachment.diskfile : real server path file name + if fix_text_encoding(attachment.filename) == attrname && File.file?(attachment.diskfile) + return attachment.diskfile + end + end + end + return nil + end + def Footer SetFont(@font_for_footer, 'I', 8) SetY(-15) @@ -344,9 +378,11 @@ pdf.SetFontStyle('B',9) pdf.RDMCell(35+155, 5, l(:field_description), "LRT", 1) pdf.SetFontStyle('',9) + + # Set resize image scale + pdf.SetImageScale(1.6) pdf.RDMwriteHTMLCell(35+155, 5, 0, 0, - Redmine::WikiFormatting.to_html( - Setting.text_formatting, issue.description.to_s),"LRB") + issue.description.to_s, issue.attachments, "LRB") pdf.Ln if issue.changesets.any? && @@ -363,8 +399,7 @@ unless changeset.comments.blank? pdf.SetFontStyle('',8) pdf.RDMwriteHTMLCell(190,5,0,0, - Redmine::WikiFormatting.to_html( - Setting.text_formatting, changeset.comments.to_s), "") + changeset.comments.to_s, issue.attachments, "") end pdf.Ln end @@ -388,8 +423,7 @@ pdf.Ln unless journal.details.empty? pdf.SetFontStyle('',8) pdf.RDMwriteHTMLCell(190,5,0,0, - Redmine::WikiFormatting.to_html( - Setting.text_formatting, journal.notes.to_s), "") + journal.notes.to_s, issue.attachments, "") end pdf.Ln end @@ -412,26 +446,44 @@ class RDMPdfEncoding include Redmine::I18n - def self.rdm_pdf_iconv(ic, txt) + def self.rdm_pdf_iconv(ic, txt, toUTF8 = false) txt ||= '' if txt.respond_to?(:force_encoding) - txt.force_encoding('UTF-8') if l(:general_pdf_encoding).upcase != 'UTF-8' - txt = txt.encode(l(:general_pdf_encoding), :invalid => :replace, - :undef => :replace, :replace => '?') + if toUTF8 == false + txt.force_encoding('UTF-8') + txtar = txt.encode(l(:general_pdf_encoding), :invalid => :replace, + :undef => :replace, :replace => '?') + txtar.force_encoding('ASCII-8BIT') + else + txt.force_encoding(l(:general_pdf_encoding)) + txtar = txt.encode('UTF-8', :invalid => :replace, + :undef => :replace, :replace => '?') + txt.force_encoding('ASCII-8BIT') + end else - txt = Redmine::CodesetUtil.replace_invalid_utf8(txt) + txt.force_encoding('UTF-8') + txtar = Redmine::CodesetUtil.replace_invalid_utf8(txt) + txtar.force_encoding('ASCII-8BIT') end - txt.force_encoding('ASCII-8BIT') + txt = txtar elsif RUBY_PLATFORM == 'java' begin - ic ||= Iconv.new(l(:general_pdf_encoding), 'UTF-8') + if toUTF8 == false + ic ||= Iconv.new(l(:general_pdf_encoding), 'UTF-8') + else + ic ||= Iconv.new('UTF-8', l(:general_pdf_encoding)) + end txt = ic.iconv(txt) rescue txt = txt.gsub(%r{[^\r\n\t\x20-\x7e]}, '?') end else - ic ||= Iconv.new(l(:general_pdf_encoding), 'UTF-8') + if toUTF8 == false + ic ||= Iconv.new(l(:general_pdf_encoding), 'UTF-8') + else + ic ||= Iconv.new('UTF-8', l(:general_pdf_encoding)) + end txtar = "" begin txtar += ic.iconv(txt)