thinreports 0.11.0 → 0.12.0
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 +4 -4
- data/.github/CONTRIBUTING.md +2 -2
- data/.github/workflows/test.yml +17 -5
- data/CHANGELOG.md +12 -1
- data/Dockerfile +1 -1
- data/Gemfile +0 -4
- data/README.md +4 -2
- data/gemfiles/prawn-2.2.gemfile +5 -0
- data/gemfiles/prawn-2.3.gemfile +5 -0
- data/gemfiles/prawn-2.4.gemfile +5 -0
- data/lib/thinreports.rb +5 -0
- data/lib/thinreports/core/shape.rb +2 -0
- data/lib/thinreports/core/shape/basic/format.rb +5 -0
- data/lib/thinreports/core/shape/stack_view.rb +17 -0
- data/lib/thinreports/core/shape/stack_view/format.rb +27 -0
- data/lib/thinreports/core/shape/stack_view/interface.rb +17 -0
- data/lib/thinreports/core/shape/stack_view/internal.rb +22 -0
- data/lib/thinreports/core/shape/stack_view/row_format.rb +39 -0
- data/lib/thinreports/core/shape/style/basic.rb +4 -1
- data/lib/thinreports/generate.rb +11 -0
- data/lib/thinreports/generator/pdf/document/draw_shape.rb +30 -9
- data/lib/thinreports/generator/pdf/document/graphics/image.rb +19 -1
- data/lib/thinreports/generator/pdf/document/graphics/text.rb +10 -5
- data/lib/thinreports/generator/pdf/document/page.rb +12 -0
- data/lib/thinreports/generator/pdf/prawn_ext/width_of.rb +7 -13
- data/lib/thinreports/section_report/build.rb +32 -0
- data/lib/thinreports/section_report/builder/item_builder.rb +49 -0
- data/lib/thinreports/section_report/builder/report_builder.rb +82 -0
- data/lib/thinreports/section_report/builder/report_data.rb +13 -0
- data/lib/thinreports/section_report/builder/stack_view_builder.rb +55 -0
- data/lib/thinreports/section_report/builder/stack_view_data.rb +11 -0
- data/lib/thinreports/section_report/generate.rb +26 -0
- data/lib/thinreports/section_report/pdf/render.rb +23 -0
- data/lib/thinreports/section_report/pdf/renderer/draw_item.rb +68 -0
- data/lib/thinreports/section_report/pdf/renderer/group_renderer.rb +57 -0
- data/lib/thinreports/section_report/pdf/renderer/headers_renderer.rb +24 -0
- data/lib/thinreports/section_report/pdf/renderer/section_height.rb +100 -0
- data/lib/thinreports/section_report/pdf/renderer/section_renderer.rb +39 -0
- data/lib/thinreports/section_report/pdf/renderer/stack_view_renderer.rb +55 -0
- data/lib/thinreports/section_report/pdf/renderer/stack_view_row_renderer.rb +38 -0
- data/lib/thinreports/section_report/schema/loader.rb +28 -0
- data/lib/thinreports/section_report/schema/parser.rb +52 -0
- data/lib/thinreports/section_report/schema/report.rb +30 -0
- data/lib/thinreports/section_report/schema/section.rb +47 -0
- data/lib/thinreports/version.rb +1 -1
- data/thinreports.gemspec +8 -2
- metadata +103 -5
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'section_renderer'
|
4
|
+
|
5
|
+
module Thinreports
|
6
|
+
module SectionReport
|
7
|
+
module Renderer
|
8
|
+
class GroupRenderer
|
9
|
+
def initialize(pdf)
|
10
|
+
@pdf = pdf
|
11
|
+
@section_renderer = Renderer::SectionRenderer.new(pdf)
|
12
|
+
end
|
13
|
+
|
14
|
+
def render(report, group)
|
15
|
+
pdf.start_new_page_for_section_report report.schema
|
16
|
+
current_page_height = 0
|
17
|
+
|
18
|
+
max_page_height = pdf.max_content_height
|
19
|
+
|
20
|
+
group.headers.each do |header|
|
21
|
+
section_renderer.render(header)
|
22
|
+
current_page_height += section_renderer.section_height(header)
|
23
|
+
end
|
24
|
+
|
25
|
+
group.details.each do |detail|
|
26
|
+
if current_page_height + section_renderer.section_height(detail) > max_page_height
|
27
|
+
pdf.start_new_page_for_section_report report.schema
|
28
|
+
current_page_height = 0
|
29
|
+
|
30
|
+
group.headers.each do |header|
|
31
|
+
if header.schema.every_page?
|
32
|
+
section_renderer.render(header)
|
33
|
+
current_page_height += section_renderer.section_height(header)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
section_renderer.render(detail)
|
38
|
+
current_page_height += section_renderer.section_height(detail)
|
39
|
+
end
|
40
|
+
|
41
|
+
group.footers.each do |footer|
|
42
|
+
if current_page_height + section_renderer.section_height(footer) > max_page_height
|
43
|
+
pdf.start_new_page_for_section_report report.schema
|
44
|
+
current_page_height = 0
|
45
|
+
end
|
46
|
+
section_renderer.render(footer)
|
47
|
+
current_page_height += section_renderer.section_height(footer)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
attr_reader :pdf, :section_renderer
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Thinreports
|
4
|
+
module SectionReport
|
5
|
+
module Renderer
|
6
|
+
class HeadersRenderer
|
7
|
+
def initialize(pdf)
|
8
|
+
@pdf = pdf
|
9
|
+
@section_renderer = Renderer::SectionRenderer.new(pdf)
|
10
|
+
end
|
11
|
+
|
12
|
+
def render(headers)
|
13
|
+
headers.each do |header|
|
14
|
+
section_renderer.render(header)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
attr_reader :pdf, :section_renderer
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Thinreports
|
4
|
+
module SectionReport
|
5
|
+
module Renderer
|
6
|
+
module SectionHeight
|
7
|
+
LayoutInfo = Struct.new(:shape, :content_height, :top_margin, :bottom_margin)
|
8
|
+
|
9
|
+
def section_height(section)
|
10
|
+
return [section.min_height || 0, section.schema.height].max if !section.schema.auto_stretch? || section.items.empty?
|
11
|
+
|
12
|
+
item_layouts = section.items.map { |item| item_layout(section, item.internal) }.compact
|
13
|
+
|
14
|
+
min_bottom_margin = item_layouts.each_with_object([]) do |l, margins|
|
15
|
+
margins << l.bottom_margin if l.shape.format.affect_bottom_margin?
|
16
|
+
end.min.to_f
|
17
|
+
|
18
|
+
max_content_bottom = item_layouts.each_with_object([]) do |l, bottoms|
|
19
|
+
bottoms << l.top_margin + l.content_height if l.shape.format.affect_bottom_margin?
|
20
|
+
end.max.to_f
|
21
|
+
|
22
|
+
[section.min_height || 0, max_content_bottom + min_bottom_margin].max
|
23
|
+
end
|
24
|
+
|
25
|
+
def calc_float_content_bottom(section)
|
26
|
+
item_layouts = section.items.map { |item| item_layout(section, item.internal) }.compact
|
27
|
+
item_layouts
|
28
|
+
.map { |l| l.top_margin + l.content_height }
|
29
|
+
.max.to_f
|
30
|
+
end
|
31
|
+
|
32
|
+
def item_layout(section, shape)
|
33
|
+
if shape.type_of?(Core::Shape::TextBlock::TYPE_NAME)
|
34
|
+
text_layout(section, shape)
|
35
|
+
elsif shape.type_of?(Core::Shape::StackView::TYPE_NAME)
|
36
|
+
stack_view_layout(section, shape)
|
37
|
+
elsif shape.type_of?(Core::Shape::ImageBlock::TYPE_NAME)
|
38
|
+
image_block_layout(section, shape)
|
39
|
+
elsif shape.type_of?('ellipse')
|
40
|
+
cy, ry = shape.format.attributes.values_at('cy', 'ry')
|
41
|
+
static_layout(section, shape, cy - ry, ry * 2)
|
42
|
+
elsif shape.type_of?('line')
|
43
|
+
y1, y2 = shape.format.attributes.values_at('y1', 'y2')
|
44
|
+
static_layout(section, shape, [y1, y2].min, (y2 - y1).abs)
|
45
|
+
else
|
46
|
+
y, height = shape.format.attributes.values_at('y', 'height')
|
47
|
+
raise ArgumentError.new("Unknown layout for #{shape}") if height == nil || y == nil
|
48
|
+
static_layout(section, shape, y, height)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def static_layout(section, shape, y, height)
|
53
|
+
LayoutInfo.new(shape, height, y, section.schema.height - height - y)
|
54
|
+
end
|
55
|
+
|
56
|
+
def image_block_layout(section, shape)
|
57
|
+
y, height = shape.format.attributes.values_at('y', 'height')
|
58
|
+
if shape.style.finalized_styles['position-y'] == 'top'
|
59
|
+
dimensions = pdf.shape_iblock_dimenions(shape)
|
60
|
+
content_height = dimensions ? dimensions[1] : 0
|
61
|
+
|
62
|
+
LayoutInfo.new(shape, content_height, y, section.schema.height - height - y)
|
63
|
+
else
|
64
|
+
static_layout(section, shape, y, height)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def calc_text_block_height(shape)
|
69
|
+
height = 0
|
70
|
+
|
71
|
+
pdf.draw_shape_tblock(shape) do |array, options|
|
72
|
+
modified_options = options.merge(at: [0, 10_000], height: 10_000)
|
73
|
+
height = pdf.pdf.height_of_formatted(array, modified_options)
|
74
|
+
end
|
75
|
+
height
|
76
|
+
end
|
77
|
+
|
78
|
+
def text_layout(section, shape)
|
79
|
+
y, schema_height = shape.format.attributes.values_at('y', 'height')
|
80
|
+
|
81
|
+
content_height = if shape.style.finalized_styles['overflow'] == 'expand'
|
82
|
+
[schema_height, calc_text_block_height(shape)].max
|
83
|
+
else
|
84
|
+
schema_height
|
85
|
+
end
|
86
|
+
|
87
|
+
LayoutInfo.new(shape, content_height, y, section.schema.height - schema_height - y)
|
88
|
+
end
|
89
|
+
|
90
|
+
def stack_view_layout(section, shape)
|
91
|
+
schema_height = 0
|
92
|
+
shape.format.rows.each {|row| schema_height += row.attributes['height']}
|
93
|
+
|
94
|
+
y = shape.format.attributes['y']
|
95
|
+
LayoutInfo.new(shape, stack_view_renderer.section_height(shape), y, section.schema.height - schema_height - y)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'stack_view_renderer'
|
4
|
+
require_relative 'section_height'
|
5
|
+
require_relative 'draw_item'
|
6
|
+
|
7
|
+
module Thinreports
|
8
|
+
module SectionReport
|
9
|
+
module Renderer
|
10
|
+
class SectionRenderer
|
11
|
+
include SectionHeight
|
12
|
+
include DrawItem
|
13
|
+
|
14
|
+
def initialize(pdf)
|
15
|
+
@pdf = pdf
|
16
|
+
end
|
17
|
+
|
18
|
+
def render(section)
|
19
|
+
doc = pdf.pdf
|
20
|
+
|
21
|
+
actual_height = section_height(section)
|
22
|
+
doc.bounding_box([0, doc.cursor], width: doc.bounds.width, height: actual_height) do
|
23
|
+
section.items.each do |item|
|
24
|
+
draw_item(item, (actual_height - section.schema.height))
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
attr_reader :pdf
|
32
|
+
|
33
|
+
def stack_view_renderer
|
34
|
+
@stack_view_renderer ||= Renderer::StackViewRenderer.new(pdf)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'stack_view_row_renderer'
|
4
|
+
|
5
|
+
module Thinreports
|
6
|
+
module SectionReport
|
7
|
+
module Renderer
|
8
|
+
class StackViewRenderer
|
9
|
+
def initialize(pdf)
|
10
|
+
@pdf = pdf
|
11
|
+
@row_renderer = Renderer::StackViewRowRenderer.new(pdf)
|
12
|
+
end
|
13
|
+
|
14
|
+
RowLayout = Struct.new(:row, :height, :top)
|
15
|
+
|
16
|
+
def section_height(shape)
|
17
|
+
row_layouts = build_row_layouts(shape.rows)
|
18
|
+
|
19
|
+
total_row_height = row_layouts.sum(0, &:height)
|
20
|
+
float_content_bottom = row_layouts
|
21
|
+
.map { |l| row_renderer.calc_float_content_bottom(l.row) + l.top }
|
22
|
+
.max.to_f
|
23
|
+
|
24
|
+
[total_row_height, float_content_bottom].max
|
25
|
+
end
|
26
|
+
|
27
|
+
def render(shape)
|
28
|
+
doc = pdf.pdf
|
29
|
+
|
30
|
+
x, y, w = shape.format.attributes.values_at('x', 'y', 'width')
|
31
|
+
doc.bounding_box([x, doc.bounds.height - y], width: w, height: section_height(shape)) do
|
32
|
+
shape.rows.each do |row|
|
33
|
+
row_renderer.render(row)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
attr_reader :pdf, :row_renderer
|
41
|
+
|
42
|
+
def build_row_layouts(rows)
|
43
|
+
row_layouts = rows.map { |row| RowLayout.new(row, row_renderer.section_height(row)) }
|
44
|
+
|
45
|
+
row_layouts.inject(0) do |top, row_layout|
|
46
|
+
row_layout.top = top
|
47
|
+
top + row_layout.height
|
48
|
+
end
|
49
|
+
|
50
|
+
row_layouts
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'section_height'
|
4
|
+
require_relative 'draw_item'
|
5
|
+
|
6
|
+
module Thinreports
|
7
|
+
module SectionReport
|
8
|
+
module Renderer
|
9
|
+
class StackViewRowRenderer
|
10
|
+
include SectionHeight
|
11
|
+
include DrawItem
|
12
|
+
|
13
|
+
def initialize(pdf)
|
14
|
+
@pdf = pdf
|
15
|
+
end
|
16
|
+
|
17
|
+
def render(row)
|
18
|
+
doc = pdf.pdf
|
19
|
+
|
20
|
+
actual_height = section_height(row)
|
21
|
+
doc.bounding_box([0, doc.cursor], width: doc.bounds.width, height: actual_height) do
|
22
|
+
row.items.each do |item|
|
23
|
+
draw_item(item, (actual_height - row.schema.height))
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
attr_reader :pdf
|
31
|
+
|
32
|
+
def stack_view_renderer
|
33
|
+
raise Thinreports::Errors::InvalidLayoutFormat, 'nested StackView does not supported'
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'parser'
|
4
|
+
|
5
|
+
module Thinreports
|
6
|
+
module SectionReport
|
7
|
+
module Schema
|
8
|
+
class Loader
|
9
|
+
def initialize
|
10
|
+
@parser = Schema::Parser.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def load_from_file(filename)
|
14
|
+
data = File.read(filename, encoding: 'UTF-8')
|
15
|
+
load_from_data(data)
|
16
|
+
end
|
17
|
+
|
18
|
+
def load_from_data(data)
|
19
|
+
parser.parse(data)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
attr_reader :parser
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
require_relative 'report'
|
6
|
+
require_relative 'section'
|
7
|
+
|
8
|
+
module Thinreports
|
9
|
+
module SectionReport
|
10
|
+
module Schema
|
11
|
+
class Parser
|
12
|
+
def parse(schema_json_data)
|
13
|
+
schema_data = JSON.parse(schema_json_data)
|
14
|
+
|
15
|
+
section_schema_datas = schema_data['sections'].group_by { |section| section['type'] }
|
16
|
+
|
17
|
+
Schema::Report.new(
|
18
|
+
schema_data,
|
19
|
+
headers: parse_sections(:header, section_schema_datas['header']),
|
20
|
+
details: parse_sections(:detail, section_schema_datas['detail']),
|
21
|
+
footers: parse_sections(:footer, section_schema_datas['footer'])
|
22
|
+
)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
attr_reader :schema_data
|
28
|
+
|
29
|
+
def parse_sections(section_type, section_schema_datas = nil)
|
30
|
+
return {} if section_schema_datas.nil?
|
31
|
+
|
32
|
+
section_schema_datas.each_with_object({}) do |section_schema_data, section_schemas|
|
33
|
+
id = section_schema_data['id']
|
34
|
+
section_schemas[id.to_sym] = parse_section(section_type, section_schema_data)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def parse_section(type, section_schema_data)
|
39
|
+
items = section_schema_data['items'].map do |item_schema_data|
|
40
|
+
item_type = item_schema_data['type']
|
41
|
+
Core::Shape::Format(item_type).new(item_schema_data)
|
42
|
+
end
|
43
|
+
section_schema_class_for(type).new(section_schema_data, items: items)
|
44
|
+
end
|
45
|
+
|
46
|
+
def section_schema_class_for(section_type)
|
47
|
+
Schema::Section.const_get(section_type.capitalize)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Thinreports
|
4
|
+
module SectionReport
|
5
|
+
module Schema
|
6
|
+
class Report < Core::Shape::Manager::Format
|
7
|
+
config_reader last_version: %w( version )
|
8
|
+
config_reader report_title: %w( title )
|
9
|
+
config_reader page_paper_type: %w( report paper-type ),
|
10
|
+
page_orientation: %w( report orientation ),
|
11
|
+
page_margin: %w( report margin ),
|
12
|
+
page_width: %w[report width],
|
13
|
+
page_height: %w[report height]
|
14
|
+
|
15
|
+
attr_reader :headers, :details, :footers
|
16
|
+
|
17
|
+
def user_paper_type?
|
18
|
+
page_paper_type == 'user'
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialize(schema_data, headers:, details:, footers:)
|
22
|
+
super(schema_data)
|
23
|
+
@headers = headers
|
24
|
+
@details = details
|
25
|
+
@footers = footers
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Thinreports
|
4
|
+
module SectionReport
|
5
|
+
module Schema
|
6
|
+
module Section
|
7
|
+
class Base < Core::Shape::Manager::Format
|
8
|
+
config_reader :id, :type
|
9
|
+
config_reader :height
|
10
|
+
config_checker true, :display
|
11
|
+
config_checker true, auto_stretch: 'auto-stretch'
|
12
|
+
|
13
|
+
attr_reader :items
|
14
|
+
|
15
|
+
def initialize(schema_data, items:)
|
16
|
+
super(schema_data)
|
17
|
+
initialize_items(items)
|
18
|
+
end
|
19
|
+
|
20
|
+
def find_item(id)
|
21
|
+
@item_with_ids[id.to_sym]
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def initialize_items(items)
|
27
|
+
@items = items
|
28
|
+
@item_with_ids = items.each_with_object({}) do |item, item_with_ids|
|
29
|
+
next if item.id.empty?
|
30
|
+
item_with_ids[item.id.to_sym] = item
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class Header < Base
|
36
|
+
config_checker true, every_page: 'every-page'
|
37
|
+
end
|
38
|
+
|
39
|
+
class Footer < Base
|
40
|
+
end
|
41
|
+
|
42
|
+
class Detail < Base
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|