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
@@ -22,7 +22,7 @@ module Thinreports
|
|
22
22
|
# @option attrs [Boolean] :single (false)
|
23
23
|
# @option attrs [:trancate, :shrink_to_fit, :expand] :overflow (:trancate)
|
24
24
|
# @option attrs [:none, :break_word] :word_wrap (:none)
|
25
|
-
def text_box(content, x, y, w, h, attrs = {})
|
25
|
+
def text_box(content, x, y, w, h, attrs = {}, &block)
|
26
26
|
w, h = s2f(w, h)
|
27
27
|
|
28
28
|
box_attrs = text_box_attrs(
|
@@ -35,10 +35,15 @@ module Thinreports
|
|
35
35
|
content = text_without_line_wrap(content) if attrs[:word_wrap] == :none
|
36
36
|
|
37
37
|
with_text_styles(attrs) do |built_attrs, font_styles|
|
38
|
-
|
39
|
-
[{ text: content, styles: font_styles }],
|
40
|
-
|
41
|
-
|
38
|
+
if block
|
39
|
+
block.call [{ text: content, styles: font_styles }],
|
40
|
+
built_attrs.merge(box_attrs)
|
41
|
+
else
|
42
|
+
pdf.formatted_text_box(
|
43
|
+
[{ text: content, styles: font_styles }],
|
44
|
+
built_attrs.merge(box_attrs)
|
45
|
+
)
|
46
|
+
end
|
42
47
|
end
|
43
48
|
rescue Prawn::Errors::CannotFit
|
44
49
|
# Nothing to do.
|
@@ -26,6 +26,18 @@ module Thinreports
|
|
26
26
|
stamp(format_id.to_s)
|
27
27
|
end
|
28
28
|
|
29
|
+
def start_new_page_for_section_report(format)
|
30
|
+
@current_page_format = format
|
31
|
+
pdf.start_new_page(new_basic_page_options(current_page_format).merge(
|
32
|
+
top_margin: current_page_format.page_margin[0],
|
33
|
+
bottom_margin: current_page_format.page_margin[2]
|
34
|
+
))
|
35
|
+
end
|
36
|
+
|
37
|
+
def max_content_height
|
38
|
+
pdf.margin_box.height
|
39
|
+
end
|
40
|
+
|
29
41
|
def add_blank_page
|
30
42
|
pdf.start_new_page(pdf.page_count.zero? ? { size: 'A4' } : {})
|
31
43
|
end
|
@@ -4,22 +4,13 @@ module Thinreports
|
|
4
4
|
module Generator
|
5
5
|
module PrawnExt
|
6
6
|
module WidthOf
|
7
|
-
#
|
8
|
-
# https://github.com/prawnpdf/prawn/pull/1117
|
7
|
+
# This patch fixes the character_spacing effect on text width calculation.
|
9
8
|
#
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
# Subtract the width of one character space from the string width calculation result.
|
14
|
-
#
|
15
|
-
# The original Prawn::Document#width_of returns the following result:
|
16
|
-
#
|
17
|
-
# Width of Character is 1
|
18
|
-
# Width of Character Space is 1
|
9
|
+
# The original #width_of:
|
19
10
|
#
|
20
11
|
# width_of('abcd') #=> 4 + 4 = 8
|
21
12
|
#
|
22
|
-
#
|
13
|
+
# The #width_of in this patch:
|
23
14
|
#
|
24
15
|
# width_of('abcd') #=> 4 + 3 = 7
|
25
16
|
#
|
@@ -32,4 +23,7 @@ module Thinreports
|
|
32
23
|
end
|
33
24
|
end
|
34
25
|
|
35
|
-
Prawn
|
26
|
+
# Prawn v2.3 and later includes this patch by https://github.com/prawnpdf/prawn/pull/1117.
|
27
|
+
if Prawn::VERSION < '2.3.0'
|
28
|
+
Prawn::Document.prepend Thinreports::Generator::PrawnExt::WidthOf
|
29
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'schema/loader'
|
4
|
+
require_relative 'builder/report_builder'
|
5
|
+
|
6
|
+
module Thinreports
|
7
|
+
module SectionReport
|
8
|
+
class Build
|
9
|
+
def call(report_params)
|
10
|
+
schema = load_schema(report_params)
|
11
|
+
params = report_params[:params] || {}
|
12
|
+
|
13
|
+
Builder::ReportBuilder.new(schema).build(params)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def load_schema(report_params)
|
19
|
+
loader = Schema::Loader.new
|
20
|
+
|
21
|
+
case
|
22
|
+
when report_params[:layout_file]
|
23
|
+
loader.load_from_file(report_params[:layout_file])
|
24
|
+
when report_params[:layout_data]
|
25
|
+
loader.load_from_data(report_params[:layout_data])
|
26
|
+
else
|
27
|
+
raise Errors::LayoutFileNotFound
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'stack_view_builder'
|
4
|
+
|
5
|
+
module Thinreports
|
6
|
+
module SectionReport
|
7
|
+
module Builder
|
8
|
+
class ItemBuilder
|
9
|
+
Context = Struct.new(:parent_schema)
|
10
|
+
|
11
|
+
def initialize(item_schema, parent_schema)
|
12
|
+
@item = Core::Shape::Interface(nil, item_schema)
|
13
|
+
@parent_schema = parent_schema
|
14
|
+
end
|
15
|
+
|
16
|
+
def build(item_params)
|
17
|
+
params = build_params(item_params)
|
18
|
+
|
19
|
+
item.visible(params[:display]) if params.key?(:display)
|
20
|
+
item.value(params[:value]) if params.key?(:value)
|
21
|
+
item.styles(params[:styles]) if params.key?(:styles)
|
22
|
+
|
23
|
+
if item.internal.format.attributes['type'] == Core::Shape::StackView::TYPE_NAME
|
24
|
+
StackViewBuilder.new(item).update(params)
|
25
|
+
end
|
26
|
+
|
27
|
+
item
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
attr_reader :item, :parent_schema
|
33
|
+
|
34
|
+
def build_params(params)
|
35
|
+
return {} unless params
|
36
|
+
|
37
|
+
case params
|
38
|
+
when Hash
|
39
|
+
params
|
40
|
+
when Proc
|
41
|
+
params.call(Context.new(parent_schema))
|
42
|
+
else
|
43
|
+
{ value: params }
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'report_data'
|
4
|
+
require_relative 'item_builder'
|
5
|
+
|
6
|
+
module Thinreports
|
7
|
+
module SectionReport
|
8
|
+
module Builder
|
9
|
+
class ReportBuilder
|
10
|
+
def initialize(schema)
|
11
|
+
@schema = schema
|
12
|
+
end
|
13
|
+
|
14
|
+
def build(params)
|
15
|
+
ReportData::Main.new(
|
16
|
+
schema,
|
17
|
+
build_groups(params[:groups])
|
18
|
+
)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
attr_reader :schema
|
24
|
+
|
25
|
+
def build_groups(groups_params)
|
26
|
+
return [] unless groups_params
|
27
|
+
|
28
|
+
groups_params.map do |group_params|
|
29
|
+
ReportData::Group.new(
|
30
|
+
build_sections(:header, group_params[:headers] || {}),
|
31
|
+
build_detail_sections(group_params[:details] || []),
|
32
|
+
build_sections(:footer, group_params[:footers] || {})
|
33
|
+
)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def build_sections(section_type, sections_params)
|
38
|
+
sections_schemas =
|
39
|
+
case section_type
|
40
|
+
when :header then schema.headers
|
41
|
+
when :footer then schema.footers
|
42
|
+
end
|
43
|
+
|
44
|
+
sections_schemas.each_with_object([]) do |(section_id, section_schema), sections|
|
45
|
+
section_params = sections_params[section_id.to_sym] || {}
|
46
|
+
next unless section_enabled?(section_schema, section_params)
|
47
|
+
|
48
|
+
items = build_items(section_schema, section_params[:items] || {})
|
49
|
+
sections << ReportData::Section.new(section_schema, items, section_params[:min_height])
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def build_detail_sections(details_params)
|
54
|
+
details_params.each_with_object([]) do |detail_params, details|
|
55
|
+
detail_id = detail_params[:id].to_sym
|
56
|
+
detail_schema = schema.details[detail_id]
|
57
|
+
|
58
|
+
next unless detail_schema
|
59
|
+
|
60
|
+
items = build_items(detail_schema, detail_params[:items] || {})
|
61
|
+
details << ReportData::Section.new(detail_schema, items, detail_params[:min_height])
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def build_items(section_schema, items_params)
|
66
|
+
section_schema.items.each_with_object([]) do |item_schema, items|
|
67
|
+
item = ItemBuilder.new(item_schema, section_schema).build(items_params[item_schema.id&.to_sym])
|
68
|
+
items << item if item.visible?
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def section_enabled?(section_schema, section_params)
|
73
|
+
if section_params.key?(:display)
|
74
|
+
section_params[:display]
|
75
|
+
else
|
76
|
+
section_schema.display?
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Thinreports
|
4
|
+
module SectionReport
|
5
|
+
module Builder
|
6
|
+
module ReportData
|
7
|
+
Main = Struct.new :schema, :groups
|
8
|
+
Group = Struct.new :headers, :details, :footers
|
9
|
+
Section = Struct.new :schema, :items, :min_height
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'stack_view_data'
|
4
|
+
|
5
|
+
module Thinreports
|
6
|
+
module SectionReport
|
7
|
+
module Builder
|
8
|
+
class StackViewBuilder
|
9
|
+
def initialize(item)
|
10
|
+
@item = item
|
11
|
+
end
|
12
|
+
|
13
|
+
def update(params)
|
14
|
+
rows_params = params[:rows] || {}
|
15
|
+
rows_schema = item.internal.format.rows
|
16
|
+
|
17
|
+
schema_row_ids = rows_schema.map {|row_schema| row_schema.id.to_sym}.to_set
|
18
|
+
|
19
|
+
rows = []
|
20
|
+
rows_schema.each do |row_schema|
|
21
|
+
row_params = rows_params[row_schema.id.to_sym] || {}
|
22
|
+
next unless row_enabled?(row_schema, row_params)
|
23
|
+
|
24
|
+
items = build_row_items(
|
25
|
+
row_schema,
|
26
|
+
row_params[:items] || {}
|
27
|
+
)
|
28
|
+
|
29
|
+
rows << StackViewData::Row.new(row_schema, items, row_params[:min_height])
|
30
|
+
end
|
31
|
+
item.internal.rows = rows
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
attr_reader :item
|
37
|
+
|
38
|
+
def build_row_items(row_schema, items_params)
|
39
|
+
row_schema.items.each_with_object([]) do |item_schema, items|
|
40
|
+
item = ItemBuilder.new(item_schema, row_schema).build(items_params[item_schema.id&.to_sym])
|
41
|
+
items << item if item.visible?
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def row_enabled?(row_schema, row_params)
|
46
|
+
if row_params.key?(:display)
|
47
|
+
row_params[:display]
|
48
|
+
else
|
49
|
+
row_schema.display?
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'build'
|
4
|
+
require_relative 'pdf/render'
|
5
|
+
|
6
|
+
module Thinreports
|
7
|
+
module SectionReport
|
8
|
+
class Generate
|
9
|
+
def initialize
|
10
|
+
@pdf = Thinreports::Generator::PDF::Document.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def call(report_params, filename: nil)
|
14
|
+
report = Build.new.call(report_params)
|
15
|
+
|
16
|
+
PDF::Render.new(pdf).call!(report)
|
17
|
+
|
18
|
+
filename ? pdf.render_file(filename) : pdf.render
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
attr_reader :pdf
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'renderer/group_renderer'
|
4
|
+
|
5
|
+
module Thinreports
|
6
|
+
module SectionReport
|
7
|
+
module PDF
|
8
|
+
class Render
|
9
|
+
def initialize(pdf)
|
10
|
+
@group_renderer = Renderer::GroupRenderer.new(pdf)
|
11
|
+
end
|
12
|
+
|
13
|
+
def call!(report)
|
14
|
+
report.groups.each { |group| group_renderer.render(report, group) }
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
attr_reader :group_renderer
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Thinreports
|
4
|
+
module SectionReport
|
5
|
+
module Renderer
|
6
|
+
module DrawItem
|
7
|
+
def draw_item(item, expanded_height = 0)
|
8
|
+
shape = item.internal
|
9
|
+
|
10
|
+
if shape.type_of?(Core::Shape::TextBlock::TYPE_NAME)
|
11
|
+
computed_height = shape.format.attributes['height']
|
12
|
+
computed_height += expanded_height if shape.format.follow_stretch == 'height'
|
13
|
+
|
14
|
+
if shape.style.finalized_styles['overflow'] == 'expand'
|
15
|
+
# When overflow is "expand", the value of the height argument is ignored and the shape is expanded to
|
16
|
+
# the bottom of the outer bounding box.
|
17
|
+
# That causes a position shift problem if vertical-align is "middle" or "bottom".
|
18
|
+
# To solve it, we overwrite the overflow to "truncate" when drawing.
|
19
|
+
# To emulate the "expand" behavior in the "truncate" mode,
|
20
|
+
# here we pass the greater value of the computed_height and the text height as text block height.
|
21
|
+
pdf.draw_shape_tblock(shape, height: [computed_height, calc_text_block_height(shape)].max, overflow: :truncate)
|
22
|
+
else
|
23
|
+
pdf.draw_shape_tblock(shape, height: computed_height)
|
24
|
+
end
|
25
|
+
elsif shape.type_of?(Core::Shape::ImageBlock::TYPE_NAME)
|
26
|
+
pdf.draw_shape_iblock(shape)
|
27
|
+
elsif shape.type_of?('text')
|
28
|
+
case shape.format.follow_stretch
|
29
|
+
when 'height'
|
30
|
+
pdf.draw_shape_text(shape, expanded_height)
|
31
|
+
else
|
32
|
+
pdf.draw_shape_text(shape)
|
33
|
+
end
|
34
|
+
elsif shape.type_of?('image')
|
35
|
+
pdf.draw_shape_image(shape)
|
36
|
+
elsif shape.type_of?('ellipse')
|
37
|
+
pdf.draw_shape_ellipse(shape)
|
38
|
+
elsif shape.type_of?('rect')
|
39
|
+
case shape.format.follow_stretch
|
40
|
+
when 'height'
|
41
|
+
pdf.draw_shape_rect(shape, expanded_height)
|
42
|
+
else
|
43
|
+
pdf.draw_shape_rect(shape)
|
44
|
+
end
|
45
|
+
elsif shape.type_of?('line')
|
46
|
+
case shape.format.follow_stretch
|
47
|
+
when 'height'
|
48
|
+
y1, y2 = shape.format.attributes.values_at('y1', 'y2')
|
49
|
+
if y1 < y2
|
50
|
+
pdf.draw_shape_line(shape, 0, expanded_height)
|
51
|
+
else
|
52
|
+
pdf.draw_shape_line(shape, expanded_height, 0)
|
53
|
+
end
|
54
|
+
when 'y'
|
55
|
+
pdf.draw_shape_line(shape, expanded_height, expanded_height)
|
56
|
+
else
|
57
|
+
pdf.draw_shape_line(shape)
|
58
|
+
end
|
59
|
+
elsif shape.type_of?(Core::Shape::StackView::TYPE_NAME)
|
60
|
+
stack_view_renderer.render(shape)
|
61
|
+
else
|
62
|
+
raise Thinreports::Errors::UnknownShapeType
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|