Mercurial > hg > soundsoftware-site
comparison .svn/pristine/aa/aa6f4c5dea2bc16ad3aaedef8479df833e7780fa.svn-base @ 1517:dffacf8a6908 redmine-2.5
Update to Redmine SVN revision 13367 on 2.5-stable branch
author | Chris Cannam |
---|---|
date | Tue, 09 Sep 2014 09:29:00 +0100 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
1516:b450a9d58aed | 1517:dffacf8a6908 |
---|---|
1 # encoding: utf-8 | |
2 # | |
3 # Redmine - project management software | |
4 # Copyright (C) 2006-2014 Jean-Philippe Lang | |
5 # | |
6 # This program is free software; you can redistribute it and/or | |
7 # modify it under the terms of the GNU General Public License | |
8 # as published by the Free Software Foundation; either version 2 | |
9 # of the License, or (at your option) any later version. | |
10 # | |
11 # This program is distributed in the hope that it will be useful, | |
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
14 # GNU General Public License for more details. | |
15 # | |
16 # You should have received a copy of the GNU General Public License | |
17 # along with this program; if not, write to the Free Software | |
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |
19 | |
20 require 'tcpdf' | |
21 require 'fpdf/chinese' | |
22 require 'fpdf/japanese' | |
23 require 'fpdf/korean' | |
24 | |
25 if RUBY_VERSION < '1.9' | |
26 require 'iconv' | |
27 end | |
28 | |
29 module Redmine | |
30 module Export | |
31 module PDF | |
32 include ActionView::Helpers::TextHelper | |
33 include ActionView::Helpers::NumberHelper | |
34 include IssuesHelper | |
35 | |
36 class ITCPDF < TCPDF | |
37 include Redmine::I18n | |
38 attr_accessor :footer_date | |
39 | |
40 def initialize(lang, orientation='P') | |
41 @@k_path_cache = Rails.root.join('tmp', 'pdf') | |
42 FileUtils.mkdir_p @@k_path_cache unless File::exist?(@@k_path_cache) | |
43 set_language_if_valid lang | |
44 pdf_encoding = l(:general_pdf_encoding).upcase | |
45 super(orientation, 'mm', 'A4', (pdf_encoding == 'UTF-8'), pdf_encoding) | |
46 case current_language.to_s.downcase | |
47 when 'vi' | |
48 @font_for_content = 'DejaVuSans' | |
49 @font_for_footer = 'DejaVuSans' | |
50 else | |
51 case pdf_encoding | |
52 when 'UTF-8' | |
53 @font_for_content = 'FreeSans' | |
54 @font_for_footer = 'FreeSans' | |
55 when 'CP949' | |
56 extend(PDF_Korean) | |
57 AddUHCFont() | |
58 @font_for_content = 'UHC' | |
59 @font_for_footer = 'UHC' | |
60 when 'CP932', 'SJIS', 'SHIFT_JIS' | |
61 extend(PDF_Japanese) | |
62 AddSJISFont() | |
63 @font_for_content = 'SJIS' | |
64 @font_for_footer = 'SJIS' | |
65 when 'GB18030' | |
66 extend(PDF_Chinese) | |
67 AddGBFont() | |
68 @font_for_content = 'GB' | |
69 @font_for_footer = 'GB' | |
70 when 'BIG5' | |
71 extend(PDF_Chinese) | |
72 AddBig5Font() | |
73 @font_for_content = 'Big5' | |
74 @font_for_footer = 'Big5' | |
75 else | |
76 @font_for_content = 'Arial' | |
77 @font_for_footer = 'Helvetica' | |
78 end | |
79 end | |
80 SetCreator(Redmine::Info.app_name) | |
81 SetFont(@font_for_content) | |
82 @outlines = [] | |
83 @outlineRoot = nil | |
84 end | |
85 | |
86 def SetFontStyle(style, size) | |
87 SetFont(@font_for_content, style, size) | |
88 end | |
89 | |
90 def SetTitle(txt) | |
91 txt = begin | |
92 utf16txt = to_utf16(txt) | |
93 hextxt = "<FEFF" # FEFF is BOM | |
94 hextxt << utf16txt.unpack("C*").map {|x| sprintf("%02X",x) }.join | |
95 hextxt << ">" | |
96 rescue | |
97 txt | |
98 end || '' | |
99 super(txt) | |
100 end | |
101 | |
102 def textstring(s) | |
103 # Format a text string | |
104 if s =~ /^</ # This means the string is hex-dumped. | |
105 return s | |
106 else | |
107 return '('+escape(s)+')' | |
108 end | |
109 end | |
110 | |
111 def fix_text_encoding(txt) | |
112 RDMPdfEncoding::rdm_from_utf8(txt, l(:general_pdf_encoding)) | |
113 end | |
114 | |
115 def formatted_text(text) | |
116 html = Redmine::WikiFormatting.to_html(Setting.text_formatting, text) | |
117 # Strip {{toc}} tags | |
118 html.gsub!(/<p>\{\{([<>]?)toc\}\}<\/p>/i, '') | |
119 html | |
120 end | |
121 | |
122 # Encodes an UTF-8 string to UTF-16BE | |
123 def to_utf16(str) | |
124 if str.respond_to?(:encode) | |
125 str.encode('UTF-16BE') | |
126 else | |
127 Iconv.conv('UTF-16BE', 'UTF-8', str) | |
128 end | |
129 end | |
130 | |
131 def RDMCell(w ,h=0, txt='', border=0, ln=0, align='', fill=0, link='') | |
132 Cell(w, h, fix_text_encoding(txt), border, ln, align, fill, link) | |
133 end | |
134 | |
135 def RDMMultiCell(w, h=0, txt='', border=0, align='', fill=0, ln=1) | |
136 MultiCell(w, h, fix_text_encoding(txt), border, align, fill, ln) | |
137 end | |
138 | |
139 def RDMwriteHTMLCell(w, h, x, y, txt='', attachments=[], border=0, ln=1, fill=0) | |
140 @attachments = attachments | |
141 writeHTMLCell(w, h, x, y, | |
142 fix_text_encoding(formatted_text(txt)), | |
143 border, ln, fill) | |
144 end | |
145 | |
146 def getImageFilename(attrname) | |
147 # attrname: general_pdf_encoding string file/uri name | |
148 atta = RDMPdfEncoding.attach(@attachments, attrname, l(:general_pdf_encoding)) | |
149 if atta | |
150 return atta.diskfile | |
151 else | |
152 return nil | |
153 end | |
154 end | |
155 | |
156 def Footer | |
157 SetFont(@font_for_footer, 'I', 8) | |
158 SetY(-15) | |
159 SetX(15) | |
160 RDMCell(0, 5, @footer_date, 0, 0, 'L') | |
161 SetY(-15) | |
162 SetX(-30) | |
163 RDMCell(0, 5, PageNo().to_s + '/{nb}', 0, 0, 'C') | |
164 end | |
165 | |
166 def Bookmark(txt, level=0, y=0) | |
167 if (y == -1) | |
168 y = GetY() | |
169 end | |
170 @outlines << {:t => txt, :l => level, :p => PageNo(), :y => (@h - y)*@k} | |
171 end | |
172 | |
173 def bookmark_title(txt) | |
174 txt = begin | |
175 utf16txt = to_utf16(txt) | |
176 hextxt = "<FEFF" # FEFF is BOM | |
177 hextxt << utf16txt.unpack("C*").map {|x| sprintf("%02X",x) }.join | |
178 hextxt << ">" | |
179 rescue | |
180 txt | |
181 end || '' | |
182 end | |
183 | |
184 def putbookmarks | |
185 nb=@outlines.size | |
186 return if (nb==0) | |
187 lru=[] | |
188 level=0 | |
189 @outlines.each_with_index do |o, i| | |
190 if(o[:l]>0) | |
191 parent=lru[o[:l]-1] | |
192 #Set parent and last pointers | |
193 @outlines[i][:parent]=parent | |
194 @outlines[parent][:last]=i | |
195 if (o[:l]>level) | |
196 #Level increasing: set first pointer | |
197 @outlines[parent][:first]=i | |
198 end | |
199 else | |
200 @outlines[i][:parent]=nb | |
201 end | |
202 if (o[:l]<=level && i>0) | |
203 #Set prev and next pointers | |
204 prev=lru[o[:l]] | |
205 @outlines[prev][:next]=i | |
206 @outlines[i][:prev]=prev | |
207 end | |
208 lru[o[:l]]=i | |
209 level=o[:l] | |
210 end | |
211 #Outline items | |
212 n=self.n+1 | |
213 @outlines.each_with_index do |o, i| | |
214 newobj() | |
215 out('<</Title '+bookmark_title(o[:t])) | |
216 out("/Parent #{n+o[:parent]} 0 R") | |
217 if (o[:prev]) | |
218 out("/Prev #{n+o[:prev]} 0 R") | |
219 end | |
220 if (o[:next]) | |
221 out("/Next #{n+o[:next]} 0 R") | |
222 end | |
223 if (o[:first]) | |
224 out("/First #{n+o[:first]} 0 R") | |
225 end | |
226 if (o[:last]) | |
227 out("/Last #{n+o[:last]} 0 R") | |
228 end | |
229 out("/Dest [%d 0 R /XYZ 0 %.2f null]" % [1+2*o[:p], o[:y]]) | |
230 out('/Count 0>>') | |
231 out('endobj') | |
232 end | |
233 #Outline root | |
234 newobj() | |
235 @outlineRoot=self.n | |
236 out("<</Type /Outlines /First #{n} 0 R"); | |
237 out("/Last #{n+lru[0]} 0 R>>"); | |
238 out('endobj'); | |
239 end | |
240 | |
241 def putresources() | |
242 super | |
243 putbookmarks() | |
244 end | |
245 | |
246 def putcatalog() | |
247 super | |
248 if(@outlines.size > 0) | |
249 out("/Outlines #{@outlineRoot} 0 R"); | |
250 out('/PageMode /UseOutlines'); | |
251 end | |
252 end | |
253 end | |
254 | |
255 # fetch row values | |
256 def fetch_row_values(issue, query, level) | |
257 query.inline_columns.collect do |column| | |
258 s = if column.is_a?(QueryCustomFieldColumn) | |
259 cv = issue.visible_custom_field_values.detect {|v| v.custom_field_id == column.custom_field.id} | |
260 show_value(cv, false) | |
261 else | |
262 value = issue.send(column.name) | |
263 if column.name == :subject | |
264 value = " " * level + value | |
265 end | |
266 if value.is_a?(Date) | |
267 format_date(value) | |
268 elsif value.is_a?(Time) | |
269 format_time(value) | |
270 else | |
271 value | |
272 end | |
273 end | |
274 s.to_s | |
275 end | |
276 end | |
277 | |
278 # calculate columns width | |
279 def calc_col_width(issues, query, table_width, pdf) | |
280 # calculate statistics | |
281 # by captions | |
282 pdf.SetFontStyle('B',8) | |
283 col_padding = pdf.GetStringWidth('OO') | |
284 col_width_min = query.inline_columns.map {|v| pdf.GetStringWidth(v.caption) + col_padding} | |
285 col_width_max = Array.new(col_width_min) | |
286 col_width_avg = Array.new(col_width_min) | |
287 word_width_max = query.inline_columns.map {|c| | |
288 n = 10 | |
289 c.caption.split.each {|w| | |
290 x = pdf.GetStringWidth(w) + col_padding | |
291 n = x if n < x | |
292 } | |
293 n | |
294 } | |
295 | |
296 # by properties of issues | |
297 pdf.SetFontStyle('',8) | |
298 col_padding = pdf.GetStringWidth('OO') | |
299 k = 1 | |
300 issue_list(issues) {|issue, level| | |
301 k += 1 | |
302 values = fetch_row_values(issue, query, level) | |
303 values.each_with_index {|v,i| | |
304 n = pdf.GetStringWidth(v) + col_padding | |
305 col_width_max[i] = n if col_width_max[i] < n | |
306 col_width_min[i] = n if col_width_min[i] > n | |
307 col_width_avg[i] += n | |
308 v.split.each {|w| | |
309 x = pdf.GetStringWidth(w) + col_padding | |
310 word_width_max[i] = x if word_width_max[i] < x | |
311 } | |
312 } | |
313 } | |
314 col_width_avg.map! {|x| x / k} | |
315 | |
316 # calculate columns width | |
317 ratio = table_width / col_width_avg.inject(0, :+) | |
318 col_width = col_width_avg.map {|w| w * ratio} | |
319 | |
320 # correct max word width if too many columns | |
321 ratio = table_width / word_width_max.inject(0, :+) | |
322 word_width_max.map! {|v| v * ratio} if ratio < 1 | |
323 | |
324 # correct and lock width of some columns | |
325 done = 1 | |
326 col_fix = [] | |
327 col_width.each_with_index do |w,i| | |
328 if w > col_width_max[i] | |
329 col_width[i] = col_width_max[i] | |
330 col_fix[i] = 1 | |
331 done = 0 | |
332 elsif w < word_width_max[i] | |
333 col_width[i] = word_width_max[i] | |
334 col_fix[i] = 1 | |
335 done = 0 | |
336 else | |
337 col_fix[i] = 0 | |
338 end | |
339 end | |
340 | |
341 # iterate while need to correct and lock coluns width | |
342 while done == 0 | |
343 # calculate free & locked columns width | |
344 done = 1 | |
345 fix_col_width = 0 | |
346 free_col_width = 0 | |
347 col_width.each_with_index do |w,i| | |
348 if col_fix[i] == 1 | |
349 fix_col_width += w | |
350 else | |
351 free_col_width += w | |
352 end | |
353 end | |
354 | |
355 # calculate column normalizing ratio | |
356 if free_col_width == 0 | |
357 ratio = table_width / col_width.inject(0, :+) | |
358 else | |
359 ratio = (table_width - fix_col_width) / free_col_width | |
360 end | |
361 | |
362 # correct columns width | |
363 col_width.each_with_index do |w,i| | |
364 if col_fix[i] == 0 | |
365 col_width[i] = w * ratio | |
366 | |
367 # check if column width less then max word width | |
368 if col_width[i] < word_width_max[i] | |
369 col_width[i] = word_width_max[i] | |
370 col_fix[i] = 1 | |
371 done = 0 | |
372 elsif col_width[i] > col_width_max[i] | |
373 col_width[i] = col_width_max[i] | |
374 col_fix[i] = 1 | |
375 done = 0 | |
376 end | |
377 end | |
378 end | |
379 end | |
380 col_width | |
381 end | |
382 | |
383 def render_table_header(pdf, query, col_width, row_height, table_width) | |
384 # headers | |
385 pdf.SetFontStyle('B',8) | |
386 pdf.SetFillColor(230, 230, 230) | |
387 | |
388 # render it background to find the max height used | |
389 base_x = pdf.GetX | |
390 base_y = pdf.GetY | |
391 max_height = issues_to_pdf_write_cells(pdf, query.inline_columns, col_width, row_height, true) | |
392 pdf.Rect(base_x, base_y, table_width, max_height, 'FD'); | |
393 pdf.SetXY(base_x, base_y); | |
394 | |
395 # write the cells on page | |
396 issues_to_pdf_write_cells(pdf, query.inline_columns, col_width, row_height, true) | |
397 issues_to_pdf_draw_borders(pdf, base_x, base_y, base_y + max_height, 0, col_width) | |
398 pdf.SetY(base_y + max_height); | |
399 | |
400 # rows | |
401 pdf.SetFontStyle('',8) | |
402 pdf.SetFillColor(255, 255, 255) | |
403 end | |
404 | |
405 # Returns a PDF string of a list of issues | |
406 def issues_to_pdf(issues, project, query) | |
407 pdf = ITCPDF.new(current_language, "L") | |
408 title = query.new_record? ? l(:label_issue_plural) : query.name | |
409 title = "#{project} - #{title}" if project | |
410 pdf.SetTitle(title) | |
411 pdf.alias_nb_pages | |
412 pdf.footer_date = format_date(Date.today) | |
413 pdf.SetAutoPageBreak(false) | |
414 pdf.AddPage("L") | |
415 | |
416 # Landscape A4 = 210 x 297 mm | |
417 page_height = 210 | |
418 page_width = 297 | |
419 left_margin = 10 | |
420 right_margin = 10 | |
421 bottom_margin = 20 | |
422 row_height = 4 | |
423 | |
424 # column widths | |
425 table_width = page_width - right_margin - left_margin | |
426 col_width = [] | |
427 unless query.inline_columns.empty? | |
428 col_width = calc_col_width(issues, query, table_width, pdf) | |
429 table_width = col_width.inject(0, :+) | |
430 end | |
431 | |
432 # use full width if the description is displayed | |
433 if table_width > 0 && query.has_column?(:description) | |
434 col_width = col_width.map {|w| w * (page_width - right_margin - left_margin) / table_width} | |
435 table_width = col_width.inject(0, :+) | |
436 end | |
437 | |
438 # title | |
439 pdf.SetFontStyle('B',11) | |
440 pdf.RDMCell(190,10, title) | |
441 pdf.Ln | |
442 render_table_header(pdf, query, col_width, row_height, table_width) | |
443 previous_group = false | |
444 issue_list(issues) do |issue, level| | |
445 if query.grouped? && | |
446 (group = query.group_by_column.value(issue)) != previous_group | |
447 pdf.SetFontStyle('B',10) | |
448 group_label = group.blank? ? 'None' : group.to_s.dup | |
449 group_label << " (#{query.issue_count_by_group[group]})" | |
450 pdf.Bookmark group_label, 0, -1 | |
451 pdf.RDMCell(table_width, row_height * 2, group_label, 1, 1, 'L') | |
452 pdf.SetFontStyle('',8) | |
453 previous_group = group | |
454 end | |
455 | |
456 # fetch row values | |
457 col_values = fetch_row_values(issue, query, level) | |
458 | |
459 # render it off-page to find the max height used | |
460 base_x = pdf.GetX | |
461 base_y = pdf.GetY | |
462 pdf.SetY(2 * page_height) | |
463 max_height = issues_to_pdf_write_cells(pdf, col_values, col_width, row_height) | |
464 pdf.SetXY(base_x, base_y) | |
465 | |
466 # make new page if it doesn't fit on the current one | |
467 space_left = page_height - base_y - bottom_margin | |
468 if max_height > space_left | |
469 pdf.AddPage("L") | |
470 render_table_header(pdf, query, col_width, row_height, table_width) | |
471 base_x = pdf.GetX | |
472 base_y = pdf.GetY | |
473 end | |
474 | |
475 # write the cells on page | |
476 issues_to_pdf_write_cells(pdf, col_values, col_width, row_height) | |
477 issues_to_pdf_draw_borders(pdf, base_x, base_y, base_y + max_height, 0, col_width) | |
478 pdf.SetY(base_y + max_height); | |
479 | |
480 if query.has_column?(:description) && issue.description? | |
481 pdf.SetX(10) | |
482 pdf.SetAutoPageBreak(true, 20) | |
483 pdf.RDMwriteHTMLCell(0, 5, 10, 0, issue.description.to_s, issue.attachments, "LRBT") | |
484 pdf.SetAutoPageBreak(false) | |
485 end | |
486 end | |
487 | |
488 if issues.size == Setting.issues_export_limit.to_i | |
489 pdf.SetFontStyle('B',10) | |
490 pdf.RDMCell(0, row_height, '...') | |
491 end | |
492 pdf.Output | |
493 end | |
494 | |
495 # Renders MultiCells and returns the maximum height used | |
496 def issues_to_pdf_write_cells(pdf, col_values, col_widths, row_height, head=false) | |
497 base_y = pdf.GetY | |
498 max_height = row_height | |
499 col_values.each_with_index do |column, i| | |
500 col_x = pdf.GetX | |
501 if head == true | |
502 pdf.RDMMultiCell(col_widths[i], row_height, column.caption, "T", 'L', 1) | |
503 else | |
504 pdf.RDMMultiCell(col_widths[i], row_height, column, "T", 'L', 1) | |
505 end | |
506 max_height = (pdf.GetY - base_y) if (pdf.GetY - base_y) > max_height | |
507 pdf.SetXY(col_x + col_widths[i], base_y); | |
508 end | |
509 return max_height | |
510 end | |
511 | |
512 # Draw lines to close the row (MultiCell border drawing in not uniform) | |
513 # | |
514 # parameter "col_id_width" is not used. it is kept for compatibility. | |
515 def issues_to_pdf_draw_borders(pdf, top_x, top_y, lower_y, | |
516 col_id_width, col_widths) | |
517 col_x = top_x | |
518 pdf.Line(col_x, top_y, col_x, lower_y) # id right border | |
519 col_widths.each do |width| | |
520 col_x += width | |
521 pdf.Line(col_x, top_y, col_x, lower_y) # columns right border | |
522 end | |
523 pdf.Line(top_x, top_y, top_x, lower_y) # left border | |
524 pdf.Line(top_x, lower_y, col_x, lower_y) # bottom border | |
525 end | |
526 | |
527 # Returns a PDF string of a single issue | |
528 def issue_to_pdf(issue, assoc={}) | |
529 pdf = ITCPDF.new(current_language) | |
530 pdf.SetTitle("#{issue.project} - #{issue.tracker} ##{issue.id}") | |
531 pdf.alias_nb_pages | |
532 pdf.footer_date = format_date(Date.today) | |
533 pdf.AddPage | |
534 pdf.SetFontStyle('B',11) | |
535 buf = "#{issue.project} - #{issue.tracker} ##{issue.id}" | |
536 pdf.RDMMultiCell(190, 5, buf) | |
537 pdf.SetFontStyle('',8) | |
538 base_x = pdf.GetX | |
539 i = 1 | |
540 issue.ancestors.visible.each do |ancestor| | |
541 pdf.SetX(base_x + i) | |
542 buf = "#{ancestor.tracker} # #{ancestor.id} (#{ancestor.status.to_s}): #{ancestor.subject}" | |
543 pdf.RDMMultiCell(190 - i, 5, buf) | |
544 i += 1 if i < 35 | |
545 end | |
546 pdf.SetFontStyle('B',11) | |
547 pdf.RDMMultiCell(190 - i, 5, issue.subject.to_s) | |
548 pdf.SetFontStyle('',8) | |
549 pdf.RDMMultiCell(190, 5, "#{format_time(issue.created_on)} - #{issue.author}") | |
550 pdf.Ln | |
551 | |
552 left = [] | |
553 left << [l(:field_status), issue.status] | |
554 left << [l(:field_priority), issue.priority] | |
555 left << [l(:field_assigned_to), issue.assigned_to] unless issue.disabled_core_fields.include?('assigned_to_id') | |
556 left << [l(:field_category), issue.category] unless issue.disabled_core_fields.include?('category_id') | |
557 left << [l(:field_fixed_version), issue.fixed_version] unless issue.disabled_core_fields.include?('fixed_version_id') | |
558 | |
559 right = [] | |
560 right << [l(:field_start_date), format_date(issue.start_date)] unless issue.disabled_core_fields.include?('start_date') | |
561 right << [l(:field_due_date), format_date(issue.due_date)] unless issue.disabled_core_fields.include?('due_date') | |
562 right << [l(:field_done_ratio), "#{issue.done_ratio}%"] unless issue.disabled_core_fields.include?('done_ratio') | |
563 right << [l(:field_estimated_hours), l_hours(issue.estimated_hours)] unless issue.disabled_core_fields.include?('estimated_hours') | |
564 right << [l(:label_spent_time), l_hours(issue.total_spent_hours)] if User.current.allowed_to?(:view_time_entries, issue.project) | |
565 | |
566 rows = left.size > right.size ? left.size : right.size | |
567 while left.size < rows | |
568 left << nil | |
569 end | |
570 while right.size < rows | |
571 right << nil | |
572 end | |
573 | |
574 half = (issue.visible_custom_field_values.size / 2.0).ceil | |
575 issue.visible_custom_field_values.each_with_index do |custom_value, i| | |
576 (i < half ? left : right) << [custom_value.custom_field.name, show_value(custom_value, false)] | |
577 end | |
578 | |
579 rows = left.size > right.size ? left.size : right.size | |
580 rows.times do |i| | |
581 item = left[i] | |
582 pdf.SetFontStyle('B',9) | |
583 pdf.RDMCell(35,5, item ? "#{item.first}:" : "", i == 0 ? "LT" : "L") | |
584 pdf.SetFontStyle('',9) | |
585 pdf.RDMCell(60,5, item ? item.last.to_s : "", i == 0 ? "RT" : "R") | |
586 | |
587 item = right[i] | |
588 pdf.SetFontStyle('B',9) | |
589 pdf.RDMCell(35,5, item ? "#{item.first}:" : "", i == 0 ? "LT" : "L") | |
590 pdf.SetFontStyle('',9) | |
591 pdf.RDMCell(60,5, item ? item.last.to_s : "", i == 0 ? "RT" : "R") | |
592 pdf.Ln | |
593 end | |
594 | |
595 pdf.SetFontStyle('B',9) | |
596 pdf.RDMCell(35+155, 5, l(:field_description), "LRT", 1) | |
597 pdf.SetFontStyle('',9) | |
598 | |
599 # Set resize image scale | |
600 pdf.SetImageScale(1.6) | |
601 pdf.RDMwriteHTMLCell(35+155, 5, 0, 0, | |
602 issue.description.to_s, issue.attachments, "LRB") | |
603 | |
604 unless issue.leaf? | |
605 # for CJK | |
606 truncate_length = ( l(:general_pdf_encoding).upcase == "UTF-8" ? 90 : 65 ) | |
607 pdf.SetFontStyle('B',9) | |
608 pdf.RDMCell(35+155,5, l(:label_subtask_plural) + ":", "LTR") | |
609 pdf.Ln | |
610 issue_list(issue.descendants.visible.sort_by(&:lft)) do |child, level| | |
611 buf = "#{child.tracker} # #{child.id}: #{child.subject}". | |
612 truncate(truncate_length) | |
613 level = 10 if level >= 10 | |
614 pdf.SetFontStyle('',8) | |
615 pdf.RDMCell(35+135,5, (level >=1 ? " " * level : "") + buf, "L") | |
616 pdf.SetFontStyle('B',8) | |
617 pdf.RDMCell(20,5, child.status.to_s, "R") | |
618 pdf.Ln | |
619 end | |
620 end | |
621 | |
622 relations = issue.relations.select { |r| r.other_issue(issue).visible? } | |
623 unless relations.empty? | |
624 # for CJK | |
625 truncate_length = ( l(:general_pdf_encoding).upcase == "UTF-8" ? 80 : 60 ) | |
626 pdf.SetFontStyle('B',9) | |
627 pdf.RDMCell(35+155,5, l(:label_related_issues) + ":", "LTR") | |
628 pdf.Ln | |
629 relations.each do |relation| | |
630 buf = "" | |
631 buf += "#{l(relation.label_for(issue))} " | |
632 if relation.delay && relation.delay != 0 | |
633 buf += "(#{l('datetime.distance_in_words.x_days', :count => relation.delay)}) " | |
634 end | |
635 if Setting.cross_project_issue_relations? | |
636 buf += "#{relation.other_issue(issue).project} - " | |
637 end | |
638 buf += "#{relation.other_issue(issue).tracker}" + | |
639 " # #{relation.other_issue(issue).id}: #{relation.other_issue(issue).subject}" | |
640 buf = buf.truncate(truncate_length) | |
641 pdf.SetFontStyle('', 8) | |
642 pdf.RDMCell(35+155-60, 5, buf, "L") | |
643 pdf.SetFontStyle('B',8) | |
644 pdf.RDMCell(20,5, relation.other_issue(issue).status.to_s, "") | |
645 pdf.RDMCell(20,5, format_date(relation.other_issue(issue).start_date), "") | |
646 pdf.RDMCell(20,5, format_date(relation.other_issue(issue).due_date), "R") | |
647 pdf.Ln | |
648 end | |
649 end | |
650 pdf.RDMCell(190,5, "", "T") | |
651 pdf.Ln | |
652 | |
653 if issue.changesets.any? && | |
654 User.current.allowed_to?(:view_changesets, issue.project) | |
655 pdf.SetFontStyle('B',9) | |
656 pdf.RDMCell(190,5, l(:label_associated_revisions), "B") | |
657 pdf.Ln | |
658 for changeset in issue.changesets | |
659 pdf.SetFontStyle('B',8) | |
660 csstr = "#{l(:label_revision)} #{changeset.format_identifier} - " | |
661 csstr += format_time(changeset.committed_on) + " - " + changeset.author.to_s | |
662 pdf.RDMCell(190, 5, csstr) | |
663 pdf.Ln | |
664 unless changeset.comments.blank? | |
665 pdf.SetFontStyle('',8) | |
666 pdf.RDMwriteHTMLCell(190,5,0,0, | |
667 changeset.comments.to_s, issue.attachments, "") | |
668 end | |
669 pdf.Ln | |
670 end | |
671 end | |
672 | |
673 if assoc[:journals].present? | |
674 pdf.SetFontStyle('B',9) | |
675 pdf.RDMCell(190,5, l(:label_history), "B") | |
676 pdf.Ln | |
677 assoc[:journals].each do |journal| | |
678 pdf.SetFontStyle('B',8) | |
679 title = "##{journal.indice} - #{format_time(journal.created_on)} - #{journal.user}" | |
680 title << " (#{l(:field_private_notes)})" if journal.private_notes? | |
681 pdf.RDMCell(190,5, title) | |
682 pdf.Ln | |
683 pdf.SetFontStyle('I',8) | |
684 details_to_strings(journal.visible_details, true).each do |string| | |
685 pdf.RDMMultiCell(190,5, "- " + string) | |
686 end | |
687 if journal.notes? | |
688 pdf.Ln unless journal.details.empty? | |
689 pdf.SetFontStyle('',8) | |
690 pdf.RDMwriteHTMLCell(190,5,0,0, | |
691 journal.notes.to_s, issue.attachments, "") | |
692 end | |
693 pdf.Ln | |
694 end | |
695 end | |
696 | |
697 if issue.attachments.any? | |
698 pdf.SetFontStyle('B',9) | |
699 pdf.RDMCell(190,5, l(:label_attachment_plural), "B") | |
700 pdf.Ln | |
701 for attachment in issue.attachments | |
702 pdf.SetFontStyle('',8) | |
703 pdf.RDMCell(80,5, attachment.filename) | |
704 pdf.RDMCell(20,5, number_to_human_size(attachment.filesize),0,0,"R") | |
705 pdf.RDMCell(25,5, format_date(attachment.created_on),0,0,"R") | |
706 pdf.RDMCell(65,5, attachment.author.name,0,0,"R") | |
707 pdf.Ln | |
708 end | |
709 end | |
710 pdf.Output | |
711 end | |
712 | |
713 # Returns a PDF string of a set of wiki pages | |
714 def wiki_pages_to_pdf(pages, project) | |
715 pdf = ITCPDF.new(current_language) | |
716 pdf.SetTitle(project.name) | |
717 pdf.alias_nb_pages | |
718 pdf.footer_date = format_date(Date.today) | |
719 pdf.AddPage | |
720 pdf.SetFontStyle('B',11) | |
721 pdf.RDMMultiCell(190,5, project.name) | |
722 pdf.Ln | |
723 # Set resize image scale | |
724 pdf.SetImageScale(1.6) | |
725 pdf.SetFontStyle('',9) | |
726 write_page_hierarchy(pdf, pages.group_by(&:parent_id)) | |
727 pdf.Output | |
728 end | |
729 | |
730 # Returns a PDF string of a single wiki page | |
731 def wiki_page_to_pdf(page, project) | |
732 pdf = ITCPDF.new(current_language) | |
733 pdf.SetTitle("#{project} - #{page.title}") | |
734 pdf.alias_nb_pages | |
735 pdf.footer_date = format_date(Date.today) | |
736 pdf.AddPage | |
737 pdf.SetFontStyle('B',11) | |
738 pdf.RDMMultiCell(190,5, | |
739 "#{project} - #{page.title} - # #{page.content.version}") | |
740 pdf.Ln | |
741 # Set resize image scale | |
742 pdf.SetImageScale(1.6) | |
743 pdf.SetFontStyle('',9) | |
744 write_wiki_page(pdf, page) | |
745 pdf.Output | |
746 end | |
747 | |
748 def write_page_hierarchy(pdf, pages, node=nil, level=0) | |
749 if pages[node] | |
750 pages[node].each do |page| | |
751 if @new_page | |
752 pdf.AddPage | |
753 else | |
754 @new_page = true | |
755 end | |
756 pdf.Bookmark page.title, level | |
757 write_wiki_page(pdf, page) | |
758 write_page_hierarchy(pdf, pages, page.id, level + 1) if pages[page.id] | |
759 end | |
760 end | |
761 end | |
762 | |
763 def write_wiki_page(pdf, page) | |
764 pdf.RDMwriteHTMLCell(190,5,0,0, | |
765 page.content.text.to_s, page.attachments, 0) | |
766 if page.attachments.any? | |
767 pdf.Ln | |
768 pdf.SetFontStyle('B',9) | |
769 pdf.RDMCell(190,5, l(:label_attachment_plural), "B") | |
770 pdf.Ln | |
771 for attachment in page.attachments | |
772 pdf.SetFontStyle('',8) | |
773 pdf.RDMCell(80,5, attachment.filename) | |
774 pdf.RDMCell(20,5, number_to_human_size(attachment.filesize),0,0,"R") | |
775 pdf.RDMCell(25,5, format_date(attachment.created_on),0,0,"R") | |
776 pdf.RDMCell(65,5, attachment.author.name,0,0,"R") | |
777 pdf.Ln | |
778 end | |
779 end | |
780 end | |
781 | |
782 class RDMPdfEncoding | |
783 def self.rdm_from_utf8(txt, encoding) | |
784 txt ||= '' | |
785 txt = Redmine::CodesetUtil.from_utf8(txt, encoding) | |
786 if txt.respond_to?(:force_encoding) | |
787 txt.force_encoding('ASCII-8BIT') | |
788 end | |
789 txt | |
790 end | |
791 | |
792 def self.attach(attachments, filename, encoding) | |
793 filename_utf8 = Redmine::CodesetUtil.to_utf8(filename, encoding) | |
794 atta = nil | |
795 if filename_utf8 =~ /^[^\/"]+\.(gif|jpg|jpe|jpeg|png)$/i | |
796 atta = Attachment.latest_attach(attachments, filename_utf8) | |
797 end | |
798 if atta && atta.readable? && atta.visible? | |
799 return atta | |
800 else | |
801 return nil | |
802 end | |
803 end | |
804 end | |
805 end | |
806 end | |
807 end |