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