card_printer 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []