pdf_tempura 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|