thinreports 0.11.0 → 0.12.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|