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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/.github/CONTRIBUTING.md +2 -2
  3. data/.github/workflows/test.yml +17 -5
  4. data/CHANGELOG.md +12 -1
  5. data/Dockerfile +1 -1
  6. data/Gemfile +0 -4
  7. data/README.md +4 -2
  8. data/gemfiles/prawn-2.2.gemfile +5 -0
  9. data/gemfiles/prawn-2.3.gemfile +5 -0
  10. data/gemfiles/prawn-2.4.gemfile +5 -0
  11. data/lib/thinreports.rb +5 -0
  12. data/lib/thinreports/core/shape.rb +2 -0
  13. data/lib/thinreports/core/shape/basic/format.rb +5 -0
  14. data/lib/thinreports/core/shape/stack_view.rb +17 -0
  15. data/lib/thinreports/core/shape/stack_view/format.rb +27 -0
  16. data/lib/thinreports/core/shape/stack_view/interface.rb +17 -0
  17. data/lib/thinreports/core/shape/stack_view/internal.rb +22 -0
  18. data/lib/thinreports/core/shape/stack_view/row_format.rb +39 -0
  19. data/lib/thinreports/core/shape/style/basic.rb +4 -1
  20. data/lib/thinreports/generate.rb +11 -0
  21. data/lib/thinreports/generator/pdf/document/draw_shape.rb +30 -9
  22. data/lib/thinreports/generator/pdf/document/graphics/image.rb +19 -1
  23. data/lib/thinreports/generator/pdf/document/graphics/text.rb +10 -5
  24. data/lib/thinreports/generator/pdf/document/page.rb +12 -0
  25. data/lib/thinreports/generator/pdf/prawn_ext/width_of.rb +7 -13
  26. data/lib/thinreports/section_report/build.rb +32 -0
  27. data/lib/thinreports/section_report/builder/item_builder.rb +49 -0
  28. data/lib/thinreports/section_report/builder/report_builder.rb +82 -0
  29. data/lib/thinreports/section_report/builder/report_data.rb +13 -0
  30. data/lib/thinreports/section_report/builder/stack_view_builder.rb +55 -0
  31. data/lib/thinreports/section_report/builder/stack_view_data.rb +11 -0
  32. data/lib/thinreports/section_report/generate.rb +26 -0
  33. data/lib/thinreports/section_report/pdf/render.rb +23 -0
  34. data/lib/thinreports/section_report/pdf/renderer/draw_item.rb +68 -0
  35. data/lib/thinreports/section_report/pdf/renderer/group_renderer.rb +57 -0
  36. data/lib/thinreports/section_report/pdf/renderer/headers_renderer.rb +24 -0
  37. data/lib/thinreports/section_report/pdf/renderer/section_height.rb +100 -0
  38. data/lib/thinreports/section_report/pdf/renderer/section_renderer.rb +39 -0
  39. data/lib/thinreports/section_report/pdf/renderer/stack_view_renderer.rb +55 -0
  40. data/lib/thinreports/section_report/pdf/renderer/stack_view_row_renderer.rb +38 -0
  41. data/lib/thinreports/section_report/schema/loader.rb +28 -0
  42. data/lib/thinreports/section_report/schema/parser.rb +52 -0
  43. data/lib/thinreports/section_report/schema/report.rb +30 -0
  44. data/lib/thinreports/section_report/schema/section.rb +47 -0
  45. data/lib/thinreports/version.rb +1 -1
  46. data/thinreports.gemspec +8 -2
  47. 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