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