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
@@ -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
- pdf.formatted_text_box(
39
- [{ text: content, styles: font_styles }],
40
- built_attrs.merge(box_attrs)
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
- # TODO: Remove this path when a version of prawn that includes the following PR is released:
8
- # https://github.com/prawnpdf/prawn/pull/1117
7
+ # This patch fixes the character_spacing effect on text width calculation.
9
8
  #
10
- # This PR makes this patch unnecessary.
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
- # In this width_of, returns the following result:
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::Document.prepend Thinreports::Generator::PrawnExt::WidthOf
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,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Thinreports
4
+ module SectionReport
5
+ module Builder
6
+ module StackViewData
7
+ Row = Struct.new :schema, :items, :min_height
8
+ end
9
+ end
10
+ end
11
+ 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