card_printer 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ MmM4OGE5NDVlMzk3MDI1M2UzZGExMDgxMWMxYTRiMmU0ZGQ3OWM3OQ==
5
+ data.tar.gz: !binary |-
6
+ YTllZGMwNGI0OWVjYWFmN2U0NTYyNzUwMGFhZmIxYWZlODhkYWNlZQ==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ NmYzOTk4NzAyNDk2NzBjNzIwN2VkNWY0YjZhMzYxMWNkYWVlYWI2ZDUzZjM4
10
+ Nzk3YjM4YzQzM2YwNWM1Zjk3ZDUyNTE1MWRiOWNmN2I4ZjNlNzFlZTc1ZmZi
11
+ MjA0ZTNiYjNkYThhMjgwOTQ1Y2M0MTg4Yzk1OTE5ZTNkYTdmZmE=
12
+ data.tar.gz: !binary |-
13
+ NWIzYTk1MWQxZTI5ZTQwZmIwNjAyYzZiNTExNWE3ZjEwNTVmNGRhN2QwNDZm
14
+ YzRkMTFhY2Y5YzEyZjBlNzk2ZDUxMzQwMGQyMjUwNWUxMGI0NmMyY2YxZGRh
15
+ MDFmYjQ1NGViNzdiYTk2OGY0ZmNiZmEzOTJjYjI2ZDdiNzM2ZjI=
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in card_printer.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 David Heath
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,30 @@
1
+ # CardPrinter
2
+
3
+ A general purpose library for generating PDFs which can be printed on 6x4
4
+ index cards.
5
+
6
+ ## Installation
7
+
8
+ Add this line to your application's Gemfile:
9
+
10
+ gem 'card_printer'
11
+
12
+ And then execute:
13
+
14
+ $ bundle
15
+
16
+ Or install it yourself as:
17
+
18
+ $ gem install card_printer
19
+
20
+ ## Usage
21
+
22
+ TODO: Write usage instructions here
23
+
24
+ ## Contributing
25
+
26
+ 1. Fork it
27
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
28
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
29
+ 4. Push to the branch (`git push origin my-new-feature`)
30
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/bin/card_printer ADDED
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'slop'
4
+ require 'card_printer'
5
+ require 'card_printer/story'
6
+ require 'json'
7
+
8
+ # stories << CardPrinter::Story.new(name: "Foobar")
9
+
10
+ # CardPrinter.render(stories, "my_stories.pdf")
11
+
12
+
13
+ class Cli
14
+ attr_reader :opts
15
+ class BadCliOptionError < StandardError; end
16
+
17
+ def initialize
18
+ @opts = Slop.parse(help: true) do
19
+ banner "Usage: #{File.basename(__FILE__)} [options] <output file.pdf>"
20
+ separator %{Options:}
21
+ on '-r=', 'renderer', %Q{Choose layout renderer (one of '#{CardPrinter.available_layouts.join("', '")}') }
22
+ on '-h=', %Q{display this help message}
23
+
24
+ separator %{
25
+ Data input:
26
+
27
+ Provide story data on standard input in json-lines format (one JSON object per
28
+ line).
29
+
30
+ Each json object represents a story with the following attributes:
31
+
32
+ * name (required)
33
+ * description
34
+ * label
35
+ * story_type - feature|bug|chore|other
36
+ * estimate
37
+
38
+ e.g.
39
+
40
+ {"name": "First story"}
41
+ {"name": "Second story", "description": "Do some things", "label": "epic one", "story_type": "feature", "estimate": "1"}
42
+ }
43
+ end
44
+
45
+ end
46
+
47
+ def renderer_name
48
+ opts[:renderer] || "6x4"
49
+ end
50
+
51
+ def run
52
+ if ARGV.size == 1
53
+ stories = []
54
+ $stdin.each_line do |line|
55
+ data = JSON.parse(line)
56
+ stories << CardPrinter::Story.new(data)
57
+ end
58
+
59
+ CardPrinter.render(stories, ARGV[0])
60
+ else
61
+ puts opts
62
+ end
63
+ end
64
+ end
65
+
66
+ Cli.new.run
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'card_printer/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "card_printer"
8
+ spec.version = CardPrinter::VERSION
9
+ spec.authors = ["David Heath"]
10
+ spec.email = ["david@davidheath.org"]
11
+ spec.description = %q{Produce nicely formatted PDFs of story cards for printing}
12
+ spec.summary = %q{}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "prawn", "~> 1.3.0"
22
+ spec.add_dependency "slop"
23
+ spec.add_development_dependency "bundler", "~> 1.3"
24
+ spec.add_development_dependency "rake"
25
+ end
Binary file
Binary file
@@ -0,0 +1,21 @@
1
+ require "card_printer/version"
2
+ require "card_printer/six_by_four_renderer"
3
+ require "card_printer/a6_renderer"
4
+ require "card_printer/renderer"
5
+
6
+ module CardPrinter
7
+ LAYOUTS = {
8
+ "6x4" => CardPrinter::SixByFourRenderer,
9
+ "a6" => CardPrinter::A6Renderer,
10
+ "a5" => CardPrinter::Renderer
11
+ }
12
+
13
+ def self.render(stories, destination_path, renderer_name = '6x4', opts = {})
14
+ renderer = LAYOUTS[renderer_name] || raise("Unknown layout renderer #{renderer_name}")
15
+ renderer.new(stories, opts).render_to(destination_path)
16
+ end
17
+
18
+ def self.available_layouts
19
+ LAYOUTS.keys
20
+ end
21
+ end
@@ -0,0 +1,196 @@
1
+ require 'card_printer'
2
+ require 'prawn'
3
+ require "prawn/measurement_extensions"
4
+ require 'ostruct'
5
+
6
+ class CardPrinter::A6Renderer
7
+ TITLE_SIZE = 12.mm
8
+ DESCRIPTION_SIZE = 7.mm
9
+
10
+ def self.description
11
+ "four a6 cards on one a4 sheet"
12
+ end
13
+
14
+ def initialize(stories, opts = {})
15
+ @stories = stories
16
+ @scale = 1
17
+ @cards_per_page = 4
18
+ end
19
+
20
+ def render_to(destination)
21
+ @pdf = Prawn::Document.new(:page_layout => :landscape,
22
+ :margin => 0,
23
+ :page_size => 'A4')
24
+
25
+
26
+ paginate_in_piles(@stories).each.with_index do |story_cluster, i|
27
+ @pdf.start_new_page unless i == 0
28
+ render_page(story_cluster)
29
+ end
30
+
31
+ @pdf.render_file destination
32
+ rescue Exception
33
+ puts "[!] There was an error while generating the PDF file... What happened was:"
34
+ raise
35
+ end
36
+
37
+ private
38
+
39
+ def paginate_in_piles(stories)
40
+ num_pages = (stories.size + 3) / @cards_per_page
41
+ first_pile, *other_piles = stories.each_slice(num_pages).to_a
42
+ first_pile.zip(*other_piles)
43
+ end
44
+
45
+ def paginate_lexicographically(stories)
46
+ @stories.each_slice(4).to_a
47
+ end
48
+
49
+ def render_page(story_cluster)
50
+ story_cluster.each_with_index do |story, index|
51
+ render_story(story, index) if story
52
+ end
53
+ end
54
+
55
+ def card_border_width
56
+ 2.mm
57
+ end
58
+
59
+ def vertical_space_between_cards
60
+ 1.mm
61
+ end
62
+
63
+ def paper_margin
64
+ 5.mm
65
+ end
66
+
67
+ def card_height
68
+ (@pdf.bounds.height / 2) - 2 * paper_margin - card_border_width
69
+ end
70
+
71
+ def card_width
72
+ (@pdf.bounds.width / 2) - 2 * paper_margin - card_border_width
73
+ end
74
+
75
+ def card_left_edge
76
+ @pdf.bounds.left + card_border_width / 2
77
+ end
78
+
79
+ def offsets_for_position(page_position)
80
+ x = page_position % 2
81
+ y = page_position / 2
82
+
83
+ x_offset = @pdf.bounds.left + paper_margin + card_border_width / 2
84
+ y_offset = @pdf.bounds.top - paper_margin - card_border_width / 2
85
+ x_offset += x * (card_width + paper_margin * 2 + card_border_width)
86
+ y_offset -= y * (card_height + paper_margin * 2 + card_border_width)
87
+ [x_offset, y_offset]
88
+ end
89
+
90
+ # page_position from 0-3
91
+ def render_story(story, page_position)
92
+ x_offset, y_offset = offsets_for_position(page_position)
93
+ @pdf.stroke_color = story_color(story)
94
+ @pdf.line_width = card_border_width
95
+
96
+ @pdf.stroke_rectangle [x_offset, y_offset], card_width, card_height
97
+ padding_x = card_border_width / 2 + 6.mm
98
+ padding_y = card_border_width / 2 + 6.mm
99
+
100
+ @pdf.bounding_box(
101
+ [x_offset + padding_x, y_offset - padding_y],
102
+ width: card_width - padding_x * 2,
103
+ height: card_height - padding_y * 2) do
104
+
105
+ render_crest
106
+ render_story_title(story)
107
+ render_story_points(story, padding_y)
108
+ end
109
+ end
110
+
111
+ def image_path(filename)
112
+ File.join(File.dirname(__FILE__), "..", "..", "images", filename)
113
+ end
114
+
115
+ def render_crest
116
+ crest_size = 20.mm * @scale
117
+ @pdf.image image_path('coat-of-arms.png'),
118
+ at: [(@pdf.bounds.right - crest_size)/2, crest_size - 5.mm],
119
+ fit: [crest_size, crest_size]
120
+ end
121
+
122
+ def render_story_title(story)
123
+ @pdf.fill_color "000000"
124
+ @pdf.text story.name, :size => TITLE_SIZE * @scale, :inline_format => true, :align => :center
125
+
126
+ end
127
+
128
+ def render_story_tags(story)
129
+ label_text = (story.label || "").strip
130
+ if ! label_text.empty?
131
+ @pdf.image image_path("label_icon.jpg"), at: [0, @pdf.cursor], fit: [6.mm * @scale, 6.mm * @scale]
132
+ @pdf.fill_color "52D017"
133
+ @pdf.text_box label_text, :size => 7.mm * @scale, at: [12.mm * @scale, @pdf.cursor]
134
+ @pdf.move_down 10.mm * @scale
135
+ end
136
+ end
137
+
138
+ def render_story_description(story)
139
+ text = story.description || ""
140
+ text = first_paragraph_of(text) if story.story_type == 'feature'
141
+ @pdf.fill_color "666666"
142
+ @pdf.move_down DESCRIPTION_SIZE
143
+ @pdf.font "Times-Roman" do
144
+ leading = count_lines(text) > 2 ? 1 : 6
145
+ @pdf.text_box text,
146
+ size: DESCRIPTION_SIZE * @scale,
147
+ inline_format: true,
148
+ at: [0, @pdf.cursor],
149
+ height: @pdf.cursor - 18.mm * @scale,
150
+ leading: leading * @scale,
151
+ overflow: :shrink_to_fit
152
+ end
153
+ end
154
+
155
+ def render_story_points(story, padding_y)
156
+ if story.story_type == 'feature'
157
+ @pdf.fill_color "000000"
158
+ @pdf.font "Helvetica", :style => :bold do
159
+ @pdf.text_box story_points(story),
160
+ :size => 12.mm * @scale,
161
+ :at => [0, padding_y + 12.mm * @scale],
162
+ :width => card_width - 15.mm * @scale,
163
+ valign: :bottom
164
+ end
165
+ end
166
+ end
167
+
168
+ def story_color(story)
169
+ case story.story_type
170
+ when "feature" then "85994b"
171
+ when "bug" then "b10e1e"
172
+ when "chore" then "b58840"
173
+ else "000000"
174
+ end
175
+ end
176
+
177
+ def story_points(story)
178
+ if story.respond_to?(:estimate) && !story.estimate.eql?(-1)
179
+ story.estimate.to_s
180
+ else
181
+ ""
182
+ end
183
+ end
184
+
185
+ def count_lines(text)
186
+ text.split("\n").size
187
+ end
188
+
189
+ def paragraphs(text)
190
+ (text || "").split(/(?:(?:\n|\r\n)[\r\t ]*){2,}/)
191
+ end
192
+
193
+ def first_paragraph_of(text)
194
+ paragraphs(text)[0] || ""
195
+ end
196
+ end
@@ -0,0 +1,178 @@
1
+ require "card_printer"
2
+ require 'prawn'
3
+ require "prawn/measurement_extensions"
4
+ require 'ostruct'
5
+
6
+ class CardPrinter::Renderer
7
+ TITLE_SIZE = 10.mm
8
+ DESCRIPTION_SIZE = 7.mm
9
+
10
+ def self.description
11
+ "two a5 cards on one a4 sheet"
12
+ end
13
+
14
+ def initialize(stories, opts = {})
15
+ @stories = stories
16
+ end
17
+
18
+ def render_to(destination)
19
+ @pdf = Prawn::Document.new(:page_layout => :portrait,
20
+ :margin => 10.mm,
21
+ :page_size => 'A4')
22
+
23
+ @stories.each_slice(2).with_index do |story_pair, i|
24
+ @pdf.start_new_page unless i == 0
25
+ render_page(story_pair)
26
+ end
27
+
28
+ @pdf.render_file destination
29
+ rescue Exception
30
+ puts "[!] There was an error while generating the PDF file... What happened was:"
31
+ raise
32
+ end
33
+
34
+ private
35
+
36
+ def render_page(story_pair)
37
+ offset = @pdf.bounds.top - card_border_width / 2
38
+ step_size = card_height + card_border_width + vertical_space_between_cards
39
+ story_pair.each_with_index do |story, index|
40
+ render_story(story, offset - index * step_size)
41
+ end
42
+ end
43
+
44
+ def card_border_width
45
+ 3.mm
46
+ end
47
+
48
+ def vertical_space_between_cards
49
+ 1.mm
50
+ end
51
+
52
+ def card_height
53
+ (@pdf.bounds.height - card_border_width * 2 - vertical_space_between_cards) / 2
54
+ end
55
+
56
+ def card_width
57
+ @pdf.bounds.width - card_border_width
58
+ end
59
+
60
+ def card_left_edge
61
+ @pdf.bounds.left + card_border_width / 2
62
+ end
63
+
64
+ def render_story(story, offset)
65
+ @pdf.stroke_color = story_color(story)
66
+ @pdf.line_width = card_border_width
67
+
68
+ @pdf.stroke_rectangle [card_left_edge, offset], card_width, card_height
69
+ padding_x = card_border_width / 2 + 6.mm
70
+ padding_y = card_border_width / 2 + 6.mm
71
+
72
+ @pdf.bounding_box(
73
+ [card_left_edge + padding_x, offset - padding_y],
74
+ width: card_width - padding_x * 2,
75
+ height: card_height - padding_y * 2) do
76
+
77
+ render_crest
78
+ render_story_title(story)
79
+ render_story_description(story)
80
+ render_story_points(story, padding_y)
81
+ render_story_type(story, padding_y)
82
+ end
83
+ end
84
+
85
+ def image_path(filename)
86
+ File.join(File.dirname(__FILE__), "..", "..", "images", filename)
87
+ end
88
+
89
+ def render_crest
90
+ crest_size = 20.mm
91
+ @pdf.image image_path('coat-of-arms.png'),
92
+ at: [(@pdf.bounds.right - crest_size)/2, crest_size - 5.mm],
93
+ fit: [crest_size, crest_size]
94
+ end
95
+
96
+ def render_story_title(story)
97
+ @pdf.fill_color "000000"
98
+ @pdf.text story.name, :size => TITLE_SIZE, :inline_format => true, :align => :center
99
+
100
+ end
101
+
102
+ def render_story_tags(story)
103
+ label_text = (story.label || "").strip
104
+ if ! label_text.empty?
105
+ @pdf.image image_path("label_icon.jpg"), at: [0, @pdf.cursor], fit: [6.mm, 6.mm]
106
+ @pdf.fill_color "52D017"
107
+ @pdf.text_box label_text, :size => 7.mm, at: [12.mm, @pdf.cursor]
108
+ @pdf.move_down 10.mm
109
+ end
110
+ end
111
+
112
+ def render_story_description(story)
113
+ text = story.description || ""
114
+ text = first_paragraph_of(text) if story.story_type == 'feature'
115
+ @pdf.fill_color "666666"
116
+ @pdf.move_down DESCRIPTION_SIZE
117
+ @pdf.font "Times-Roman" do
118
+ leading = count_lines(text) > 2 ? 1 : 6
119
+ @pdf.text_box text,
120
+ size: DESCRIPTION_SIZE,
121
+ inline_format: true,
122
+ at: [0, @pdf.cursor],
123
+ height: @pdf.cursor - 18.mm,
124
+ leading: leading,
125
+ overflow: :shrink_to_fit
126
+ end
127
+ end
128
+
129
+ def render_story_points(story, padding_y)
130
+ if story.story_type == 'feature'
131
+ @pdf.fill_color "000000"
132
+ @pdf.text_box "Points: #{story_points(story)}",
133
+ :size => 12.mm,
134
+ :at => [0, padding_y + 12.mm],
135
+ :width => card_width - 15.mm,
136
+ valign: :bottom
137
+ end
138
+ end
139
+
140
+ def render_story_type(story, padding_y)
141
+ @pdf.fill_color "aaaaaa"
142
+ @pdf.text_box story.story_type.capitalize,
143
+ :size => 12.mm,
144
+ :align => :right,
145
+ :at => [@pdf.bounds.right - 80.mm, padding_y + 12.mm],
146
+ :width => 80.mm,
147
+ valign: :bottom
148
+ end
149
+
150
+ def story_color(story)
151
+ case story.story_type
152
+ when "feature" then "85994b"
153
+ when "bug" then "b10e1e"
154
+ when "chore" then "b58840"
155
+ else "000000"
156
+ end
157
+ end
158
+
159
+ def story_points(story)
160
+ if story.respond_to?(:estimate) && !story.estimate.eql?(-1)
161
+ story.estimate.to_s
162
+ else
163
+ ""
164
+ end
165
+ end
166
+
167
+ def count_lines(text)
168
+ text.split("\n").size
169
+ end
170
+
171
+ def paragraphs(text)
172
+ (text || "").split(/(?:(?:\n|\r\n)[\r\t ]*){2,}/)
173
+ end
174
+
175
+ def first_paragraph_of(text)
176
+ paragraphs(text)[0] || ""
177
+ end
178
+ end
@@ -0,0 +1,189 @@
1
+ require "card_printer"
2
+ require 'prawn'
3
+ require "prawn/measurement_extensions"
4
+ require 'ostruct'
5
+
6
+ class CardPrinter::SixByFourRenderer
7
+ TITLE_SIZE = 12.mm
8
+ DESCRIPTION_SIZE = 7.mm
9
+
10
+ def self.description
11
+ "6x4 record cards (use manual feed)"
12
+ end
13
+
14
+ def initialize(stories, opts = {})
15
+ @stories = stories
16
+ @scale = 1
17
+ @opts = opts
18
+ @iteration_number = opts[:iteration_number]
19
+ end
20
+
21
+ def render_to(destination)
22
+ @pdf = Prawn::Document.new(:page_layout => :landscape,
23
+ :margin => 0,
24
+ :page_size => [102.mm, 152.mm])
25
+
26
+
27
+ @stories.each.with_index do |story, i|
28
+ @pdf.start_new_page unless i == 0
29
+ render_story(story)
30
+ end
31
+
32
+ @pdf.render_file destination
33
+ rescue Exception
34
+ puts "[!] There was an error while generating the PDF file... What happened was:"
35
+ raise
36
+ end
37
+
38
+ private
39
+
40
+ def card_border_width
41
+ 2.mm
42
+ end
43
+
44
+ def paper_margin
45
+ 5.mm
46
+ end
47
+
48
+ def card_height
49
+ @pdf.bounds.height - paper_margin * 2 - card_border_width
50
+ end
51
+
52
+ def card_width
53
+ @pdf.bounds.width - paper_margin * 2 - card_border_width
54
+ end
55
+
56
+ def card_left_edge
57
+ @pdf.bounds.left + paper_margin + card_border_width / 2
58
+ end
59
+
60
+ def offsets_for_position(page_position)
61
+ x = page_position % 2
62
+ y = page_position / 2
63
+
64
+ x_offset = @pdf.bounds.left + paper_margin + card_border_width / 2
65
+ y_offset = @pdf.bounds.top - paper_margin - card_border_width / 2
66
+ x_offset += x * (card_width + paper_margin * 2 + card_border_width)
67
+ y_offset -= y * (card_height + paper_margin * 2 + card_border_width)
68
+ [x_offset, y_offset]
69
+ end
70
+
71
+ # page_position from 0-3
72
+ def render_story(story)
73
+ x_offset, y_offset = offsets_for_position(0)
74
+ @pdf.stroke_color = story_color(story)
75
+ @pdf.line_width = card_border_width
76
+
77
+ @pdf.stroke_rectangle [x_offset, y_offset], card_width, card_height
78
+ padding_x = card_border_width / 2 + 6.mm
79
+ padding_y = card_border_width / 2 + 6.mm
80
+
81
+ @pdf.bounding_box(
82
+ [x_offset + padding_x, y_offset - padding_y],
83
+ width: card_width - padding_x * 2,
84
+ height: card_height - padding_y * 2) do
85
+
86
+ render_crest
87
+ render_story_title(story)
88
+ render_story_points(story, padding_y)
89
+ end
90
+ render_iteration_number(story)
91
+ end
92
+
93
+ def image_path(filename)
94
+ File.join(File.dirname(__FILE__), "..", "..", "images", filename)
95
+ end
96
+
97
+ def render_crest
98
+ crest_size = 20.mm * @scale
99
+ @pdf.image image_path('coat-of-arms.png'),
100
+ at: [(@pdf.bounds.right - crest_size)/2, crest_size - 5.mm],
101
+ fit: [crest_size, crest_size]
102
+ end
103
+
104
+ def render_story_title(story)
105
+ @pdf.fill_color "000000"
106
+ @pdf.text story.name, :size => TITLE_SIZE * @scale, :inline_format => true, :align => :center
107
+ end
108
+
109
+ def render_story_tags(story)
110
+ label_text = (story.label || "").strip
111
+ if ! label_text.empty?
112
+ @pdf.image image_path("label_icon.jpg"), at: [0, @pdf.cursor], fit: [6.mm * @scale, 6.mm * @scale]
113
+ @pdf.fill_color "52D017"
114
+ @pdf.text_box label_text, :size => 7.mm * @scale, at: [12.mm * @scale, @pdf.cursor]
115
+ @pdf.move_down 10.mm * @scale
116
+ end
117
+ end
118
+
119
+ def render_story_description(story)
120
+ text = story.description || ""
121
+ text = first_paragraph_of(text) if story.story_type == 'feature'
122
+ @pdf.fill_color "666666"
123
+ @pdf.move_down DESCRIPTION_SIZE
124
+ @pdf.font "Times-Roman" do
125
+ leading = count_lines(text) > 2 ? 1 : 6
126
+ @pdf.text_box text,
127
+ size: DESCRIPTION_SIZE * @scale,
128
+ inline_format: true,
129
+ at: [0, @pdf.cursor],
130
+ height: @pdf.cursor - 18.mm * @scale,
131
+ leading: leading * @scale,
132
+ overflow: :shrink_to_fit
133
+ end
134
+ end
135
+
136
+ def render_story_points(story, padding_y)
137
+ if story.story_type == 'feature'
138
+ @pdf.fill_color "000000"
139
+ @pdf.font "Helvetica", :style => :bold do
140
+ @pdf.text_box story_points(story),
141
+ :size => 12.mm * @scale,
142
+ :at => [0, padding_y + 12.mm * @scale],
143
+ :width => card_width - 15.mm * @scale,
144
+ valign: :bottom
145
+ end
146
+ end
147
+ end
148
+
149
+ def render_iteration_number(story)
150
+ @pdf.fill_color "000000"
151
+ text_height = 6.mm * @scale
152
+ @pdf.text_box @iteration_number.to_s,
153
+ :size => text_height,
154
+ :at => [@pdf.bounds.right - paper_margin - card_border_width - 10.mm,
155
+ @pdf.bounds.bottom + paper_margin + card_border_width + text_height + 1.mm],
156
+ :width => 15.mm,
157
+ valign: :top,
158
+ alighn: :right
159
+ end
160
+
161
+ def story_color(story)
162
+ case story.story_type
163
+ when "feature" then "85994b"
164
+ when "bug" then "b10e1e"
165
+ when "chore" then "b58840"
166
+ else "000000"
167
+ end
168
+ end
169
+
170
+ def story_points(story)
171
+ if story.respond_to?(:estimate) && !story.estimate.eql?(-1)
172
+ story.estimate.to_s
173
+ else
174
+ ""
175
+ end
176
+ end
177
+
178
+ def count_lines(text)
179
+ text.split("\n").size
180
+ end
181
+
182
+ def paragraphs(text)
183
+ (text || "").split(/(?:(?:\n|\r\n)[\r\t ]*){2,}/)
184
+ end
185
+
186
+ def first_paragraph_of(text)
187
+ paragraphs(text)[0] || ""
188
+ end
189
+ end
@@ -0,0 +1,14 @@
1
+ require "card_printer"
2
+
3
+ class CardPrinter::Story
4
+ # story_type = {feature|bug|chore|other}
5
+ ATTRS = [:name, :description, :story_type, :estimate, :label]
6
+
7
+ attr_accessor *ATTRS
8
+
9
+ def initialize(data = {})
10
+ ATTRS.each do |attr_name|
11
+ self.send(:"#{attr_name}=", data[attr_name] || data[attr_name.to_s])
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,3 @@
1
+ module CardPrinter
2
+ VERSION = "0.0.1"
3
+ end
metadata ADDED
@@ -0,0 +1,116 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: card_printer
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - David Heath
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-03-19 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: prawn
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: 1.3.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: 1.3.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: slop
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ! '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ! '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: '1.3'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: '1.3'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ! '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: Produce nicely formatted PDFs of story cards for printing
70
+ email:
71
+ - david@davidheath.org
72
+ executables:
73
+ - card_printer
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - .gitignore
78
+ - Gemfile
79
+ - LICENSE.txt
80
+ - README.md
81
+ - Rakefile
82
+ - bin/card_printer
83
+ - card_printer.gemspec
84
+ - images/coat-of-arms.png
85
+ - images/label_icon.jpg
86
+ - lib/card_printer.rb
87
+ - lib/card_printer/a6_renderer.rb
88
+ - lib/card_printer/renderer.rb
89
+ - lib/card_printer/six_by_four_renderer.rb
90
+ - lib/card_printer/story.rb
91
+ - lib/card_printer/version.rb
92
+ homepage: ''
93
+ licenses:
94
+ - MIT
95
+ metadata: {}
96
+ post_install_message:
97
+ rdoc_options: []
98
+ require_paths:
99
+ - lib
100
+ required_ruby_version: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ! '>='
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ required_rubygems_version: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ requirements: []
111
+ rubyforge_project:
112
+ rubygems_version: 2.1.0
113
+ signing_key:
114
+ specification_version: 4
115
+ summary: ''
116
+ test_files: []