pdf_tempura 0.0.2
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 +15 -0
- data/.gitignore +18 -0
- data/.rspec +2 -0
- data/.travis.yml +5 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +342 -0
- data/Rakefile +1 -0
- data/example/my_pdf.rb +37 -0
- data/lib/pdf_tempura.rb +8 -0
- data/lib/pdf_tempura/document.rb +121 -0
- data/lib/pdf_tempura/document/boxed_characters.rb +70 -0
- data/lib/pdf_tempura/document/boxed_characters/groups.rb +53 -0
- data/lib/pdf_tempura/document/character_field.rb +37 -0
- data/lib/pdf_tempura/document/checkbox_field.rb +15 -0
- data/lib/pdf_tempura/document/default_commands.rb +40 -0
- data/lib/pdf_tempura/document/field/base.rb +55 -0
- data/lib/pdf_tempura/document/field_set.rb +51 -0
- data/lib/pdf_tempura/document/page.rb +33 -0
- data/lib/pdf_tempura/document/table.rb +95 -0
- data/lib/pdf_tempura/document/table/boxed_character_column.rb +20 -0
- data/lib/pdf_tempura/document/table/checkbox_column.rb +9 -0
- data/lib/pdf_tempura/document/table/column.rb +18 -0
- data/lib/pdf_tempura/document/table/spacer.rb +13 -0
- data/lib/pdf_tempura/document/table/text_column.rb +9 -0
- data/lib/pdf_tempura/document/text_field.rb +18 -0
- data/lib/pdf_tempura/document/validation.rb +79 -0
- data/lib/pdf_tempura/extensions/hash/stringify_keys.rb +35 -0
- data/lib/pdf_tempura/render.rb +18 -0
- data/lib/pdf_tempura/render/boxed_characters.rb +42 -0
- data/lib/pdf_tempura/render/character_field.rb +49 -0
- data/lib/pdf_tempura/render/checkbox_field.rb +35 -0
- data/lib/pdf_tempura/render/debug.rb +16 -0
- data/lib/pdf_tempura/render/debug/annotation/base.rb +83 -0
- data/lib/pdf_tempura/render/debug/character_field_annotation.rb +13 -0
- data/lib/pdf_tempura/render/debug/checkbox_field_annotation.rb +24 -0
- data/lib/pdf_tempura/render/debug/field_set_annotation.rb +23 -0
- data/lib/pdf_tempura/render/debug/grid.rb +59 -0
- data/lib/pdf_tempura/render/debug/outside_annotation.rb +42 -0
- data/lib/pdf_tempura/render/debug/table_annotation.rb +19 -0
- data/lib/pdf_tempura/render/debug/text_field_annotation.rb +42 -0
- data/lib/pdf_tempura/render/field.rb +40 -0
- data/lib/pdf_tempura/render/field_bounds.rb +26 -0
- data/lib/pdf_tempura/render/field_data_mapper.rb +15 -0
- data/lib/pdf_tempura/render/field_set.rb +31 -0
- data/lib/pdf_tempura/render/option_access.rb +21 -0
- data/lib/pdf_tempura/render/page.rb +23 -0
- data/lib/pdf_tempura/render/table.rb +35 -0
- data/lib/pdf_tempura/render/text_field.rb +13 -0
- data/lib/pdf_tempura/renderer.rb +39 -0
- data/lib/pdf_tempura/version.rb +3 -0
- data/pdf_tempura.gemspec +27 -0
- data/spec/assets/sample_pdf_form.odg +0 -0
- data/spec/assets/sample_pdf_form.pdf +0 -0
- data/spec/integration_spec.rb +88 -0
- data/spec/lib/pdf_tempura/document/boxed_characters_spec.rb +125 -0
- data/spec/lib/pdf_tempura/document/checkbox_field_spec.rb +54 -0
- data/spec/lib/pdf_tempura/document/field_common.rb +12 -0
- data/spec/lib/pdf_tempura/document/field_set_spec.rb +38 -0
- data/spec/lib/pdf_tempura/document/page_spec.rb +57 -0
- data/spec/lib/pdf_tempura/document/table_spec.rb +161 -0
- data/spec/lib/pdf_tempura/document/text_field_spec.rb +195 -0
- data/spec/lib/pdf_tempura/document_spec.rb +131 -0
- data/spec/lib/pdf_tempura/extensions/hash/stringify_keys_spec.rb +42 -0
- data/spec/lib/pdf_tempura/render/boxed_characters_spec.rb +68 -0
- data/spec/lib/pdf_tempura/render/checkbox_field_spec.rb +39 -0
- data/spec/lib/pdf_tempura/render/debug/annotation_renderer/base_spec.rb +45 -0
- data/spec/lib/pdf_tempura/render/debug/checkbox_field_annotation_spec.rb +45 -0
- data/spec/lib/pdf_tempura/render/debug/grid_spec.rb +15 -0
- data/spec/lib/pdf_tempura/render/debug/text_field_annotation_spec.rb +46 -0
- data/spec/lib/pdf_tempura/render/field_data_mapper_spec.rb +31 -0
- data/spec/lib/pdf_tempura/render/field_set_spec.rb +41 -0
- data/spec/lib/pdf_tempura/render/field_spec.rb +37 -0
- data/spec/lib/pdf_tempura/render/page_spec.rb +77 -0
- data/spec/lib/pdf_tempura/render/table_spec.rb +44 -0
- data/spec/lib/pdf_tempura/render/text_field_spec.rb +39 -0
- data/spec/lib/pdf_tempura/renderer_spec.rb +79 -0
- data/spec/spec_helper.rb +29 -0
- data/spec/support/shared_examples/field_examples.rb +265 -0
- metadata +219 -0
data/lib/pdf_tempura.rb
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
module PdfTempura
|
2
|
+
class Document
|
3
|
+
|
4
|
+
class << self
|
5
|
+
|
6
|
+
def template_file_path
|
7
|
+
@template_file_path
|
8
|
+
end
|
9
|
+
|
10
|
+
def template(path)
|
11
|
+
raise ArgumentError, "Template path must be a string." unless path.is_a?(String)
|
12
|
+
@template_file_path = path
|
13
|
+
end
|
14
|
+
|
15
|
+
def pages
|
16
|
+
@pages ||= []
|
17
|
+
end
|
18
|
+
|
19
|
+
def repeatable
|
20
|
+
@repeatable = true
|
21
|
+
end
|
22
|
+
|
23
|
+
def repeatable_option
|
24
|
+
@repeatable || false
|
25
|
+
end
|
26
|
+
|
27
|
+
def page(page_number, &block)
|
28
|
+
page = Page.new(page_number)
|
29
|
+
pages << page
|
30
|
+
page.instance_eval(&block)
|
31
|
+
nil
|
32
|
+
end
|
33
|
+
|
34
|
+
def debug_options
|
35
|
+
@debug_options ||= []
|
36
|
+
end
|
37
|
+
|
38
|
+
def debug(*options)
|
39
|
+
debug_options.concat options
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
def initialize(data = {})
|
45
|
+
load_data(data)
|
46
|
+
end
|
47
|
+
|
48
|
+
def pages
|
49
|
+
@pages ||= []
|
50
|
+
end
|
51
|
+
|
52
|
+
def render(&block)
|
53
|
+
new_renderer.render(&block)
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def repeatable
|
59
|
+
self.class.repeatable_option
|
60
|
+
end
|
61
|
+
|
62
|
+
def new_renderer
|
63
|
+
PdfTempura::Renderer.new(
|
64
|
+
self.class.template_file_path,
|
65
|
+
self.pages,
|
66
|
+
{
|
67
|
+
debug: self.class.debug_options,
|
68
|
+
repeatable: self.class.repeatable_option,
|
69
|
+
template_page_count: class_pages.count
|
70
|
+
}
|
71
|
+
)
|
72
|
+
end
|
73
|
+
|
74
|
+
def generate_pages_from_data(data)
|
75
|
+
data.each_with_index do |page_data,number|
|
76
|
+
page = class_pages[number % class_pages.count ]
|
77
|
+
|
78
|
+
self.pages << page.dup.tap{ |new_page|
|
79
|
+
new_page.data = page_data || {}
|
80
|
+
}
|
81
|
+
end
|
82
|
+
|
83
|
+
generate_missing_pages
|
84
|
+
end
|
85
|
+
|
86
|
+
def class_pages
|
87
|
+
self.class.pages
|
88
|
+
end
|
89
|
+
|
90
|
+
def generate_missing_pages
|
91
|
+
if pages.count % class_pages.count != 0
|
92
|
+
[(pages.count % class_pages.count ) .. class_pages.count].each do |number|
|
93
|
+
page = class_pages[number]
|
94
|
+
self.pages << page.dup
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def load_data(data)
|
100
|
+
if !repeatable && data.keys.count > self.class.pages.count
|
101
|
+
raise ArgumentError.new("There are more pages in the data than pages defined. Use 'repeatable' to repeat template pages in the document class.")
|
102
|
+
end
|
103
|
+
|
104
|
+
keys = data.keys.select{|key| key.kind_of?(Numeric) || key.to_i.to_s == key.to_s}.sort {|key1,key2| key1.to_i <=> key2.to_i}
|
105
|
+
data_for_pages = data.values_at(*keys)
|
106
|
+
generate_pages_from_data(data_for_pages)
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
require_relative 'document/validation'
|
113
|
+
require_relative 'document/default_commands'
|
114
|
+
require_relative 'document/page'
|
115
|
+
require_relative 'document/field/base'
|
116
|
+
require_relative 'document/character_field'
|
117
|
+
require_relative 'document/text_field'
|
118
|
+
require_relative 'document/checkbox_field'
|
119
|
+
require_relative 'document/table'
|
120
|
+
require_relative 'document/boxed_characters'
|
121
|
+
require_relative 'document/field_set'
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module PdfTempura
|
2
|
+
class Document::BoxedCharacters < Document::Field::Base
|
3
|
+
|
4
|
+
attr_reader :box_width, :box_spacing, :groups, :truncate, :text_options, :padding
|
5
|
+
attr_accessor :coordinates
|
6
|
+
alias :truncate? :truncate
|
7
|
+
|
8
|
+
validates :box_width, required: true, type: Numeric
|
9
|
+
validates :box_spacing, required: true, type: Numeric
|
10
|
+
validates :truncate, boolean: true
|
11
|
+
|
12
|
+
def initialize(name, coordinates, height, options = {}, &block)
|
13
|
+
@groups = []
|
14
|
+
|
15
|
+
super name, coordinates, [0, height], options
|
16
|
+
|
17
|
+
instance_eval(&block) if block_given?
|
18
|
+
end
|
19
|
+
|
20
|
+
def characters(characters)
|
21
|
+
@groups << CharacterGroup.new(characters)
|
22
|
+
end
|
23
|
+
|
24
|
+
def space(width)
|
25
|
+
@groups << SpaceGroup.new(width)
|
26
|
+
end
|
27
|
+
|
28
|
+
def supported_characters
|
29
|
+
@groups.map(&:characters).inject(:+)
|
30
|
+
end
|
31
|
+
|
32
|
+
def fields
|
33
|
+
@fields ||= generate_text_fields
|
34
|
+
end
|
35
|
+
|
36
|
+
def width
|
37
|
+
groups.inject(0){ |sum,group| sum + group.width(box_width, box_spacing) }
|
38
|
+
end
|
39
|
+
|
40
|
+
def dimensions
|
41
|
+
[width, @dimensions[1]]
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def generate_text_fields
|
47
|
+
[].tap do |fields|
|
48
|
+
groups.inject(self.x) do |x, group|
|
49
|
+
group.each_supported_character do
|
50
|
+
fields << Document::CharacterField.new(name, [x,y], [box_width,height], text_options)
|
51
|
+
x+= box_width + box_spacing
|
52
|
+
end
|
53
|
+
|
54
|
+
x + group.spacing - (group.characters > 0 ? box_spacing : 0)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def load_options(options)
|
60
|
+
@box_width = options["box_width"]
|
61
|
+
@box_spacing = options["box_spacing"]
|
62
|
+
@truncate = options["truncate"] || false
|
63
|
+
@text_options = options.reject { |key,v| ["box_width", "box_spacing", "truncate"].include?(key) }
|
64
|
+
@padding = [0,0,0,0]
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
require_relative 'boxed_characters/groups'
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module PdfTempura
|
2
|
+
class Document::BoxedCharacters::CharacterGroup
|
3
|
+
include Document::Validation
|
4
|
+
|
5
|
+
attr_reader :characters
|
6
|
+
|
7
|
+
validates :characters, type: Numeric
|
8
|
+
|
9
|
+
def initialize(characters)
|
10
|
+
@characters = characters
|
11
|
+
validate!
|
12
|
+
end
|
13
|
+
|
14
|
+
def spacing
|
15
|
+
0
|
16
|
+
end
|
17
|
+
|
18
|
+
def each_supported_character(&block)
|
19
|
+
characters.times{ yield }
|
20
|
+
end
|
21
|
+
|
22
|
+
def width(box_width, spacing)
|
23
|
+
box_width * characters + spacing * (characters - 1)
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
class Document::BoxedCharacters::SpaceGroup
|
29
|
+
include Document::Validation
|
30
|
+
|
31
|
+
attr_reader :spacing
|
32
|
+
|
33
|
+
validates :spacing, type: Numeric
|
34
|
+
|
35
|
+
def initialize(units)
|
36
|
+
@spacing = units
|
37
|
+
validate!
|
38
|
+
end
|
39
|
+
|
40
|
+
def characters
|
41
|
+
0
|
42
|
+
end
|
43
|
+
|
44
|
+
def each_supported_character(&block)
|
45
|
+
#we support no characters, nein!
|
46
|
+
end
|
47
|
+
|
48
|
+
def width(ignored,ignored2)
|
49
|
+
spacing
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module PdfTempura
|
2
|
+
class Document::CharacterField < Document::Field::Base
|
3
|
+
|
4
|
+
attr_reader :default_value, :font_name, :font_size, :bold, :italic, :padding,
|
5
|
+
:alignment, :multi_line, :valign, :leading
|
6
|
+
|
7
|
+
alias_method :bold?, :bold
|
8
|
+
alias_method :italic?, :italic
|
9
|
+
alias_method :multi_line?, :multi_line
|
10
|
+
|
11
|
+
validates :font_name, type: String
|
12
|
+
validates :font_size, type: Numeric
|
13
|
+
validates :bold, inclusion: [true, false]
|
14
|
+
validates :italic, inclusion: [true, false]
|
15
|
+
validates :alignment, inclusion: ["left", "right", "center"]
|
16
|
+
validates :multi_line, inclusion: [true, false]
|
17
|
+
validates :padding, type: Array, inner_type: Numeric, count: 4
|
18
|
+
validates :default_value, type: String
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def load_options(options)
|
23
|
+
@default_value = options["default_value"] || ""
|
24
|
+
@font_name = options["font_name"] || "Helvetica"
|
25
|
+
@font_size = options["font_size"] || 10
|
26
|
+
@bold = options["bold"] || false
|
27
|
+
@italic = options["italic"] || false
|
28
|
+
@bold = options["bold"] || false
|
29
|
+
@padding = options["padding"] || [0,0,0,0]
|
30
|
+
@alignment = "center"
|
31
|
+
@multi_line = false
|
32
|
+
@valign = "center"
|
33
|
+
@leader = 0
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module PdfTempura
|
2
|
+
class Document::CheckboxField < Document::Field::Base
|
3
|
+
|
4
|
+
attr_reader :default_value, :padding
|
5
|
+
|
6
|
+
validates :default_value, inclusion: [true, false]
|
7
|
+
validates :padding, type: Array, inner_type: Numeric, count: 4
|
8
|
+
|
9
|
+
def load_options(options)
|
10
|
+
@default_value = options["default_value"] || false
|
11
|
+
@padding = options["padding"] || [1,1,1,1]
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module PdfTempura
|
2
|
+
module Document::DefaultCommands
|
3
|
+
|
4
|
+
def self.included(base)
|
5
|
+
base.send(:attr_accessor, :fields)
|
6
|
+
end
|
7
|
+
|
8
|
+
def text_field(name, coordinates, dimensions, options = {})
|
9
|
+
fields << Document::TextField.new(name, coordinates, dimensions, @options.merge(options))
|
10
|
+
end
|
11
|
+
|
12
|
+
def checkbox_field(name, coordinates, dimensions, options = {})
|
13
|
+
fields << Document::CheckboxField.new(name, coordinates, dimensions, @options.merge(options))
|
14
|
+
end
|
15
|
+
|
16
|
+
def table(name, coordinates, options = {}, &block)
|
17
|
+
fields << Document::Table.new(name, coordinates, @options.merge(options), &block)
|
18
|
+
end
|
19
|
+
|
20
|
+
def boxed_characters(name, coordinates, height, options = {}, &block)
|
21
|
+
fields << Document::BoxedCharacters.new(name, coordinates, height, @options.merge(options), &block)
|
22
|
+
end
|
23
|
+
|
24
|
+
def field_set(name, options = {}, &block)
|
25
|
+
fields << Document::FieldSet.new(name, @options.merge(options), &block)
|
26
|
+
end
|
27
|
+
|
28
|
+
def with_default_options(options = {}, &block)
|
29
|
+
previous_options = @options
|
30
|
+
|
31
|
+
begin
|
32
|
+
@options = @options.merge(options)
|
33
|
+
instance_eval(&block)
|
34
|
+
ensure
|
35
|
+
@options = previous_options
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module PdfTempura
|
2
|
+
module Document::Field
|
3
|
+
class Base
|
4
|
+
include Document::Validation
|
5
|
+
|
6
|
+
def initialize(name, coordinates, dimensions, options = {})
|
7
|
+
@name = name.is_a?(Symbol) ? name.to_s : name
|
8
|
+
@coordinates = coordinates
|
9
|
+
@dimensions = dimensions
|
10
|
+
|
11
|
+
convert_options_hash(options)
|
12
|
+
load_options(options)
|
13
|
+
|
14
|
+
validate!
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :name, :coordinates, :dimensions
|
18
|
+
|
19
|
+
validates :name, type: String
|
20
|
+
validates :coordinates, type: Array, inner_type: Numeric, count: 2
|
21
|
+
validates :dimensions, type: Array, inner_type: Numeric, count: 2
|
22
|
+
|
23
|
+
def x
|
24
|
+
coordinates.first
|
25
|
+
end
|
26
|
+
|
27
|
+
def y
|
28
|
+
coordinates.last
|
29
|
+
end
|
30
|
+
|
31
|
+
def width
|
32
|
+
dimensions.first
|
33
|
+
end
|
34
|
+
|
35
|
+
def height
|
36
|
+
dimensions.last
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def load_options(options)
|
42
|
+
raise NotImplementedError, "Implement 'load_options' in your subclass."
|
43
|
+
end
|
44
|
+
|
45
|
+
def convert_options_hash(options)
|
46
|
+
if options.is_a?(Hash)
|
47
|
+
options.extend(Extensions::Hash::StringifyKeys).stringify_keys!
|
48
|
+
else
|
49
|
+
raise ArgumentError, "Options must be a hash."
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module PdfTempura
|
2
|
+
class Document::FieldSet
|
3
|
+
include Document::Validation
|
4
|
+
include Document::DefaultCommands
|
5
|
+
|
6
|
+
attr_reader :name
|
7
|
+
|
8
|
+
validates :name, type: String
|
9
|
+
|
10
|
+
def initialize(name, options = {}, &block)
|
11
|
+
@name = name
|
12
|
+
@fields = []
|
13
|
+
@options = options
|
14
|
+
|
15
|
+
instance_eval(&block) if block_given?
|
16
|
+
validate!
|
17
|
+
end
|
18
|
+
|
19
|
+
def coordinates
|
20
|
+
[x,y]
|
21
|
+
end
|
22
|
+
|
23
|
+
def dimensions
|
24
|
+
[width,height]
|
25
|
+
end
|
26
|
+
|
27
|
+
def x
|
28
|
+
fields.map(&:x).min || 0
|
29
|
+
end
|
30
|
+
|
31
|
+
def y
|
32
|
+
fields.map(&:y).max || 0
|
33
|
+
end
|
34
|
+
|
35
|
+
def width
|
36
|
+
return 0 if fields.empty?
|
37
|
+
fields.map{ |field| field.x + field.width }.max - x
|
38
|
+
end
|
39
|
+
|
40
|
+
def height
|
41
|
+
return 0 if fields.empty?
|
42
|
+
y - fields.map{ |field| field.y - field.height }.min
|
43
|
+
end
|
44
|
+
|
45
|
+
def padding
|
46
|
+
[0,0,0,0]
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|