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
@@ -0,0 +1,33 @@
|
|
1
|
+
module PdfTempura
|
2
|
+
class Document::Page
|
3
|
+
include Document::Validation
|
4
|
+
include Document::DefaultCommands
|
5
|
+
|
6
|
+
def initialize(page_number, options = {})
|
7
|
+
@number = page_number
|
8
|
+
@fields = []
|
9
|
+
@options = options
|
10
|
+
|
11
|
+
validate!
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_reader :number
|
15
|
+
|
16
|
+
validates :number, type: Numeric
|
17
|
+
|
18
|
+
def ==(other)
|
19
|
+
self.number == other.number
|
20
|
+
end
|
21
|
+
|
22
|
+
def data
|
23
|
+
@data ||= {}
|
24
|
+
end
|
25
|
+
|
26
|
+
def data=(data)
|
27
|
+
@data = data.extend(Extensions::Hash::StringifyKeys).stringify_keys!
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
require_relative "default_commands"
|
@@ -0,0 +1,95 @@
|
|
1
|
+
module PdfTempura
|
2
|
+
class Document::Table < Document::Field::Base
|
3
|
+
|
4
|
+
attr_accessor :columns,:name,:row_height,:padding,:cell_padding,:row_count
|
5
|
+
|
6
|
+
validates :row_height, type: Numeric
|
7
|
+
validates :row_count, type: Numeric
|
8
|
+
validates :padding, type: Array, inner_type: Numeric, count: 4
|
9
|
+
validates :cell_padding, type: Numeric
|
10
|
+
|
11
|
+
def initialize(name,coordinates,options = {},&block)
|
12
|
+
super name, coordinates, [0,0], options
|
13
|
+
|
14
|
+
@columns = []
|
15
|
+
|
16
|
+
instance_eval(&block) if block_given?
|
17
|
+
end
|
18
|
+
|
19
|
+
def width
|
20
|
+
table_width + padding_width
|
21
|
+
end
|
22
|
+
|
23
|
+
def height
|
24
|
+
@height ||= row_height * row_count
|
25
|
+
end
|
26
|
+
|
27
|
+
def row_height
|
28
|
+
@row_height ||= height.to_f / row_count.to_f
|
29
|
+
end
|
30
|
+
|
31
|
+
def text_column(name, width, options = {})
|
32
|
+
columns << Document::Table::TextColumn.new(name, width, row_height, @options.merge(options))
|
33
|
+
end
|
34
|
+
|
35
|
+
def checkbox_column(name, width, options = {})
|
36
|
+
columns << Document::Table::CheckboxColumn.new(name, width, row_height, @options.merge(options))
|
37
|
+
end
|
38
|
+
|
39
|
+
def boxed_character_column(name, options = {}, &block)
|
40
|
+
columns << Document::Table::BoxedCharacterColumn.new(name, row_height, @options.merge(options), &block)
|
41
|
+
end
|
42
|
+
|
43
|
+
def space(width)
|
44
|
+
columns << Document::Table::Spacer.new(width, row_height)
|
45
|
+
end
|
46
|
+
|
47
|
+
def fields_for(values, &block)
|
48
|
+
values.inject(self.y) do |y, value_hash|
|
49
|
+
generate_columns(y, value_hash, &block)
|
50
|
+
y - row_height
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def generate_columns(y, values)
|
57
|
+
columns.inject(self.x) do |x, column|
|
58
|
+
yield column.field_at([x,y]), values[column.name] if column.generates_field?
|
59
|
+
x + column.width + cell_padding
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def table_width
|
64
|
+
columns.map(&:width).inject(0, :+)
|
65
|
+
end
|
66
|
+
|
67
|
+
def padding_width
|
68
|
+
(columns.any?) ? cell_padding * (columns.count - 1) : 0
|
69
|
+
end
|
70
|
+
|
71
|
+
def validate_height
|
72
|
+
unless row_count && (@row_height || @height)
|
73
|
+
raise ArgumentError.new("You must pass number_of_rows and either height or row_height")
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def load_options(options)
|
78
|
+
@height = options["height"]
|
79
|
+
@row_height = options["row_height"]
|
80
|
+
@row_count = options["number_of_rows"]
|
81
|
+
@padding = options["padding"] || [0,0,0,0]
|
82
|
+
@cell_padding = options["cell_padding"] || 0
|
83
|
+
@options = options
|
84
|
+
|
85
|
+
validate_height
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
require_relative 'table/column'
|
92
|
+
require_relative 'table/text_column'
|
93
|
+
require_relative 'table/checkbox_column'
|
94
|
+
require_relative 'table/spacer'
|
95
|
+
require_relative 'table/boxed_character_column'
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module PdfTempura
|
2
|
+
class Document::Table::BoxedCharacterColumn < Document::Table::Column
|
3
|
+
|
4
|
+
def initialize(name, height, options = {}, &block)
|
5
|
+
@name = name
|
6
|
+
@height = height
|
7
|
+
@options = options
|
8
|
+
@block = block if block_given?
|
9
|
+
end
|
10
|
+
|
11
|
+
def width
|
12
|
+
field_at([0,0]).width
|
13
|
+
end
|
14
|
+
|
15
|
+
def field_at(coordinates)
|
16
|
+
PdfTempura::Document::BoxedCharacters.new(@name, coordinates, @height, @options, &@block)
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module PdfTempura
|
2
|
+
class Document::Table::Column
|
3
|
+
|
4
|
+
attr_reader :name, :width, :options, :height
|
5
|
+
|
6
|
+
def initialize(name, width, height, options = {})
|
7
|
+
@name = name
|
8
|
+
@width = width
|
9
|
+
@height = height
|
10
|
+
@options = options
|
11
|
+
end
|
12
|
+
|
13
|
+
def generates_field?
|
14
|
+
true
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require_relative 'character_field'
|
2
|
+
|
3
|
+
module PdfTempura
|
4
|
+
class Document::TextField < Document::CharacterField
|
5
|
+
|
6
|
+
private
|
7
|
+
|
8
|
+
def load_options(options)
|
9
|
+
super
|
10
|
+
|
11
|
+
@alignment = (options["alignment"] || "left").to_s
|
12
|
+
@multi_line = options["multi_line"] || false
|
13
|
+
@valign = options["valign"] || "center"
|
14
|
+
@leading = options["leading"] || 0
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module PdfTempura
|
2
|
+
module Document::Validation
|
3
|
+
|
4
|
+
def self.included(base)
|
5
|
+
base.extend(ClassMethods)
|
6
|
+
end
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
|
10
|
+
def validations
|
11
|
+
@validations ||= (superclass.respond_to?(:validations) ? superclass.validations.dup : {})
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def validates(name, validation_options)
|
17
|
+
if self.instance_methods.include?(name)
|
18
|
+
validations[name] = validation_options
|
19
|
+
else
|
20
|
+
raise NoMethodError, "Can't validate undefined method '#{name}'."
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def validate!
|
29
|
+
self.class.validations.each do |method, validation_options|
|
30
|
+
validation_options.each do |validation_type, values|
|
31
|
+
find_validator(validation_type).new.validate(self, method, values)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def find_validator(validation_type)
|
37
|
+
validator_name = validation_type.to_s.split("_").push("validator").map(&:capitalize).join
|
38
|
+
Document::Validation.const_get(validator_name)
|
39
|
+
end
|
40
|
+
|
41
|
+
class RequiredValidator
|
42
|
+
def validate(object, method, not_used)
|
43
|
+
raise ArgumentError, "#{method.capitalize} is required." unless object.send(method)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class BooleanValidator
|
48
|
+
def validate(object, method, not_used)
|
49
|
+
raise ArgumentError, "#{method.capitalize} must be of type boolean." unless object.send(method).is_a?(TrueClass) || object.send(method).is_a?(FalseClass)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
class InclusionValidator
|
54
|
+
def validate(object, method, values)
|
55
|
+
raise ArgumentError, "#{method.capitalize} must be one of the following values: #{values.join(", ")}." unless values.include?(object.send(method))
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
class TypeValidator
|
60
|
+
def validate(object, method, type)
|
61
|
+
raise ArgumentError, "#{method.capitalize} must be of type #{type.inspect}." unless object.send(method).is_a?(type)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
class InnerTypeValidator
|
66
|
+
def validate(object, method, type)
|
67
|
+
raise ArgumentError, "#{method.capitalize} must contain only #{type.inspect} values." unless object.send(method).all?{ |inner| inner.is_a?(type) }
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
class CountValidator
|
72
|
+
def validate(object, method, count)
|
73
|
+
count_objects = object.send(method)
|
74
|
+
raise ArgumentError, "#{method.capitalize} must contain #{count} values." unless count_objects.respond_to?(:count) && count_objects.count == count
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module PdfTempura
|
2
|
+
module Extensions
|
3
|
+
module Hash
|
4
|
+
module StringifyKeys
|
5
|
+
|
6
|
+
def stringify_keys
|
7
|
+
dup.extend(StringifyKeys).stringify_keys!
|
8
|
+
end
|
9
|
+
|
10
|
+
def stringify_keys!
|
11
|
+
keys.each do |k|
|
12
|
+
stringify_keys_recursively!(self[k])
|
13
|
+
self[k.to_s] = self.delete(k)
|
14
|
+
end
|
15
|
+
|
16
|
+
self
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def stringify_keys_recursively!(object)
|
22
|
+
object.extend(StringifyKeys) if object.is_a?(::Hash)
|
23
|
+
|
24
|
+
if object.respond_to?(:stringify_keys!)
|
25
|
+
object.stringify_keys!
|
26
|
+
else
|
27
|
+
object.each{ |o| stringify_keys_recursively!(o) } if object.is_a?(::Array)
|
28
|
+
object
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module PdfTempura
|
2
|
+
module Render
|
3
|
+
|
4
|
+
end
|
5
|
+
end
|
6
|
+
|
7
|
+
require_relative 'render/option_access'
|
8
|
+
require_relative 'render/field_bounds'
|
9
|
+
require_relative 'render/debug'
|
10
|
+
require_relative 'render/field'
|
11
|
+
require_relative 'render/character_field'
|
12
|
+
require_relative 'render/text_field'
|
13
|
+
require_relative 'render/boxed_characters'
|
14
|
+
require_relative 'render/checkbox_field'
|
15
|
+
require_relative 'render/field_data_mapper'
|
16
|
+
require_relative 'render/page'
|
17
|
+
require_relative 'render/table'
|
18
|
+
require_relative 'render/field_set'
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module PdfTempura
|
2
|
+
module Render
|
3
|
+
class BoxedCharacters
|
4
|
+
include OptionAccess
|
5
|
+
include FieldBounds
|
6
|
+
|
7
|
+
attr_accessor :field,:value
|
8
|
+
|
9
|
+
def initialize(field, value, options = {})
|
10
|
+
@field = field
|
11
|
+
@options = options
|
12
|
+
@value = value ? value.dup : ""
|
13
|
+
|
14
|
+
validate_value_length!
|
15
|
+
end
|
16
|
+
|
17
|
+
def render(pdf)
|
18
|
+
characters = value.chars.to_a
|
19
|
+
|
20
|
+
field.fields.each do |field|
|
21
|
+
break if characters.empty?
|
22
|
+
Field::generate(field, characters.shift, @options).render(pdf)
|
23
|
+
end
|
24
|
+
|
25
|
+
render_debug_annotation(pdf) if draw_outlines?
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def validate_value_length!
|
31
|
+
if value && !field.truncate? && value.length > field.supported_characters
|
32
|
+
raise ArgumentError.new("Data for #{field.name} must be exactly #{field.supported_characters} characters or use the truncate option.")
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def render_debug_annotation(pdf)
|
37
|
+
Debug::OutsideAnnotation.new(field).render(pdf)
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module PdfTempura
|
2
|
+
module Render
|
3
|
+
class CharacterField < Field
|
4
|
+
|
5
|
+
private
|
6
|
+
|
7
|
+
def set_styling(pdf)
|
8
|
+
pdf.fill_color = "000000"
|
9
|
+
pdf.font @field.font_name, style: font_style
|
10
|
+
end
|
11
|
+
|
12
|
+
def font_style
|
13
|
+
if @field.bold? && @field.italic?
|
14
|
+
:bold_italic
|
15
|
+
elsif @field.bold?
|
16
|
+
:bold
|
17
|
+
elsif @field.italic?
|
18
|
+
:italic
|
19
|
+
else
|
20
|
+
:normal
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def render_field(pdf)
|
25
|
+
field_bounds_box(pdf) do
|
26
|
+
padding_bounds_box(pdf) do
|
27
|
+
pdf.text_box(@value.to_s, field_options)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def field_options
|
33
|
+
{
|
34
|
+
valign: @field.valign.to_sym,
|
35
|
+
align: @field.alignment.to_sym,
|
36
|
+
single_line: !@field.multi_line?,
|
37
|
+
overflow: :shrink_to_fit,
|
38
|
+
size: @field.font_size,
|
39
|
+
leading: @field.leading
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
43
|
+
def render_debug_annotation(pdf)
|
44
|
+
Debug::CharacterFieldAnnotation.new(@field).render(pdf)
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|