Repository: thinreports/thinreports-generator Branch: main Commit: fbdad1818082 Files: 271 Total size: 857.7 KB Directory structure: gitextract_0zurrwy2/ ├── .github/ │ ├── CONTRIBUTING.md │ ├── dependabot.yml │ └── workflows/ │ └── test.yml ├── .gitignore ├── CHANGELOG.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── fonts/ │ └── IPA_Font_License_Agreement_v1.0.txt ├── gemfiles/ │ ├── prawn-2.4.gemfile │ └── prawn-2.5.gemfile ├── lib/ │ ├── thinreports/ │ │ ├── basic_report/ │ │ │ ├── core/ │ │ │ │ ├── errors.rb │ │ │ │ ├── format/ │ │ │ │ │ └── base.rb │ │ │ │ ├── shape/ │ │ │ │ │ ├── base/ │ │ │ │ │ │ ├── interface.rb │ │ │ │ │ │ └── internal.rb │ │ │ │ │ ├── base.rb │ │ │ │ │ ├── basic/ │ │ │ │ │ │ ├── block_format.rb │ │ │ │ │ │ ├── block_interface.rb │ │ │ │ │ │ ├── block_internal.rb │ │ │ │ │ │ ├── format.rb │ │ │ │ │ │ ├── interface.rb │ │ │ │ │ │ └── internal.rb │ │ │ │ │ ├── basic.rb │ │ │ │ │ ├── image_block/ │ │ │ │ │ │ ├── format.rb │ │ │ │ │ │ ├── interface.rb │ │ │ │ │ │ └── internal.rb │ │ │ │ │ ├── image_block.rb │ │ │ │ │ ├── list/ │ │ │ │ │ │ ├── format.rb │ │ │ │ │ │ ├── manager.rb │ │ │ │ │ │ ├── page.rb │ │ │ │ │ │ ├── page_state.rb │ │ │ │ │ │ ├── section_format.rb │ │ │ │ │ │ ├── section_interface.rb │ │ │ │ │ │ └── section_internal.rb │ │ │ │ │ ├── list.rb │ │ │ │ │ ├── manager/ │ │ │ │ │ │ ├── format.rb │ │ │ │ │ │ ├── internal.rb │ │ │ │ │ │ └── target.rb │ │ │ │ │ ├── manager.rb │ │ │ │ │ ├── page_number/ │ │ │ │ │ │ ├── format.rb │ │ │ │ │ │ ├── interface.rb │ │ │ │ │ │ └── internal.rb │ │ │ │ │ ├── page_number.rb │ │ │ │ │ ├── stack_view/ │ │ │ │ │ │ ├── format.rb │ │ │ │ │ │ ├── interface.rb │ │ │ │ │ │ ├── internal.rb │ │ │ │ │ │ └── row_format.rb │ │ │ │ │ ├── stack_view.rb │ │ │ │ │ ├── style/ │ │ │ │ │ │ ├── base.rb │ │ │ │ │ │ ├── basic.rb │ │ │ │ │ │ ├── graphic.rb │ │ │ │ │ │ └── text.rb │ │ │ │ │ ├── style.rb │ │ │ │ │ ├── text/ │ │ │ │ │ │ ├── format.rb │ │ │ │ │ │ ├── interface.rb │ │ │ │ │ │ └── internal.rb │ │ │ │ │ ├── text.rb │ │ │ │ │ ├── text_block/ │ │ │ │ │ │ ├── format.rb │ │ │ │ │ │ ├── formatter/ │ │ │ │ │ │ │ ├── basic.rb │ │ │ │ │ │ │ ├── datetime.rb │ │ │ │ │ │ │ ├── number.rb │ │ │ │ │ │ │ └── padding.rb │ │ │ │ │ │ ├── formatter.rb │ │ │ │ │ │ ├── interface.rb │ │ │ │ │ │ └── internal.rb │ │ │ │ │ └── text_block.rb │ │ │ │ ├── shape.rb │ │ │ │ └── utils.rb │ │ │ ├── generator/ │ │ │ │ ├── pdf/ │ │ │ │ │ ├── document/ │ │ │ │ │ │ ├── draw_shape.rb │ │ │ │ │ │ ├── draw_template_items.rb │ │ │ │ │ │ ├── font.rb │ │ │ │ │ │ ├── graphics/ │ │ │ │ │ │ │ ├── attributes.rb │ │ │ │ │ │ │ ├── basic.rb │ │ │ │ │ │ │ ├── image.rb │ │ │ │ │ │ │ └── text.rb │ │ │ │ │ │ ├── graphics.rb │ │ │ │ │ │ ├── page.rb │ │ │ │ │ │ └── parse_color.rb │ │ │ │ │ ├── document.rb │ │ │ │ │ ├── drawer/ │ │ │ │ │ │ ├── base.rb │ │ │ │ │ │ ├── list.rb │ │ │ │ │ │ ├── list_section.rb │ │ │ │ │ │ └── page.rb │ │ │ │ │ ├── prawn_ext/ │ │ │ │ │ │ ├── calc_image_dimensions.rb │ │ │ │ │ │ └── width_of.rb │ │ │ │ │ └── prawn_ext.rb │ │ │ │ └── pdf.rb │ │ │ ├── layout/ │ │ │ │ ├── base.rb │ │ │ │ ├── format.rb │ │ │ │ ├── legacy_schema.rb │ │ │ │ └── version.rb │ │ │ ├── layout.rb │ │ │ ├── report/ │ │ │ │ ├── base.rb │ │ │ │ ├── internal.rb │ │ │ │ └── page.rb │ │ │ └── report.rb │ │ ├── basic_report.rb │ │ ├── config.rb │ │ ├── section_report/ │ │ │ ├── build.rb │ │ │ ├── builder/ │ │ │ │ ├── item_builder.rb │ │ │ │ ├── report_builder.rb │ │ │ │ ├── report_data.rb │ │ │ │ ├── stack_view_builder.rb │ │ │ │ └── stack_view_data.rb │ │ │ ├── generate.rb │ │ │ ├── pdf/ │ │ │ │ ├── render.rb │ │ │ │ └── renderer/ │ │ │ │ ├── draw_item.rb │ │ │ │ ├── group_renderer.rb │ │ │ │ ├── section_height.rb │ │ │ │ ├── section_renderer.rb │ │ │ │ ├── stack_view_renderer.rb │ │ │ │ └── stack_view_row_renderer.rb │ │ │ └── schema/ │ │ │ ├── loader.rb │ │ │ ├── parser.rb │ │ │ ├── report.rb │ │ │ └── section.rb │ │ ├── section_report.rb │ │ └── version.rb │ └── thinreports.rb ├── test/ │ ├── basic_report/ │ │ ├── data/ │ │ │ ├── image_normal_jpg_noext │ │ │ ├── image_normal_png_noext │ │ │ └── legacy_layout/ │ │ │ └── all-items.tlf │ │ ├── features/ │ │ │ ├── dynamic_style/ │ │ │ │ ├── templates/ │ │ │ │ │ ├── styles.tlf │ │ │ │ │ └── styles_in_list.tlf │ │ │ │ └── test_feature.rb │ │ │ ├── eudc/ │ │ │ │ ├── template.tlf │ │ │ │ └── test_feature.rb │ │ │ ├── graphics/ │ │ │ │ ├── template.tlf │ │ │ │ └── test_feature.rb │ │ │ ├── hidden_item/ │ │ │ │ ├── template.tlf │ │ │ │ └── test_feature.rb │ │ │ ├── image_block/ │ │ │ │ ├── template.tlf │ │ │ │ └── test_feature.rb │ │ │ ├── list_events/ │ │ │ │ ├── template.tlf │ │ │ │ └── test_feature.rb │ │ │ ├── list_manually/ │ │ │ │ ├── template.tlf │ │ │ │ └── test_feature.rb │ │ │ ├── page_number/ │ │ │ │ ├── template.tlf │ │ │ │ └── test_feature.rb │ │ │ ├── page_number_with_list/ │ │ │ │ ├── template.tlf │ │ │ │ └── test_feature.rb │ │ │ ├── palleted_png/ │ │ │ │ ├── template.tlf │ │ │ │ └── test_feature.rb │ │ │ ├── report_callbacks/ │ │ │ │ ├── template.tlf │ │ │ │ └── test_feature.rb │ │ │ ├── text_align/ │ │ │ │ ├── template.tlf │ │ │ │ └── test_feature.rb │ │ │ ├── text_block_formatting/ │ │ │ │ ├── template.tlf │ │ │ │ └── test_feature.rb │ │ │ ├── text_block_overflow/ │ │ │ │ ├── template.tlf │ │ │ │ └── test_feature.rb │ │ │ ├── text_block_singleline/ │ │ │ │ ├── template.tlf │ │ │ │ └── test_feature.rb │ │ │ ├── text_block_style/ │ │ │ │ ├── templates/ │ │ │ │ │ ├── basic_styles.tlf │ │ │ │ │ └── font_size.tlf │ │ │ │ └── test_feature.rb │ │ │ ├── text_character_spacing/ │ │ │ │ ├── template.tlf │ │ │ │ └── test_feature.rb │ │ │ ├── text_overflow_wrap/ │ │ │ │ ├── template.tlf │ │ │ │ └── test_feature.rb │ │ │ └── text_word_wrap/ │ │ │ ├── template.tlf │ │ │ └── test_feature.rb │ │ ├── schema_helper.rb │ │ ├── test_helper.rb │ │ └── units/ │ │ ├── core/ │ │ │ ├── format/ │ │ │ │ └── test_base.rb │ │ │ ├── shape/ │ │ │ │ ├── base/ │ │ │ │ │ └── test_internal.rb │ │ │ │ ├── basic/ │ │ │ │ │ ├── test_block_format.rb │ │ │ │ │ ├── test_block_interface.rb │ │ │ │ │ ├── test_block_internal.rb │ │ │ │ │ ├── test_format.rb │ │ │ │ │ ├── test_interface.rb │ │ │ │ │ └── test_internal.rb │ │ │ │ ├── image_block/ │ │ │ │ │ ├── test_interface.rb │ │ │ │ │ └── test_internal.rb │ │ │ │ ├── list/ │ │ │ │ │ ├── test_format.rb │ │ │ │ │ ├── test_manager.rb │ │ │ │ │ ├── test_page.rb │ │ │ │ │ ├── test_page_state.rb │ │ │ │ │ ├── test_section_format.rb │ │ │ │ │ ├── test_section_interface.rb │ │ │ │ │ └── test_section_internal.rb │ │ │ │ ├── manager/ │ │ │ │ │ ├── test_format.rb │ │ │ │ │ ├── test_internal.rb │ │ │ │ │ └── test_target.rb │ │ │ │ ├── page_number/ │ │ │ │ │ ├── test_format.rb │ │ │ │ │ ├── test_interface.rb │ │ │ │ │ └── test_internal.rb │ │ │ │ ├── styles/ │ │ │ │ │ ├── test_base.rb │ │ │ │ │ ├── test_basic.rb │ │ │ │ │ ├── test_graphic.rb │ │ │ │ │ └── test_text.rb │ │ │ │ ├── text/ │ │ │ │ │ ├── test_format.rb │ │ │ │ │ └── test_internal.rb │ │ │ │ └── text_block/ │ │ │ │ ├── formatter/ │ │ │ │ │ ├── test_basic.rb │ │ │ │ │ ├── test_datetime.rb │ │ │ │ │ ├── test_number.rb │ │ │ │ │ └── test_padding.rb │ │ │ │ ├── test_format.rb │ │ │ │ ├── test_formatter.rb │ │ │ │ ├── test_interface.rb │ │ │ │ └── test_internal.rb │ │ │ ├── test_shape.rb │ │ │ └── test_utils.rb │ │ ├── generator/ │ │ │ ├── pdf/ │ │ │ │ ├── document/ │ │ │ │ │ ├── graphics/ │ │ │ │ │ │ ├── test_attributes.rb │ │ │ │ │ │ ├── test_basic.rb │ │ │ │ │ │ ├── test_image.rb │ │ │ │ │ │ └── test_text.rb │ │ │ │ │ ├── test_font.rb │ │ │ │ │ ├── test_graphics.rb │ │ │ │ │ ├── test_page.rb │ │ │ │ │ └── test_parse_color.rb │ │ │ │ ├── prawn_ext/ │ │ │ │ │ ├── test_calc_image_dimensions.rb │ │ │ │ │ └── test_width_of.rb │ │ │ │ └── test_document.rb │ │ │ └── test_pdf.rb │ │ ├── layout/ │ │ │ ├── test_base.rb │ │ │ ├── test_format.rb │ │ │ ├── test_legacy_schema.rb │ │ │ └── test_version.rb │ │ ├── report/ │ │ │ ├── test_base.rb │ │ │ └── test_internal.rb │ │ ├── test_layout.rb │ │ └── test_report.rb │ ├── feature_test.rb │ ├── main/ │ │ ├── test_config.rb │ │ └── test_helper.rb │ └── section_report/ │ ├── features/ │ │ ├── basic/ │ │ │ ├── README.md │ │ │ ├── template.tlf │ │ │ └── test_feature.rb │ │ ├── item_follow_stretch/ │ │ │ ├── README.md │ │ │ ├── template.tlf │ │ │ └── test_feature.rb │ │ ├── item_parameters/ │ │ │ ├── README.md │ │ │ ├── template.tlf │ │ │ └── test_feature.rb │ │ ├── multiple_groups/ │ │ │ ├── README.md │ │ │ ├── template.tlf │ │ │ └── test_feature.rb │ │ ├── nonexistent_id/ │ │ │ ├── README.md │ │ │ ├── template.tlf │ │ │ └── test_feature.rb │ │ ├── section_auto_stretch/ │ │ │ ├── README.md │ │ │ ├── template.tlf │ │ │ └── test_feature.rb │ │ ├── section_bottom_margin/ │ │ │ ├── README.md │ │ │ ├── template.tlf │ │ │ └── test_feature.rb │ │ ├── section_parameters/ │ │ │ ├── README.md │ │ │ ├── template.tlf │ │ │ └── test_feature.rb │ │ ├── stack_view/ │ │ │ ├── README.md │ │ │ ├── template.tlf │ │ │ └── test_feature.rb │ │ ├── stack_view_row_auto_stretch/ │ │ │ ├── README.md │ │ │ ├── template.tlf │ │ │ └── test_feature.rb │ │ ├── stack_view_row_bottom_margin/ │ │ │ ├── README.md │ │ │ ├── template.tlf │ │ │ └── test_feature.rb │ │ ├── stack_view_row_parameters/ │ │ │ ├── README.md │ │ │ ├── template.tlf │ │ │ └── test_feature.rb │ │ ├── stack_view_with_floating_item/ │ │ │ ├── README.md │ │ │ ├── template.tlf │ │ │ └── test_feature.rb │ │ └── text_block_vertical_align/ │ │ ├── README.md │ │ ├── template.tlf │ │ └── test_feature.rb │ └── test_helper.rb └── thinreports.gemspec ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/CONTRIBUTING.md ================================================ We only accept bug reports and pull requests in GitHub. If you have a support question about how to use thinreports, please [ask on GitHub Discussions](https://github.com/thinreports/thinreports/discussions). ================================================ FILE: .github/dependabot.yml ================================================ # To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 updates: - package-ecosystem: github-actions directory: / schedule: interval: daily ================================================ FILE: .github/workflows/test.yml ================================================ name: Test on: push: branches: - main - "dev**" pull_request: jobs: setup: name: Ruby ${{ matrix.ruby }} with prawn ${{ matrix.prawn }} runs-on: ubuntu-latest strategy: matrix: ruby: - "3.0" - "3.1" - "3.2" - "3.3" prawn: - "2.4" - "2.5" steps: - uses: actions/checkout@v4 - uses: hidakatsuya/action-setup-diff-pdf@v1 with: diff-pdf-version: "0.5" - name: Set up Ruby ${{ matrix.ruby }} uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} bundler-cache: true - name: Install dependencies run: bundle install --gemfile gemfiles/prawn-${{ matrix.prawn }}.gemfile --jobs 4 --retry 3 - name: Run tests for main run: bundle exec rake test:main - name: Run tests for basic report run: xvfb-run -a bundle exec rake test:basic_report - name: Run tests for section report run: xvfb-run -a bundle exec rake test:section_report ================================================ FILE: .gitignore ================================================ Gemfile.lock *.gem *.pdf .bundle .yardoc doc !/test/**/*/expect.pdf ================================================ FILE: CHANGELOG.md ================================================ ## main (Unreleased) ## 0.14.2 Changes: * Add base64 and bigdecimal to the gemspec #133 * Remove matrix gem version constraint ## 0.14.1 Enhancements: * Add overflow-wrap option #129 ## 0.14.0 Breaking Changes: * Drop Ruby 2.7 support Bug Fixes: * Fix LoadError: cannot load such file -- matrix #131 ## 0.13.1 Bug Fixes: * allow title argument to be specified in Report.generate method #127 ## 0.13.0 Breaking Changes: * Drop Ruby 2.5,2.6,JRuby support * Drop Prawn 2.2, 2.3 support * Moved Basic Format implementation to `Thinreports::BasicReport` namespace * `Thinreports::Report` and `Thinreports::Layout` are defined as aliases for `Thinreports::BasicReport::Report` and `Thinreports::BasicReport::Layout` respectively, but other than those can be a destructive change if you rely on internal implementations Enhancements: * Allow any string to be set to the title attribute of the PDF document metadata #125 * Add Ruby 3.1, 3.2 support Notes: * thinreports gem has opted in to the ["MFA required to publish" setting](https://guides.rubygems.org/mfa-requirement-opt-in/) on Rubygems.org ## 0.12.1 Bug Fixes: * Fix argument of Thinreports::Report.generate in Ruby 3.0 #116 [@sada] ## 0.12.0 Breaking Changes: * Drop Ruby 2.4 support #108, #109 Enhancements: * Ruby 3.0 support #108, #109 * Prawn 2.4 support #108, #109 ## 0.11.0 Breaking Changes: * Dropped Ruby 2.1 support * Dropped Ruby 2.2 support * Dropped Ruby 2.3 support * Formatter of a text-block works even with multiple-line (#95) Enhancements: * Ruby 2.7.0 support * Support Pathname layout option on `Thinreports::Report` #99 [@meganemura] Bug Fixes: * Fixed: thousands separation is applied to decimal part #95 ## 0.10.3 Bug Fixes: * Fixed: corner-radius of Rect is not applied correctly #93 ## 0.10.2 Bug Fixes: * Fixed: Prawn::Errors::UnknownFont error occured: "is not a known font" #89 ## 0.10.1 Bug Fixes: * Fixed a bug that an error occurred on page break in other list when there is a list with page-break disabled #87 [@meganemura] ## 0.10.0 * Thinreports requires Prawn 2.2 * Dropped Ruby 1.9.3 and 2.0.0 support * Fixed a bug line-height of a text line-wrapped is broken #36 [@tkobayashi0330] * Remove deprecated top-module `ThinReports` * Remove `config.convert_palleted_transparency_png` option * Remove `config.generator.default` option * Deprecate `:report` and `:generator` argument of `Thinreports::Report.generate` ### Remove config.convert_palleted_transparency_png option PNG image with indexed transparency has been supported since Prawn version 2.0. And now, thinreports requires Prawn 2.2+, so the unique support in thinreports is no longer needed. ### Remove deprecated top-module ThinReports Please use `Thinreports` instead. ## 0.9.1 * Fixed bug that line-height of text-block is always set to 0 #68 * Fixed a referenced text-block not rendered #68 * Fixed #66 an error occurred when render list #68 ## 0.9.0 * Drop support for layout file saved with Editor v0.7 or lower #62 * New format for layout file and deprecate old format #35 * Fixed fail in rendering a static image #61 * Make possible to set an image-data to image-block item #65 * Remove old way to define callback of list #54 * Remove `config.eudc_fonts=` option #53 * Deprecate `config.convert_palleted_transparency_png` option #49 ### Drop support for layout file saved with Editor v0.7 or lower Generator v0.9 or higher can't load the layout file saved with Editor v0.7 or lower. ### New format for layout file and deprecate old format This is a BIG internal change. Thinreports has two format types for layout file now: 1. Old format generated by Thinreports Editor 0.8.x or lower 2. New format generated by Thinreports Editor 0.9.x or higher thinreports-generator will supports as below: | Generator | Format type(1) | Format type(2) | | ---------- | :------------: | :------------: | | =< 0.8 | o | x | | 0.9, 1.0 | o | o | | >= 1.1 | x | o | See https://github.com/thinreports/thinreports/issues/4 for further details. ### Make possible to set an image-data to image-block item The following code works fine now. ```ruby png_image = StringIO.new(png_data) page.item(:image_block).src(png_image) require 'open-uri' open('http://example.com/image.png') do |image_data| page.item(:image_block).src(image_data) end ``` ### Remove old way to define callback of list The block of `Report::Base#use_layout` never performed: ```ruby report.use_layout 'foo.tlf' do |config| config.events.on(:page_create) { ... } config.list.events.on(:footer_insert) { ... } end ``` `List#store` and `List#events` has been removed: ```ruby report.list.store.foo += 1 # => NoMethodError report.list.events.on(:footer_insert) { ... } # => NoMethodError ``` `Layout::Base#config` has been removed: ```ruby report.default_layout.config { ... } # => NoMethodError ``` You can use the following method instead: * `Report::Base#on_page_create` * `List#on_page_finalize` * `List#on_footer_insert` * `List#on_page_footer_insert` See https://github.com/thinreports/thinreports-generator/tree/master/examples/list_events. ### Remove config.eudc_fonts= option ```ruby Thinreports.config.eudc_fonts = true # => NoMethodError ``` You can use `config.fallback_fonts` instead. ### Deprecate config.convert_palleted_transparency_png option ```ruby Thinreports.config.convert_palleted_transparency_png = true # => warn "[DEPRECATION]" ``` This is option to enable palette-based transparency PNG support. The option will be removed in version 1.0, but thinreports will support the feature by default. Please see [PR#32](https://github.com/thinreports/thinreports-generator/pull/32) for detailed the feature. ## 0.8.2 * Fixed: some Ruby warnings (#40, #41, #43) [Akira Matsuda, Katsuya Hidaka] * Some code improvements ## 0.8.1 ### Add OPTIONAL feature for converting a palette-based PNG w. transparency This feature is **DISABLED** by default. Please see [PR#32](https://github.com/thinreports/thinreports-generator/pull/32) for further details. ## 0.8.0 This release is a stepping stone to next major version 1.0.0 release. * Upgrade to Prawn 1.3 and drop support for Ruby 1.8.7 (#11) * Change name of root module to Thinreports from ThinReports (#15) * Implement `Item#value=` method (#20) * Implement new list callbacks (#17) * Implement `page[:item_id]=` as alias for `page.item(:item_id).value=` (#22) * Support font-size style for Text and TextBlock (#23) * Support for setting the default fallback font (#7) * Remove `Report#generate_file` method (#13) * Deprecate `Report#events`, and implement new callbacks (#18) * Deprecate `List#events`, recommend to use `List#on_page_finalize` callback or create manually (#9) ### Upgrade to Prawn 1.3 and drop support for Ruby 1.8.7 We have dropped support for MRI 1.8.7 and 1.8 mode JRuby by upgrading to Prawn 1.3. Currently supported versions are MRI 1.9.3 and 2.0.0 or higher, JRuby(1.9 mode) 1.7 or higher. ### Change name of root module to Thinreports from ThinReports We have changed name of root module to `Thinreports` from `ThinReports`. Old name `ThinReports` has been enabling as alias, but it will be removed in the next major release. ### Deprecate `List#events` and `List#store` `List#events` and `List#store` have been deprecated. ```ruby report.layout.config.list do |list| list.events.on :page_footer_insert do |e| # ... end # => warn: "[DEPRECATION] ..." list.events.on :footer_insert do |e| # ... end # => warn: "[DEPRECATION] ..." list.events.on :page_finalize do |e| # ... end # => warn: "[DEPRECATION] ..." end list.store.price += 0 # => warn: "[DEPRECATION] ..." ``` Please use new callbacks instead: ```ruby report.list do |list| price = 0 list.on_page_footer_insert do |footer| footer.item(:price).value = price end list.on_footer_insert do |footer| # ... end list.on_page_finalize do # ... end end ``` See [Issue #17](https://github.com/thinreports/thinreports-generator/issues/17), [Issue #9](https://github.com/thinreports/thinreports-generator/issues/9) and [examples/list_events](https://github.com/thinreports/thinreports-generator/tree/master/examples/list_events) for further details. ### Deprecate `Report#events`, and implement new callbacks `Report#events` has been deprecated: ```ruby report.events.on :page_create do |e| e.page.item(:text1).value('Text1') end # => warn: "[DEPRECATION] ..." report.events.on :generate do |e| e.pages.each do |page| page.item(:text2).value('Text2') end end # => warn: "[DEPRECATION] ..." ``` Please use `Report#on_page_create` callback instead. However `Report#on_generate` callback has not been implemented, but you can do the same things using `Report#pages` method. ```ruby report.on_page_create do |page| page.item(:text1).value('Text1') end report.pages.each do |page| page.item(:text2).value('Text2') end report.generate filename: 'foo.pdf' ``` See [Issue #18](https://github.com/thinreports/thinreports-generator/issues/18) for further details. ### Implement `page[:item_id]=` as alias for `page.item(:item_id).value=` (#22) ```ruby # New setter, same as `page.item(:text_block).value = 'tblock value'` page[:text_block] = 'tblock value' # New getter, same as `page.item(:text_block).value` page[:text_block] # => page[:text_block].value # => "tblock value" page.item(:text_block).value # => "tblock value" page[:image_block] = '/path/to/image.png' page[:image_block].src # => "/path/to/image.png" page.item(:image_block).src # => "/path/to/image.png" ``` See [Issue #22](https://github.com/thinreports/thinreports-generator/issues/22) for further details. ### Implement `Item#value=` method ```ruby page.item(:text_block).value('value') page.item(:text_block).value = 'value' page.item(:image_block).src('/path/to/image.tlf') page.item(:image_block).src = '/path/to/image.tlf' ``` See [Issue #20](https://github.com/thinreports/thinreports-generator/issues/20) for further details. ### Support font-size style for Text and TextBlock ```ruby page.item(:text).style(:font_size, 20) page.item(:text_block).style(:font_size, 20) page.item(:text_block).style(:font_size) # => 20 ``` See [Issue #23](https://github.com/thinreports/thinreports-generator/issues/23) for further details. ### Support for setting the default fallback font Please see [Issue #7](https://github.com/thinreports/thinreports-generator/issues/7) for further details. ## 0.7.7.1 * No release for generator ## 0.7.7 ### Features * ページ番号ツール [Katsuya Hidaka] * New "Word-wrap" property of TextBlock [Katsuya Hidaka] * B4/B5 の ISO サイズを追加 [Takumi FUJISE / Minoru Maeda / Katsuya Hidaka] * generate filename: 'foo.pdf' を実装、#generate_file を非推奨へ [Katsuya Hidaka] * start_new_page layout: 'file.tlf' でもデフォルトレイアウトが設定されるよう改善 [Eito Katagiri / Katsuya Hidaka] ### Bug fixes * Report#use_layout で list の設定を行うとエラーが発生する [Katsuya Hidaka] * Layout::Format#page_margin_right が不正な値を返す [Katsuya Hidaka] * セキュリティを設定した PDF を印刷すると "このページにはエラーがあります..." メッセージが表示される [Katsuya Hidaka] * B4 サイズで出力した PDF の用紙サイズが正しくない [Takumi FUJISE / Katsuya Hidaka] ## 0.7.6 ### Features * デフォルトレイアウトを書き換え可能へ変更 [Katsuya Hidaka] ### Bug fixes * Fix raise NoMethodError when has no default layout [Katsuya Hidaka] ## 0.7.5 ### Features * テキストブロックのオーバフロープロパティ [Katsuya Hidaka] * list メソッドのデフォルト id と Report#list の追加 [Katsuya Hidaka] ### Bug fixes * gem install 時にRDoc生成時のエラーが表示される場合がある [Katsuya Hidaka] * エディターにて一覧表ツールのヘッダーを使わない場合の動作について [吉田 和弘 / Katsuya Hidaka] ## 0.7.0 ### Features * Listに :page_finalize イベントを追加 * ダイナミックスタイルの拡充 * イメージブロックの実装 * Tblockで行間、横位置、縦位置、文字間隔の指定を可能に * Prawn 0.12.0 を採用 * メタ情報のタイトルに反映 * Example Test環境の構築 * 外字の埋め込みサポート * clean taskの削除 * YARD Docのテンプレート追加 * ロードするprawnのバージョンを固定 * .generate, .generate_fileメソッドのオプション指定をフラット化 * 単行モードテキストブロックがレイアウトで定義した「高さ」の影響を受けないように * PXD形式の出力フォーマット廃止とデフォルトタイプの導入 * $KCODEによる文字カウント処理の改善 * List#headerの挙動を改善 * Errors::UnknownItemId 時のエラーメッセージを分かりやすく * テスト漏れに対するテストコード作成とテスト ### Bug fixes * フッターが挿入時、リスト領域をオーバフローしない場合でも改ページされる場合がある * Tblockで基本書式のみ設定されている場合、その書式が反映されない * Tblockがフォントサイズに対して小さすぎる場合にエラー ================================================ FILE: Gemfile ================================================ # frozen_string_literal: true source 'https://rubygems.org' git_source(:github) { |repo| "https://github.com/#{repo}.git" } gemspec gem 'rake' gem 'minitest' gem 'mocha' gem 'pdf-inspector' gem 'pdf_matcher-testing' # suppress warning: assigned but unused variable - y1 # https://github.com/yob/pdf-reader/pull/502 gem 'pdf-reader', github: 'yob/pdf-reader', ref: 'dc7e6e46e1d842486bd34288df087b590e8a7b38' ================================================ FILE: LICENSE.txt ================================================ The MIT License (MIT) Copyright (c) 2015 Matsukei Co.,Ltd. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Thinreports Generator [![Gem Version](https://badge.fury.io/rb/thinreports.svg)](http://badge.fury.io/rb/thinreports) [![Test](https://github.com/thinreports/thinreports-generator/workflows/Test/badge.svg)](https://github.com/thinreports/thinreports-generator/actions) A Ruby library for [Thinreports](https://github.com/thinreports/thinreports). ## Prerequisites ### Supported Versions - Ruby 3.0, 3.1, 3.2, 3.3 - Prawn 2.4+ ## Installation Add this line to your application's Gemfile: ```ruby gem 'thinreports' ``` And then execute: $ bundle install Or install it yourself as: $ gem install thinreports ## Getting Started First of all, check out [the README](https://github.com/thinreports/thinreports#getting-started) for an overview of Thinreports, its features, the two template formats available, and a complete example with template files and Ruby code. ## Usage See [the README](https://github.com/thinreports/thinreports) for usage of the Section Format. ### Basic Format The template file (`.tlf`) must be created in the [Thinreports Basic Editor](https://github.com/thinreports/thinreports-basic-editor/). ```ruby require 'thinreports' report = Thinreports::Report.new layout: 'report.tlf' report.start_new_page do item(:title).value('Thinreports') end report.start_new_page do |page| # Item Finder page.item(:item_id) # => Item object page[:item_id] # => Item object # Text block page.item(:text_block).value('Pure Ruby') page.item(:text_block).value = 'Pure Ruby' page[:text_block] = 'Pure Ruby' page.item('text_block').set('value', color: '#0000ff') page.item(:text_block).format_enabled(false) # Image block page.item(:image_block).src('/path/to/image.png') page.item(:image_block).src = '/path/to/image.png' page[:image_block] = '/path/to/image.png' require 'open-uri' page.item(:image_block).src(open('http://www.thinreports.org/assets/logos/thinreports-logo.png')) # Attributes page.item(:any).hide page.item(:any).show page.item(:any).visible(true) page.item(:any).visible? # => true page.item(:any).id # => "any" # Styles page.item(:text).style(:color, 'red') page.item(:text).style(:bold, true) page.item(:text).style(:italic, true) page.item(:text).style(:linethrough, true) page.item(:text).style(:underline, true) page.item(:text).style(:font_size, 20) page.item(:text).style(:align, :left or :center or :right) page.item(:text).style(:valign, :top or :center or :bottom) page.item(:rectangle).style(:border_color, '#ff0000') .style(:border_width, 1) .style(:fill_color, '#ffffff') # Bulk setting of styles page.item(:text).styles(color: '#00000', align: :right) # Bulk setting of values page.values text_block_a: 'value', text_block_b: 'value' # Helpers page.item_exists?(:existing_id) # => true page.item_exists?('existing_id') # => true page.item_exists?(:unknown_id) # => false end report.generate(filename: 'report.pdf') ``` ```ruby Thinreports::Report.generate(filename: 'report.pdf', layout: 'report.tlf') do |report| report.start_new_page do |page| # : end end ``` #### Report and Page ```ruby report = Thinreports::Report.new layout: 'foo.tlf' 3.times { report.start_new_page } # Returns all pages report.pages # => [, , ] # Returns number of pages report.page_count # => 3 # Add a blank page report.add_blank_page report.pages.last # => Report::BlankPage ``` #### Using multiple layouts ```ruby report = Thinreports::Report.new report.use_layout '/path/to/default.tlf', default: true report.use_layout '/path/to/other1.tlf', id: :other report.start_new_page do |page| # use '/path/to/default.tlf' layout end report.start_new_page layout: :other do |page| # use '/path/to/other1.tlf' layout end report.start_new_page layout: '/path/to/other2.tlf' do |page| # use '/path/to/other2.tlf' layout end ``` #### Callbacks ```ruby report = Thinreports::Report.new layout: 'foo.tlf' # It will be called before finalizing each page report.on_page_create do |page| page.item(:text).value('Text for all pages') end ``` See also [basic_report/features/report_callbacks](https://github.com/thinreports/thinreports-generator/tree/main/test/basic_report/features/report_callbacks). #### List ```ruby report = Thinreports::Report.new layout: 'list.tlf' report.list.header do |header| header.item(:text_block).value('Title') end 10.times do |n| report.list.add_row do |row| row.item(:text_block).value(n) end end report.generate(filename: 'list.pdf') ``` ```ruby report = Thinreports::Report.new layout: 'list_with_footer.tlf' report.list do |list| total_price = 0 price_per_page = 0 list.on_page_finalize do total_price += price_per_page price_per_page = 0 end list.on_page_footer_insert do |footer| footer.values price: price_per_page end list.on_footer_insert do |footer| footer.item(:price).value(total_price) end [100, 200, 300].each do |price| list.add_row do |row| row[:price] = price end price_per_page += price end end ``` See also [basic_report/features/list_events](https://github.com/thinreports/thinreports-generator/tree/main/test/basic_report/features/list_events). #### Page Number ```ruby # Setting starting number of page report.start_page_number_from 5 # Setting whether to count new page report.start_new_page count: true # default report.start_new_page count: false # Change styles report.start_new_page do |page| page.item(:pageno).hide page.item(:pageno).show page.item(:pageno).visible(false) page.item(:pageno).styles(color: 'red', bold: true) end ``` See also [basic_report/features/page_number](https://github.com/thinreports/thinreports-generator/blob/main/test/basic_report/features/page_number) and [basic_report/features/page_number_with_list](https://github.com/thinreports/thinreports-generator/blob/main/test/basic_report/features/page_number_with_list). #### Configuring fallback fonts ```ruby Thinreports.configure do |config| config.fallback_fonts = '/path/to/fallback.ttf' end Thinreports.config.fallback_fonts = ['/path/to/font_a.ttf', '/path/to/font_b.ttf'] ``` See also [basic_report/features/eudc](https://github.com/thinreports/thinreports-generator/blob/main/test/basic_report/features/eudc). ## Contributing Bug reports and pull requests are welcome on GitHub at https://github.com/thinreports/thinreports-generator. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/thinreports/thinreports/blob/main/CODE_OF_CONDUCT.md). ## Development ### Feature test requires diff-pdf command In order to run feature test, you need to install [diff-pdf](https://github.com/vslavik/diff-pdf) in your environment, or you can run test in the docker container as below. ### How to develop in Docker container You can use the Docker container for development. This container contains the libraries required for testing, such as diff-pdf. ``` $ docker pull ghcr.io/hidakatsuya/ruby-with-diff-pdf:latest $ docker run -v $PWD:/src:cached -it ghcr.io/hidakatsuya/ruby-with-diff-pdf bash > /src# ``` You can run test: ``` > /src# bundle install > /src# bundle exec rake test ``` ## Releasing Generator 1. Update the version number in `version.rb` 2. Update `CHANGELOG.md` and `README.md` (if needed) 3. Create the Release PR like #105 4. Merge the PR 5. Is the main CI green? If not, make it green 6. Run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to rubygems.org ## License The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). ## Copyright (c) 2021 [Matsukei Co.,Ltd](http://www.matsukei.co.jp). ================================================ FILE: Rakefile ================================================ require 'bundler/gem_tasks' require 'rake/testtask' task default: :test Rake::TestTask.new do |t| t.name = 'test:main' t.description = 'Run tests for main' t.libs << 'test/main' t.test_files = Dir['test/main/**/test_*.rb'] end Rake::TestTask.new do |t| t.name = 'test:basic_report' t.description = 'Run tests for basic report' t.libs << 'test' << 'test/basic_report' t.test_files = Dir['test/basic_report/**/test_*.rb'] end Rake::TestTask.new do |t| t.name = 'test:section_report' t.description = 'Run tests for section report' t.libs << 'test' << 'test/section_report' t.test_files = Dir['test/section_report/**/test_*.rb'] end desc 'Run all tests' task test: %i(test:main test:basic_report test:section_report) ================================================ FILE: fonts/IPA_Font_License_Agreement_v1.0.txt ================================================ -------------------------------------------------- IPA Font License Agreement v1.0 -------------------------------------------------- IPAフォントライセンスv1.0 許諾者は、この使用許諾(以下「本契約」といいます。)に定める条件の下で、許諾プログラム(1条に定義するところによります。)を提供します。受領者(1条に定義するところによります。)が、許諾プログラムを使用し、複製し、または頒布する行為、その他、本契約に定める権利の利用を行った場合、受領者は本契約に同意したものと見なします。 第1条 用語の定義 本契約において、次の各号に掲げる用語は、当該各号に定めるところによります。 1.「デジタル・フォント・プログラム」とは、フォントを含み、レンダリングしまたは表示するために用いられるコンピュータ・プログラムをいいます。 2.「許諾プログラム」とは、許諾者が本契約の下で許諾するデジタル・フォント・プログラムをいいます。 3.「派生プログラム」とは、許諾プログラムの一部または全部を、改変し、加除修正等し、入れ替え、その他翻案したデジタル・フォント・プログラムをいい、許諾プログラムの一部もしくは全部から文字情報を取り出し、またはデジタル・ドキュメント・ファイルからエンベッドされたフォントを取り出し、取り出された文字情報をそのまま、または改変をなして新たなデジタル・フォント・プログラムとして製作されたものを含みます。 4.「デジタル・コンテンツ」とは、デジタル・データ形式によってエンド・ユーザに提供される制作物のことをいい、動画・静止画等の映像コンテンツおよびテレビ番組等の放送コンテンツ、ならびに文字テキスト、画像、図形等を含んで構成された制作物を含みます。 5.「デジタル・ドキュメント・ファイル」とは、PDFファイルその他、各種ソフトウェア・プログラムによって製作されたデジタル・コンテンツであって、その中にフォントを表示するために許諾プログラムの全部または一部が埋め込まれた(エンベッドされた)ものをいいます。フォントが「エンベッドされた」とは、当該フォントが埋め込まれた特定の「デジタル・ドキュメント・ファイル」においてのみ表示されるために使用されている状態を指し、その特定の「デジタル・ドキュメント・ファイル」以外でフォントを表示するために使用できるデジタル・フォント・プログラムに含まれている場合と区別されます。 6.「コンピュータ」とは、本契約においては、サーバを含みます。 7.「複製その他の利用」とは、複製、譲渡、頒布、貸与、公衆送信、上映、展示、翻案その他の利用をいいます。 8.「受領者」とは、許諾プログラムを本契約の下で受領した人をいい、受領者から許諾プログラムを受領した人を含みます。 第2条 使用許諾の付与 許諾者は受領者に対し、本契約の条項に従い、すべての国で、許諾プログラムを使用することを許諾します。ただし、許諾プログラムに存在する一切の権利はすべて許諾者が保有しています。本契約は、本契約で明示的に定められている場合を除き、いかなる意味においても、許諾者が保有する許諾プログラムに関する一切の権利および、いかなる商標、商号、もしくはサービス・マークに関する権利をも受領者に移転するものではありません。 1.受領者は本契約に定める条件に従い、許諾プログラムを任意の数のコンピュータにインストールし、当該コンピュータで使用することができます。 2.受領者はコンピュータにインストールされた許諾プログラムをそのまま、または改変を行ったうえで、印刷物およびデジタル・コンテンツにおいて、文字テキスト表現等として使用することができます。 3.受領者は前項の定めに従い作成した印刷物およびデジタル・コンテンツにつき、その商用・非商用の別、および放送、通信、各種記録メディアなどの媒体の形式を問わず、複製その他の利用をすることができます。 4.受領者がデジタル・ドキュメント・ファイルからエンベッドされたフォントを取り出して派生プログラムを作成した場合には、かかる派生プログラムは本契約に定める条件に従う必要があります。 5.許諾プログラムのエンベッドされたフォントがデジタル・ドキュメント・ファイル内のデジタル・コンテンツをレンダリングするためにのみ使用される場合において、受領者が当該デジタル・ドキュメント・ファイルを複製その他の利用をする場合には、受領者はかかる行為に関しては本契約の下ではいかなる義務をも負いません。 6.受領者は、3条2項の定めに従い、商用・非商用を問わず、許諾プログラムをそのままの状態で改変することなく複製して第三者への譲渡し、公衆送信し、その他の方法で再配布することができます(以下、「再配布」といいます。)。 7.受領者は、上記の許諾プログラムについて定められた条件と同様の条件に従って、派生プログラムを作成し、使用し、複製し、再配布することができます。ただし、受領者が派生プログラムを再配布する場合には、3条1項の定めに従うものとします。 第3条 制限 前条により付与された使用許諾は、以下の制限に服します。 1.派生プログラムが前条4項及び7項に基づき再配布される場合には、以下の全ての条件を満たさなければなりません。  (1)派生プログラムを再配布する際には、下記もまた、当該派生プログラムと一緒に再配布され、オンラインで提供され、または、郵送費・媒体及び取扱手数料の合計を超えない実費と引き換えに媒体を郵送する方法により提供されなければなりません。   (a)派生プログラムの写し; および   (b)派生プログラムを作成する過程でフォント開発プログラムによって作成された追加のファイルであって派生プログラムをさらに加工するにあたって利用できるファイルが存在すれば、当該ファイル  (2)派生プログラムの受領者が、派生プログラムを、このライセンスの下で最初にリリースされた許諾プログラム(以下、「オリジナル・プログラム」といいます。)に置き換えることができる方法を再配布するものとします。かかる方法は、オリジナル・ファイルからの差分ファイルの提供、または、派生プログラムをオリジナル・プログラムに置き換える方法を示す指示の提供などが考えられます。  (3)派生プログラムを、本契約書に定められた条件の下でライセンスしなければなりません。  (4)派生プログラムのプログラム名、フォント名またはファイル名として、許諾プログラムが用いているのと同一の名称、またはこれを含む名称を使用してはなりません。  (5)本項の要件を満たすためにオンラインで提供し、または媒体を郵送する方法で提供されるものは、その提供を希望するいかなる者によっても提供が可能です。 2.受領者が前条6項に基づき許諾プログラムを再配布する場合には、以下の全ての条件を満たさなければなりません。  (1)許諾プログラムの名称を変更してはなりません。  (2)許諾プログラムに加工その他の改変を加えてはなりません。  (3)本契約の写しを許諾プログラムに添付しなければなりません。 3.許諾プログラムは、現状有姿で提供されており、許諾プログラムまたは派生プログラムについて、許諾者は一切の明示または黙示の保証(権利の所在、非侵害、商品性、特定目的への適合性を含むがこれに限られません)を行いません。いかなる場合にも、その原因を問わず、契約上の責任か厳格責任か過失その他の不法行為責任かにかかわらず、また事前に通知されたか否かにかかわらず、許諾者は、許諾プログラムまたは派生プログラムのインストール、使用、複製その他の利用または本契約上の権利の行使によって生じた一切の損害(直接・間接・付随的・特別・拡大・懲罰的または結果的損害)(商品またはサービスの代替品の調達、システム障害から生じた損害、現存するデータまたはプログラムの紛失または破損、逸失利益を含むがこれに限られません)について責任を負いません。 4.許諾プログラムまたは派生プログラムのインストール、使用、複製その他の利用に関して、許諾者は技術的な質問や問い合わせ等に対する対応その他、いかなるユーザ・サポートをも行う義務を負いません。 第4条 契約の終了 1.本契約の有効期間は、受領者が許諾プログラムを受領した時に開始し、受領者が許諾プログラムを何らかの方法で保持する限り続くものとします。 2.前項の定めにかかわらず、受領者が本契約に定める各条項に違反したときは、本契約は、何らの催告を要することなく、自動的に終了し、当該受領者はそれ以後、許諾プログラムおよび派生プログラムを一切使用しまたは複製その他の利用をすることができないものとします。ただし、かかる契約の終了は、当該違反した受領者から許諾プログラムまたは派生プログラムの配布を受けた受領者の権利に影響を及ぼすものではありません。 第5条 準拠法 1.IPAは、本契約の変更バージョンまたは新しいバージョンを公表することができます。その場合には、受領者は、許諾プログラムまたは派生プログラムの使用、複製その他の利用または再配布にあたり、本契約または変更後の契約のいずれかを選択することができます。その他、上記に記載されていない条項に関しては日本の著作権法および関連法規に従うものとします。 2.本契約は、日本法に基づき解釈されます。 ---------- IPA Font License Agreement v1.0 The Licensor provides the Licensed Program (as defined in Article 1 below) under the terms of this license agreement (“Agreement”). Any use, reproduction or distribution of the Licensed Program, or any exercise of rights under this Agreement by a Recipient (as defined in Article 1 below) constitutes the Recipient's acceptance of this Agreement. Article 1 (Definitions) 1.“Digital Font Program” shall mean a computer program containing, or used to render or display fonts. 2.“Licensed Program” shall mean a Digital Font Program licensed by the Licensor under this Agreement. 3.“Derived Program” shall mean a Digital Font Program created as a result of a modification, addition, deletion, replacement or any other adaptation to or of a part or all of the Licensed Program, and includes a case where a Digital Font Program newly created by retrieving font information from a part or all of the Licensed Program or Embedded Fonts from a Digital Document File with or without modification of the retrieved font information. 4.“Digital Content” shall mean products provided to end users in the form of digital data, including video content, motion and/or still pictures, TV programs or other broadcasting content and products consisting of character text, pictures, photographic images, graphic symbols and/or the like. 5.“Digital Document File” shall mean a PDF file or other Digital Content created by various software programs in which a part or all of the Licensed Program becomes embedded or contained in the file for the display of the font (“Embedded Fonts”). Embedded Fonts are used only in the display of characters in the particular Digital Document File within which they are embedded, and shall be distinguished from those in any Digital Font Program, which may be used for display of characters outside that particular Digital Document File. 6.“Computer” shall include a server in this Agreement. 7.“Reproduction and Other Exploitation” shall mean reproduction, transfer, distribution, lease, public transmission, presentation, exhibition, adaptation and any other exploitation. 8.“Recipient” shall mean anyone who receives the Licensed Program under this Agreement, including one that receives the Licensed Program from a Recipient. Article 2 (Grant of License) The Licensor grants to the Recipient a license to use the Licensed Program in any and all countries in accordance with each of the provisions set forth in this Agreement. However, any and all rights underlying in the Licensed Program shall be held by the Licensor. In no sense is this Agreement intended to transfer any right relating to the Licensed Program held by the Licensor except as specifically set forth herein or any right relating to any trademark, trade name, or service mark to the Recipient. 1.The Recipient may install the Licensed Program on any number of Computers and use the same in accordance with the provisions set forth in this Agreement. 2.The Recipient may use the Licensed Program, with or without modification in printed materials or in Digital Content as an expression of character texts or the like. 3.The Recipient may conduct Reproduction and Other Exploitation of the printed materials and Digital Content created in accordance with the preceding Paragraph, for commercial or non-commercial purposes and in any form of media including but not limited to broadcasting, communication and various recording media. 4.If any Recipient extracts Embedded Fonts from a Digital Document File to create a Derived Program, such Derived Program shall be subject to the terms of this agreement. 5.If any Recipient performs Reproduction or Other Exploitation of a Digital Document File in which Embedded Fonts of the Licensed Program are used only for rendering the Digital Content within such Digital Document File then such Recipient shall have no further obligations under this Agreement in relation to such actions. 6.The Recipient may reproduce the Licensed Program as is without modification and transfer such copies, publicly transmit or otherwise redistribute the Licensed Program to a third party for commercial or non-commercial purposes (“Redistribute”), in accordance with the provisions set forth in Article 3 Paragraph 2. 7.The Recipient may create, use, reproduce and/or Redistribute a Derived Program under the terms stated above for the Licensed Program: provided, that the Recipient shall follow the provisions set forth in Article 3 Paragraph 1 when Redistributing the Derived Program. Article 3 (Restriction) The license granted in the preceding Article shall be subject to the following restrictions: 1.If a Derived Program is Redistributed pursuant to Paragraph 4 and 7 of the preceding Article, the following conditions must be met :  (1)The following must be also Redistributed together with the Derived Program, or be made available online or by means of mailing mechanisms in exchange for a cost which does not exceed the total costs of postage, storage medium and handling fees:   (a)a copy of the Derived Program; and   (b)any additional file created by the font developing program in the course of creating the Derived Program that can be used for further modification of the Derived Program, if any.  (2)It is required to also Redistribute means to enable recipients of the Derived Program to replace the Derived Program with the Licensed Program first released under this License (the “Original Program”). Such means may be to provide a difference file from the Original Program, or instructions setting out a method to replace the Derived Program with the Original Program.  (3)The Recipient must license the Derived Program under the terms and conditions of this Agreement.  (4)No one may use or include the name of the Licensed Program as a program name, font name or file name of the Derived Program.  (5)Any material to be made available online or by means of mailing a medium to satisfy the requirements of this paragraph may be provided, verbatim, by any party wishing to do so. 2.If the Recipient Redistributes the Licensed Program pursuant to Paragraph 6 of the preceding Article, the Recipient shall meet all of the following conditions:  (1)The Recipient may not change the name of the Licensed Program.  (2)The Recipient may not alter or otherwise modify the Licensed Program.  (3)The Recipient must attach a copy of this Agreement to the Licensed Program. 3.THIS LICENSED PROGRAM IS PROVIDED BY THE LICENSOR “AS IS” AND ANY EXPRESSED OR IMPLIED WARRANTY AS TO THE LICENSED PROGRAM OR ANY DERIVED PROGRAM, INCLUDING, BUT NOT LIMITED TO, WARRANTIES OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED. IN NO EVENT SHALL THE LICENSOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXTENDED, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO; PROCUREMENT OF SUBSTITUTED GOODS OR SERVICE; DAMAGES ARISING FROM SYSTEM FAILURE; LOSS OR CORRUPTION OF EXISTING DATA OR PROGRAM; LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE INSTALLATION, USE, THE REPRODUCTION OR OTHER EXPLOITATION OF THE LICENSED PROGRAM OR ANY DERIVED PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 4.The Licensor is under no obligation to respond to any technical questions or inquiries, or provide any other user support in connection with the installation, use or the Reproduction and Other Exploitation of the Licensed Program or Derived Programs thereof. Article 4 (Termination of Agreement) 1.The term of this Agreement shall begin from the time of receipt of the Licensed Program by the Recipient and shall continue as long as the Recipient retains any such Licensed Program in any way. 2.Notwithstanding the provision set forth in the preceding Paragraph, in the event of the breach of any of the provisions set forth in this Agreement by the Recipient, this Agreement shall automatically terminate without any notice. In the case of such termination, the Recipient may not use or conduct Reproduction and Other Exploitation of the Licensed Program or a Derived Program: provided that such termination shall not affect any rights of any other Recipient receiving the Licensed Program or the Derived Program from such Recipient who breached this Agreement. Article 5 (Governing Law) 1.IPA may publish revised and/or new versions of this License. In such an event, the Recipient may select either this Agreement or any subsequent version of the Agreement in using, conducting the Reproduction and Other Exploitation of, or Redistributing the Licensed Program or a Derived Program. Other matters not specified above shall be subject to the Copyright Law of Japan and other related laws and regulations of Japan. 2.This Agreement shall be construed under the laws of Japan. ================================================ FILE: gemfiles/prawn-2.4.gemfile ================================================ source 'https://rubygems.org' gem 'prawn', '~> 2.4.0' gemspec path: '../' ================================================ FILE: gemfiles/prawn-2.5.gemfile ================================================ source 'https://rubygems.org' gem 'prawn', '~> 2.5.0' gemspec path: '../' ================================================ FILE: lib/thinreports/basic_report/core/errors.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Errors class Basic < ::StandardError end class UnknownShapeStyleName < Basic def initialize(style, availables) super("The specified style name, '#{style}', cannot be used. " \ "The available styles are #{availables.map { |s| ":#{s}" }.join(', ')}.") end end class UnknownShapeType < Basic end class UnknownFormatterType < Basic end class LayoutFileNotFound < Basic end class FontFileNotFound < Basic end class NoRegisteredLayoutFound < Basic end class UnknownItemId < Basic def initialize(id, item_type = 'Basic') super("The layout does not have a #{item_type} Item with id '#{id}'.") end end class DisabledListSection < Basic def initialize(section) super("The #{section} section is disabled.") end end class UnknownLayoutId < Basic end class UnsupportedColorName < Basic end class InvalidLayoutFormat < Basic end class IncompatibleLayoutFormat < Basic def initialize(filename, fileversion, required_rules) super("Generator #{Thinreports::VERSION} can not be built this file, " \ "'#{File.basename(filename)}'. " \ "This file is updated in the Editor of version '#{fileversion}', " \ "but Generator requires version #{required_rules}.") end end end end end ================================================ FILE: lib/thinreports/basic_report/core/format/base.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Core module Format # @abstract class Base class << self def config_reader(*configs, &block) each_configs(*configs) do |m, location| define_read_method(m, location, &block) end end def config_checker(check, *configs) checker = ->(val) { val == check } each_configs(*configs) do |m, location| define_read_method(:"#{m}?", location, &checker) end end def config_writer(*configs) each_configs(*configs) do |m, location| define_write_method(m, location) end end def config_accessor(*configs, &block) config_reader(*configs, &block) config_writer(*configs) end private def define_read_method(m, location = nil, &block) define_method(m) do read(*location, &block) end end def define_write_method(m, location = nil) define_method(:"#{m}=") do |value| write(value, *location) end end def each_configs(*configs, &block) c = configs.first.is_a?(::Hash) ? configs.first : (configs || []) c.each do |m, location| block.call(m, location || [m.to_s]) end end end def initialize(config, &block) @config = config block.call(self) if ::Kernel.block_given? end def attributes @config end private def find(*keys) if keys.empty? @config else keys.inject(@config) do |c, k| c.is_a?(::Hash) ? c[k] : (break c) end end end def write(value, *keys) key = keys.pop owner = find(*keys) owner[key] = value end def read(*keys, &block) value = find(*keys) ::Kernel.block_given? ? block.call(value) : value end end end end end end ================================================ FILE: lib/thinreports/basic_report/core/shape/base/interface.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Core module Shape module Base # @abstract class Interface include Utils extend Forwardable def self.internal_delegators(*args) def_delegators :internal, *args end private_class_method :internal_delegators attr_reader :internal def initialize(parent, format, internal = nil) @internal = internal || init_internal(parent, format) end def copy(parent) self.class.new(parent, internal.format, internal.copy(parent)) end private # @param [Thinreports::BasicReport::Report::Page, Thinreports::BasicReport::Core::Shape::List::SectionInterface] parent # @param [Thinreports::BasicReport::Core::Shape::Basic::Format] format # @return [Thinreports::BasicReport::Core::Shape::Basic::Internal] # @abstract def init_internal(parent, format) raise NotImplementedError end end end end end end end ================================================ FILE: lib/thinreports/basic_report/core/shape/base/internal.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Core module Shape module Base # @abstract class Internal include Utils extend Forwardable def self.format_delegators(*args) def_delegators :format, *args end private_class_method :format_delegators attr_reader :parent attr_reader :format attr_writer :style attr_accessor :states def initialize(parent, format) @parent = parent @format = format @states = {} @style = nil @finalized_attributes = nil end def style raise NotImplementedError end def copy(new_parent, &block) new_internal = self.class.new(new_parent, format) new_internal.style = style.copy new_internal.states = deep_copy(states) block.call(new_internal) if block_given? new_internal end def type_of? raise NotImplementedError end end end end end end end ================================================ FILE: lib/thinreports/basic_report/core/shape/base.rb ================================================ # frozen_string_literal: true require 'forwardable' require_relative 'base/internal' require_relative 'base/interface' ================================================ FILE: lib/thinreports/basic_report/core/shape/basic/block_format.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Core module Shape module Basic class BlockFormat < Basic::Format config_reader :value end end end end end end ================================================ FILE: lib/thinreports/basic_report/core/shape/basic/block_interface.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Core module Shape module Basic class BlockInterface < Basic::Interface # @overload value(val) # Set a val # @param [Object] val # @return [self] # @overload value # Return the value # @return [Object] def value(*args) if args.empty? internal.read_value else internal.write_value(args.first) self end end # @param [Object] val def value=(val) value(val) end end end end end end end ================================================ FILE: lib/thinreports/basic_report/core/shape/basic/block_internal.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Core module Shape module Basic class BlockInternal < Basic::Internal format_delegators :box def style @style ||= Style::Basic.new(format) end def read_value states.key?(:value) ? states[:value] : format.value.dup end alias value read_value def write_value(val) states[:value] = val end def real_value read_value end def type_of?(type_name) type_name == :block end end end end end end end ================================================ FILE: lib/thinreports/basic_report/core/shape/basic/format.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Core module Shape module Basic class Format < Core::Format::Base include Utils config_reader :type, :id config_reader :style config_checker true, :display config_reader follow_stretch: %w[follow-stretch] def affect_bottom_margin? attributes.fetch('affect-bottom-margin', true) end end end end end end end ================================================ FILE: lib/thinreports/basic_report/core/shape/basic/interface.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Core module Shape module Basic class Interface < Base::Interface internal_delegators :type # @return [String] def id internal.id.dup end # @param [Boolean] visibility # @return [self] def visible(visibility) internal.style.visible = visibility self end # @return [Boolean] def visible? internal.style.visible end # @overload style(style_name) # @param [Symbol] style_name # @return [Object] # @overload style(style_name, value) # @param [Symbol] style_name # @param [String, Symbol, Number, Array] value # @return [self] # @overload style(style_name, value1, value2) # @param [Symbol] style_name # @param [String, Number] value1 # @param [String, Number] value2 # @return [self] def style(*args) case args.length when 1 internal.style[args.first] when 2 internal.style[args.first] = args.last self when 3 internal.style[args.shift] = args self else raise ArgumentError, '#style requires 1 or 2, 3 arguments' end end # @param [Hash] settings style_name: value # @return [self] def styles(settings) settings.each { |args| style(*args) } self end # @see #visible # @return [self] def hide visible(false) self end # @see #visible # @return [self] def show visible(true) self end private # @see Thinreports::BasicReport::Core::Shape::Base::Interface#init_internal def init_internal(parent, format) Basic::Internal.new(parent, format) end end end end end end end ================================================ FILE: lib/thinreports/basic_report/core/shape/basic/internal.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Core module Shape module Basic class Internal < Base::Internal # Delegate to Format's methods format_delegators :id, :type def style @style ||= Style::Graphic.new(format) end def type_of?(type_name) [:basic, type].include?(type_name) end def identifier "#{id}#{style.identifier}" end end end end end end end ================================================ FILE: lib/thinreports/basic_report/core/shape/basic.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Core module Shape module Basic TYPE_NAMES = %w[line rect ellipse image].freeze end end end end end require_relative 'basic/format' require_relative 'basic/internal' require_relative 'basic/interface' require_relative 'basic/block_format' require_relative 'basic/block_internal' require_relative 'basic/block_interface' ================================================ FILE: lib/thinreports/basic_report/core/shape/image_block/format.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Core module Shape module ImageBlock class Format < Basic::BlockFormat end end end end end end ================================================ FILE: lib/thinreports/basic_report/core/shape/image_block/interface.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Core module Shape module ImageBlock class Interface < Basic::BlockInterface # @see #value alias src value # @see #value= alias src= value= private # @see Thinreports::BasicReport::Core::Shape::Base::Interface#init_internal def init_internal(parent, format) ImageBlock::Internal.new(parent, format) end end end end end end end ================================================ FILE: lib/thinreports/basic_report/core/shape/image_block/internal.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Core module Shape module ImageBlock class Internal < Basic::BlockInternal alias src read_value def type_of?(type_name) type_name == ImageBlock::TYPE_NAME || super end end end end end end end ================================================ FILE: lib/thinreports/basic_report/core/shape/image_block.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Core module Shape module ImageBlock TYPE_NAME = 'image-block' end end end end end require_relative 'image_block/format' require_relative 'image_block/internal' require_relative 'image_block/interface' ================================================ FILE: lib/thinreports/basic_report/core/shape/list/format.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Core module Shape module List class Format < Basic::Format config_reader height: %w[content-height] config_checker true, auto_page_break: %w[auto-page-break] # @deprecated config_reader :header, :detail, :footer # @deprecated config_reader page_footer: %w[page-footer] config_checker true, has_header: %w[header enabled] config_checker true, has_footer: %w[footer enabled] config_checker true, has_page_footer: %w[page-footer enabled] config_reader page_footer_height: %w[page-footer height] config_reader footer_height: %w[footer height] config_reader header_height: %w[header height] config_reader detail_height: %w[detail height] attr_reader :sections def initialize(*) super initialize_sections end # @param [Symbol] section_name # @return [Hash] # @deprecated def section(section_name) __send__(section_name) end # @param [Symbol] section_name # @return [Boolean] def has_section?(section_name) section_name == :detail ? true : __send__(:"has_#{section_name}?") end # @param [Symbol] section_name # @return [Numeric] def section_height(section_name) has_section?(section_name) ? __send__(:"#{section_name}_height") : 0 end # @param [:detai, :header, :page_footer, :footer] section_name # @return [Numeric] def section_base_position_top(section_name) section = @sections[section_name] return 0 unless has_section?(section_name) top = section.relative_top case section_name when :page_footer top - section_height(:detail) when :footer top - section_height(:detail) - section_height(:page_footer) else top end end private def initialize_sections @sections = { detail: List::SectionFormat.new(attributes['detail']) } @sections[:header] = section_format('header') if has_section?(:header) @sections[:page_footer] = section_format('page-footer') if has_section?(:page_footer) @sections[:footer] = section_format('footer') if has_section?(:footer) end def section_format(section_name) List::SectionFormat.new(attributes[section_name]) end end end end end end end ================================================ FILE: lib/thinreports/basic_report/core/shape/list/manager.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Core module Shape module List class Manager include Utils # @return [Thinreports::BasicReport::Core::Shape:::List::Page] attr_reader :current_page # @return [Thinreports::BasicReport::Core::Shape::List::PageState] attr_reader :current_page_state # @return [Integer] attr_accessor :page_count # @return [Proc] attr_accessor :page_finalize_handler # @return [Proc] attr_accessor :page_footer_handler # @return [Proc] attr_accessor :footer_handler # @param [Thinreports::BasicReport::Core::Shape::List::Page] page def initialize(page) switch_current!(page) @finalized = false @page_count = 0 @page_finalize_handler = nil @page_footer_handler = nil @footer_handler = nil end # @param [Thinreports::BasicReport::Core::Shape::List::Page] page # @return [Thinreports::BasicReport::Core::Shape::List::Manager] def switch_current!(page) @current_page = page @current_page_state = page.internal self end # @yield [new_list] # @yieldparam [Thinreports::BasicReport::Core::Shape::List::Page] new_list def change_new_page(&block) finalize_page new_page = report.internal.copy_page block.call(new_page.list(current_page.id)) if block_given? end # @param [Hash] values ({}) # @yield [header] # @yieldparam [Thinreports::BasicReport::Core::Shape::List::SectionInterface] header # @return [Thinreports::BasicReport::Core::Shape::List::SectionInterface] # @raise [Thinreports::BasicReport::Errors::DisabledListSection] def build_header(values = {}, &block) raise Thinreports::BasicReport::Errors::DisabledListSection, 'header' unless format.has_header? current_page_state.header ||= init_section(:header) build_section(header_section, values, &block) end # @return [Thinreports::BasicReport::Core::Shape::List::SectionInterface] def header_section current_page_state.header end # @param (see #build_section) # @return [Boolean] def add_detail(values = {}, &block) return false if current_page_state.finalized? successful = true if overflow_with?(:detail) if auto_page_break? change_new_page do |new_list| new_list.manager.insert_detail(values, &block) end else finalize successful = false end else insert_detail(values, &block) end successful end # @param values (see Thinreports::BasicReport::Core::Shape::Manager::Target#values) # @yield [section,] # @yieldparam [Thinreports::BasicReport::Core::Shape::List::SectionInterface] section # @return [Thinreports::BasicReport::Core::Shape::List::SectionInterface] def insert_detail(values = {}, &block) detail = build_section(init_section(:detail), values, &block) insert_row(detail) end # @param [Thinreports::BasicReport::Core::Shape::List::SectionInterface] row # @return [Thinreports::BasicReport::Core::Shape::List::SectionInterface] def insert_row(row) row.internal.move_top_to(current_page_state.height) current_page_state.rows << row current_page_state.height += row.height row end # @param [Thinreports::BasicReport::Core::Shape::List::SectionInterface] section # @param values (see Thinreports::BasicReport::Core::Shape::Manager::Target#values) # @yield [section,] # @yieldparam [Thinreports::BasicReport::Core::Shape::List::SectionInterface] section # @return [Thinreports::BasicReport::Core::Shape::List::SectionInterface] def build_section(section, values = {}, &block) section.values(values) call_block_in(section, &block) end # @param [Symbol] section_name # @return [Thinreports::BasicReport::Core::Shape::List::SectionInterface] def init_section(section_name) List::SectionInterface.new(current_page, format.sections[section_name], section_name) end # @param [Symbol] section_name # @return [Boolean] def overflow_with?(section_name = :detail) max_height = page_max_height max_height += format.section_height(:page_footer) if section_name == :footer && format.has_page_footer? height = format.section_height(section_name) (current_page_state.height + height) > max_height end # @return [Numeric] def page_max_height @page_max_height ||= begin h = format.height h -= format.section_height(:page_footer) h -= format.section_height(:footer) unless auto_page_break? h end end # @return [Boolean] def auto_page_break? format.auto_page_break? end # @param [Hash] options # @option options [Boolean] :ignore_page_footer (false) # When the switch of the page is generated by #finalize, it is used. def finalize_page(options = {}) return if current_page_state.finalized? build_header if format.has_header? finalize_page_footer unless options[:ignore_page_footer] current_page_state.finalized! @page_finalize_handler.call if @page_finalize_handler @page_count += 1 current_page_state.no = @page_count end def finalize_page_footer return unless format.has_page_footer? page_footer = init_section(:page_footer) insert_row(page_footer) @page_footer_handler.call(page_footer) if @page_footer_handler end def finalize return if finalized? finalize_page if format.has_footer? footer = init_section(:footer) @footer_handler.call(footer) if @footer_handler if auto_page_break? && overflow_with?(:footer) change_new_page do |new_list| new_list.manager.insert_row(footer) new_list.manager.finalize_page(ignore_page_footer: true) end else insert_row(footer) end end @finalized = true end # @return [Boolean] def finalized? @finalized end # @return [Thinreports::BasicReport::Report::Base] def report current_page_state.parent.report end # @return [Thinreports::BasicReport::Layout::Base] def layout current_page_state.parent.layout end # @return [Thinreports::BasicReport::Core::Shape::List::Format] def format current_page_state.format end end end end end end end ================================================ FILE: lib/thinreports/basic_report/core/shape/list/page.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Core module Shape module List class Page < Basic::Interface extend Forwardable attr_reader :manager # @param [Thinreports::BasicReport::Report::Page] parent # @param [Thinreports::BasicReport::Core::Shape::Basic::Format] format # @param [Thinreports::BasicReport::Core::Shape::List::PageState] internal (nil) # @param [Thinreports::BasicReport::Core::Shape::List::Manager] manager (nil) def initialize(parent, format, internal = nil, manager = nil) super(parent, format, internal) @manager = if manager manager.switch_current!(self) else List::Manager.new(self) end # Set a reference to List::PageState List::Manager self.internal.manager = self.manager end # @see Thinreports::BasicReport::Core::Shape::List::Manager#build_header def_delegator :manager, :build_header, :header def on_page_finalize(&block) manager.page_finalize_handler = block end # @yield [page_footer] # @yieldparam [Thinreports::BasicReport::Core::Shape::List::SectionInterface] page_footer def on_page_footer_insert(&block) manager.page_footer_handler = block end # @yield [footer] # @yieldparam [Thinreports::BasicReport::Core::Shape::List::SectionInterface] footer # @return [Thinreports::BasicReport::Core::Shape::List::SectionInterface] def on_footer_insert(&block) manager.footer_handler = block end # @param [Hash] values ({}) # @yield [row,] # @yieldparam [Thinreports::BasicReport::Core::Shape::List::SectionInterface] row # @return [Boolean] def add_row(values = {}, &block) manager.add_detail(values, &block) end # If enabled, the auto-page-break of the list will force a page break # at the time this method is called. Otherwise, this list will be finalized. def page_break if manager.auto_page_break? manager.change_new_page else manager.finalize_page end end alias finalize page_break # @return [Boolean] Returns true if list has overflowed # when `list#add_row` is called at the next time. def overflow? manager.overflow_with?(:detail) end # @param [Thinreports::BasicReport::Report::Page] new_parent # @return [Thinreports::BasicReport::Core::Shape::List::Page] def copy(new_parent) if manager.auto_page_break? new_list = self.class.new(new_parent, internal.format, nil, manager) else manager.finalize new_list = self.class.new(new_parent, internal.format, internal.copy(new_parent), manager) internal.rows.each do |row| new_list.internal.rows << row.copy(new_list) end new_list.internal.finalized! end new_list.internal.header = internal.header.copy(new_list) if internal.format.has_header? new_list end private # @see Thinreports::BasicReport::Core::Shape::Base::Interface#init_internal def init_internal(parent, format) List::PageState.new(parent, format) end end # Alias to List::Page. # @see Thinreports::BasicReport::Core::Shape::List::Page List::Interface = List::Page end end end end end ================================================ FILE: lib/thinreports/basic_report/core/shape/list/page_state.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Core module Shape module List class PageState < Basic::Internal attr_reader :rows attr_accessor :height attr_accessor :header attr_accessor :no attr_accessor :manager def initialize(*args) super(*args) @rows = [] @height = 0 @finalized = false @header = nil end def style @style ||= Style::Basic.new(format) end def finalized? @finalized end def finalized! @finalized = true end def type_of?(type_name) type_name == List::TYPE_NAME end end # Alias to List::PageState. # @see Thinreports::BasicReport::Core::Shape::List::PageState List::Internal = List::PageState end end end end end ================================================ FILE: lib/thinreports/basic_report/core/shape/list/section_format.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Core module Shape module List class SectionFormat < Shape::Manager::Format config_reader :height config_reader relative_left: %w[translate x], relative_top: %w[translate y] config_reader :style # For compatible 0.8.x format API config_checker true, display: %w[enabled] def initialize(*) super initialize_items(attributes['items']) end private def initialize_items(item_schemas) item_schemas.each do |item_schema| id, type = item_schema.values_at 'id', 'type' next if id.empty? shapes[id.to_sym] = Core::Shape::Format(type).new(item_schema) end end end end end end end end ================================================ FILE: lib/thinreports/basic_report/core/shape/list/section_interface.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Core module Shape module List class SectionInterface < Base::Interface include Core::Shape::Manager::Target undef_method :list internal_delegators :height # @param [Thinreports::BasicReport::Core::Shape::List::Page] parent # @param [Thinreports::BasicReport::Core::Shape::List::SectionFormat] format # @param [Symbol] section_name def initialize(parent, format, section_name) super(parent, format) internal.section_name = section_name initialize_manager(format) do |f| Core::Shape::Interface(self, f) end end # @param [Thinreports::BasicReport::Core::Shape::List::Page] parent # @return [Thinreports::BasicReport::Core::Shape::List::SectionInterface] def copy(parent) new_sec = super new_sec.internal.section_name = internal.section_name manager.shapes.each do |id, shape| new_sec.manager.shapes[id] = shape.copy(new_sec) end new_sec end private # @param parent (see #initialize) # @param format (see #initialize) # @return [Thinreports::BasicReport::Core::Shape::List::SectionInternal] def init_internal(parent, format) List::SectionInternal.new(parent, format) end end end end end end end ================================================ FILE: lib/thinreports/basic_report/core/shape/list/section_internal.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Core module Shape module List class SectionInternal < Base::Internal format_delegators :height, :relative_left # @return [Symbol] attr_accessor :section_name def style @style ||= Style::Base.new(format) end # @param [Numeric] ry def move_top_to(ry) states[:relative_top] = ry end # @return [Float] def relative_top states[:relative_top] || 0 end end end end end end end ================================================ FILE: lib/thinreports/basic_report/core/shape/list.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Core module Shape module List TYPE_NAME = 'list' end end end end end require_relative 'list/format' require_relative 'list/manager' require_relative 'list/page' require_relative 'list/page_state' require_relative 'list/section_format' require_relative 'list/section_interface' require_relative 'list/section_internal' ================================================ FILE: lib/thinreports/basic_report/core/shape/manager/format.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Core module Shape module Manager class Format < Core::Format::Base # @return [Symbol, Integer] attr_reader :identifier def initialize(config, id = nil, &block) super(config, &block) @identifier = id || object_id end def find_shape(id) shapes[id] end def has_shape?(id) shapes.key?(id) end def shapes @shapes ||= {} end end end end end end end ================================================ FILE: lib/thinreports/basic_report/core/shape/manager/internal.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Core module Shape module Manager class Internal include Utils attr_reader :format attr_reader :shapes attr_reader :lists # @param [Thinreports::BasicReport::Core::Manager::Format] format # @param [Proc] init_item_handler def initialize(format, init_item_handler) @format = format @shapes = {} @lists = {} @init_item_handler = init_item_handler end # @param [String, Symbol] id # @return [Thinreports::BasicReport::Core::Shape::Basic::Format] def find_format(id) format.find_shape(id.to_sym) end # @param [String, Symbol] id # @param limit (see #valid_type?) def find_item(id, limit = {}) id = id.to_sym if shapes.key?(id) shape = shapes[id] valid_type?(shape.type, limit) ? shape : nil elsif find_format(id) shape_format = find_format(id) return nil unless valid_type?(shape_format.type, limit) shape = init_item(shape_format) shapes[id] = shape shape end end # @param [String, Symbol] id # @return [Thinreports::BasicReport::Core::Shape::Base::Interface, nil] def final_shape(id) shape = shapes[id] # When shape was found in registry. if shape return nil unless shape.visible? # In the case of TextBlock or ImageBlock. if shape.internal.type_of?(:block) blank_value?(shape.internal.real_value) ? nil : shape else shape end # When shape was not found in registry. elsif format.has_shape?(id) shape_format = find_format(id) return nil unless shape_format.display? case shape_format.type # In the case of TextBlock. when TextBlock::TYPE_NAME return nil if !shape_format.has_reference? && blank_value?(shape_format.value) init_item(shape_format) # In the case of ImageBlock, Return the nil constantly. when ImageBlock::TYPE_NAME nil else init_item(shape_format) end end end # @param [Thinreports::BasicReport::Core::Shape::Basic::Format] format def init_item(format) @init_item_handler.call(format) end # @param [String] type # @param [Hash] limit # @option limit [String] :only # @option limit [String] :except # @return [Booldan] def valid_type?(type, limit = {}) return true if limit.empty? if limit[:only] type == limit[:only] elsif limit[:except] type != limit[:except] else raise ArgumentError end end end end end end end end ================================================ FILE: lib/thinreports/basic_report/core/shape/manager/target.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Core module Shape module Manager module Target include Utils attr_reader :manager # @example # item(:title).value('Title').style(:fill, 'red') # # item(:title) do # value('Title') # style(:fill, 'red') # end # # item(:title) do |t| # t.value('Title') # t.style(:fill, 'red') # end # item(:list) # => Error: UnknownItemId # item(:unknown_id) # => Error: UnknownItemId # @param [String, Symbol] id # @yield [item,] # @yieldparam [Thinreports::BasicReport::Core::Shape::Base::Interface] item # @raise [Thinreports::BasicReport::Errors::UnknownItemId] # @return [Thinreports::BasicReport::Core::Shape::Base::Interface] def item(id, &block) shape = find_item(id, except: Core::Shape::List::TYPE_NAME) raise Thinreports::BasicReport::Errors::UnknownItemId, id unless shape call_block_in(shape, &block) end # @example # page[:text_block].style(:bold, true) # page[:rect].style(:border_color, 'red') # # page[:list] # => Error: UnknownItemId # page[:unknown_id] # => Error: UnknownItemId # @param [String, Symbol] id # @return [Thinreports::BasicReport::Core::Shape::Base::Interface] def [](id) item(id) end # @example # page[:text_block] = 'Title' # page[:image_block] = '/path/to/image.png' # page[:list] = 'value' # => Error: UnknownItemId # page[:ellipse] = 'value' # => Error: NoMethodError #value # page[:unknown_id] = 'value' # => Error: UnknownItemId # @param [String, Symbol] id # @param [Object] value def []=(id, value) item(id).value = value end # @example # page.values text_block: 'value', # image_block: '/path/to/image.png' # @param [Hash] item_values def values(item_values) item_values.each { |id, val| item(id).value(val) } end # @param [Symbol, String] id # @return [Boolean] def item_exists?(id) !manager.find_format(id).nil? end alias exists? item_exists? # @example # report.list.add_row do |row| # row.item(:price).value(1000) # end # # report.list(:list_id) # => List # report.list(:text_block_id) # => raises UnknownItemId # @see #item def list(id = nil, &block) shape = find_item(id ||= :default, only: Core::Shape::List::TYPE_NAME) raise Thinreports::BasicReport::Errors::UnknownItemId.new(id, 'List') unless shape manager.lists[id.to_sym] ||= shape call_block_in(shape, &block) end private # @param format (see Thinreports::BasicReport::Core::Shape::Manager::Internal#initialize) # @yield [format] Handler for initialize item. # @yieldparam [Thinreports::BasicReport::Core::Shape::Basic::Format] format def initialize_manager(format, &block) @manager = Manager::Internal.new(format, block) end # @see Thinreports::BasicReport::Core::Shape::Manager::Internal#find_item def find_item(id, limit = {}) manager.find_item(id, limit) end end end end end end end ================================================ FILE: lib/thinreports/basic_report/core/shape/manager.rb ================================================ # frozen_string_literal: true require_relative 'manager/format' require_relative 'manager/target' require_relative 'manager/internal' ================================================ FILE: lib/thinreports/basic_report/core/shape/page_number/format.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Core module Shape module PageNumber class Format < Basic::Format config_reader :target config_reader default_format: %w[format] # For saving compatible 0.8.x format API config_reader overflow: %w[style overflow] def id @id ||= blank_value?(read('id')) ? self.class.next_default_id : read('id') end def for_report? blank_value?(target) end def self.next_default_id @id_counter ||= 0 "__pageno#{@id_counter += 1}" end end end end end end end ================================================ FILE: lib/thinreports/basic_report/core/shape/page_number/interface.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Core module Shape module PageNumber class Interface < Basic::Interface internal_delegators :reset_format def format(*args) if args.empty? internal.read_format else internal.write_format(args.first) self end end private # @see Thinreports::BasicReport::Core::Shape::Base::Interface#init_internal def init_internal(parent, format) PageNumber::Internal.new(parent, format) end end end end end end end ================================================ FILE: lib/thinreports/basic_report/core/shape/page_number/internal.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Core module Shape module PageNumber class Internal < Basic::Internal format_delegators :box, :for_report? def read_format states.key?(:format) ? states[:format] : format.default_format.dup end def reset_format states.delete(:format) end def write_format(format) states[:format] = format.to_s end def build_format(page_no, page_count) return '' if blank_value?(read_format) if start_page_number > 1 page_no += start_page_number - 1 page_count += start_page_number - 1 end read_format.dup.tap do |f| f.gsub! '{page}', page_no.to_s f.gsub! '{total}', page_count.to_s end end def style @style ||= PageNumber::Style.new(format) end def type_of?(type_name) type_name == PageNumber::TYPE_NAME end def start_page_number for_report? ? parent.report.start_page_number : 1 end end class Style < Style::Text accessible_styles.delete :valign end end end end end end ================================================ FILE: lib/thinreports/basic_report/core/shape/page_number.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Core module Shape module PageNumber TYPE_NAME = 'page-number' end end end end end require_relative 'page_number/format' require_relative 'page_number/internal' require_relative 'page_number/interface' ================================================ FILE: lib/thinreports/basic_report/core/shape/stack_view/format.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Core module Shape module StackView class Format < Basic::Format attr_reader :rows def initialize(*) super initialize_rows end private def initialize_rows @rows = [] attributes['rows'].each do |row| @rows << StackView::RowFormat.new(row) end end end end end end end end ================================================ FILE: lib/thinreports/basic_report/core/shape/stack_view/interface.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Core module Shape module StackView class Interface < Basic::Interface private def init_internal(parent, format) StackView::Internal.new(parent, format) end end end end end end end ================================================ FILE: lib/thinreports/basic_report/core/shape/stack_view/internal.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Core module Shape module StackView class Internal < Basic::Internal def initialize(parent, format) super @rows = [] end attr_accessor :rows def type_of?(type_name) type_name == StackView::TYPE_NAME end end end end end end end ================================================ FILE: lib/thinreports/basic_report/core/shape/stack_view/row_format.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Core module Shape module StackView class RowFormat < Core::Format::Base config_reader :id config_reader :height config_checker true, :display config_checker true, auto_stretch: 'auto-stretch' attr_reader :items def initialize(*) super @items = [] @item_with_ids = {} initialize_items(attributes['items']) end def find_item(id) @item_with_ids[id.to_sym] end private def initialize_items(item_schemas) item_schemas.each do |item_schema| item = Core::Shape::Format(item_schema['type']).new(item_schema) @items << item @item_with_ids[item.id.to_sym] = item unless item.id.empty? end end end end end end end end ================================================ FILE: lib/thinreports/basic_report/core/shape/stack_view.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Core module Shape module StackView TYPE_NAME = 'stack-view'.freeze end end end end end require_relative 'stack_view/format' require_relative 'stack_view/interface' require_relative 'stack_view/internal' require_relative 'stack_view/row_format' ================================================ FILE: lib/thinreports/basic_report/core/shape/style/base.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Core module Shape module Style class Base include Utils class << self # @param [Symbol] style_method # @param [String] style # @return [void] def style_accessor(style_method, style) style_reader(style_method, style) style_writer(style_method, style) end # @see .style_accessor def style_reader(style_method, style) define_method(style_method) do read_internal_style(style) end end # @see .style_accessor def style_writer(style_method, style) define_method(:"#{style_method}=") do |value| write_internal_style(style, value) end end # @param [Array] style_methods def style_accessible(*style_methods) accessible_styles.concat(style_methods) end # @return [Array] def accessible_styles @accessible_styles ||= [] end def inherited(s) s.accessible_styles.concat(accessible_styles.dup) end end # @return [Hash] attr_accessor :styles # @see .accessible_styles attr_reader :accessible_styles # @param [Thinreports::BasicReport::Core::Format::Base] format # @param [Hash] default_styles ({}) def initialize(format, default_styles = {}) @format = format @styles = default_styles @base_styles = format.style || {} @accessible_styles = self.class.accessible_styles.dup end # @param [Symbol] style_method # @return [Object] def [](style_method) verify_style_method(style_method) send(style_method.to_sym) end # @param [Symbol] style_method # @param [String, Number, Array] value def []=(style_method, value) verify_style_method(style_method) send(:"#{style_method}=", value) end # @return [String] def identifier create_identifier(@styles) end # @return [self] def copy self.class.new(@format, @styles.empty? ? {} : deep_copy(@styles)) end # @param [String, Symbol] style # @return [Object] def read_internal_style(style) style = style.to_s @styles.key?(style) ? @styles[style] : @base_styles[style] end # @param [String, Symbol] style # @param [Object] value def write_internal_style(style, value) @styles[style.to_s] = value end # @param [Symbol] style_method # @return [Boolean] def has_style?(style_method) accessible_styles.include?(style_method) end # @return [Hash] def finalized_styles @finalized_styles ||= if @styles.empty? @base_styles.dup else @base_styles.merge(@styles) end end private # @param [Hash] s # @return [String] def create_identifier(s) s.empty? ? '' : s.hash.to_s end # @param [Symbol] style_method # @raise Thinreports::BasicReport::Errors::UnknownShapeStyleName def verify_style_method(style_method) return if has_style?(style_method) raise Thinreports::BasicReport::Errors::UnknownShapeStyleName.new( style_method, accessible_styles ) end # @param [Object] value # @param [Array] allows # @param [String] msg (nil) # @raise ArgumentError def verify_style_value(value, allows, msg = nil) raise ArgumentError, msg unless allows.include?(value) end end end end end end end ================================================ FILE: lib/thinreports/basic_report/core/shape/style/basic.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Core module Shape module Style class Basic < Style::Base style_accessible :visible, :offset_x, :offset_y attr_accessor :visible style_accessor :offset_x, 'offset-x' style_accessor :offset_y, 'offset-y' def initialize(*args) super @visible = @format.display? end end end end end end end ================================================ FILE: lib/thinreports/basic_report/core/shape/style/graphic.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Core module Shape module Style class Graphic < Style::Basic style_accessible :border_color, :border_width, :fill_color, :border # @method border_color # @return [String] # @method border_color=(color) # @param [String] color style_accessor :border_color, 'border-color' # @method border_width # @return [Number] style_accessor :border_width, 'border-width' # @method fill_color # @return [String] # @method fill_color=(color) # @param [String] color style_accessor :fill_color, 'fill-color' # @return [Array] def border [border_width, border_color] end # @param [Array] width_and_color def border=(width_and_color) w, c = width_and_color self.border_width = w self.border_color = c end end end end end end end ================================================ FILE: lib/thinreports/basic_report/core/shape/style/text.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Core module Shape module Style class Text < Style::Basic style_accessible :bold, :italic, :underline, :linethrough, :align, :valign, :color, :font_size # @method color # @return [String] # @method color=(v) # @param [String] v style_accessor :color, 'color' # @method font_size # @return [Numeric, String] # @method font_size=(v) # @param [Numeric, String] v style_accessor :font_size, 'font-size' def initialize(*) super initialize_font_style end # @return [Boolean] def bold read_internal_style('font-style').include?('bold') end # @param [Boolean] enable def bold=(enable) write_font_style('bold', enable) end # @return [Boolean] def italic read_internal_style('font-style').include?('italic') end # @param [Boolean] enable def italic=(enable) write_font_style('italic', enable) end # @return [Boolean] def underline read_internal_style('font-style').include?('underline') end # @param [Boolean] enable def underline=(enable) write_font_style('underline', enable) end # @return [Boolean] def linethrough read_internal_style('font-style').include?('linethrough') end # @param [Boolean] enable def linethrough=(enable) write_font_style('linethrough', enable) end # @return [:left, :center, :right] def align read_internal_style('text-align').to_sym end # @param [:left, :center, :right] align_name def align=(align_name) verify_style_value(align_name, %i[left center right], 'Only :left or :center, :right can be specified as align.') write_internal_style('text-align', align_name.to_s) end # @return [:top, :middle, :bottom] def valign vertical_align = read_internal_style('vertical-align') blank_value?(vertical_align) ? :top : vertical_align.to_sym end # @param [:top, :center, :middle, :bottom] valign_name def valign=(valign_name) if valign_name == :center warn '[DEPRECATION] :center value for valign style is deprecated' \ ' and will be removed in thinreports-generator 1.0.' \ ' Please use :middle instead of :center.' valign_name = :middle end verify_style_value( valign_name, %i[top middle bottom], 'Only :top or :middle (:center), :bottom can be specified as valign.' ) write_internal_style('vertical-align', valign_name.to_s) end private def initialize_font_style styles['font-style'] ||= (@base_styles['font-style'] || []).dup end def write_font_style(style_name, enable) if enable styles['font-style'].push(style_name).uniq! else styles['font-style'].delete(style_name) end end end end end end end end ================================================ FILE: lib/thinreports/basic_report/core/shape/style.rb ================================================ # frozen_string_literal: true require_relative 'style/base' require_relative 'style/basic' require_relative 'style/graphic' require_relative 'style/text' ================================================ FILE: lib/thinreports/basic_report/core/shape/text/format.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Core module Shape module Text class Format < Basic::Format config_reader :texts config_reader valign: %w[style vertical-align] config_reader line_height: %w[style line-height] end end end end end end ================================================ FILE: lib/thinreports/basic_report/core/shape/text/interface.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Core module Shape module Text class Interface < Basic::Interface private # @see Thinreports::BasicReport::Core::Shape::Base::Interface#init_internal def init_internal(parent, format) Text::Internal.new(parent, format) end end end end end end end ================================================ FILE: lib/thinreports/basic_report/core/shape/text/internal.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Core module Shape module Text class Internal < Basic::Internal # Delegate to Format's methods format_delegators :texts, :box def style @style ||= Style::Text.new(format) end def type_of?(type_name) type_name == Text::TYPE_NAME end end end end end end end ================================================ FILE: lib/thinreports/basic_report/core/shape/text.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Core module Shape module Text TYPE_NAME = 'text' end end end end end require_relative 'text/format' require_relative 'text/internal' require_relative 'text/interface' ================================================ FILE: lib/thinreports/basic_report/core/shape/text_block/format.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Core module Shape module TextBlock class Format < Basic::BlockFormat # For saving compatible 0.8.x format API config_reader ref_id: %w[reference-id] config_reader valign: %w[style vertical-align] config_reader overflow: %w[style overflow] config_reader line_height: %w[style line-height] config_reader format_base: %w[format base], format_type: %w[format type], format_datetime_format: %w[format datetime format], format_number_delimiter: %w[format number delimiter], format_number_precision: %w[format number precision], format_padding_char: %w[format padding char], format_padding_dir: %w[format padding direction] config_checker true, multiple: %w[multiple-line] config_checker 'R', format_padding_rdir: %w[format padding direction] config_reader format_padding_length: %w[format padding length] do |len| blank_value?(len) ? 0 : len.to_i end config_reader has_format?: %w[format type] do |type| %w[datetime number padding].include?(type) end # For saving compatible 0.8.x format API def has_reference? !blank_value?(ref_id) end end end end end end end ================================================ FILE: lib/thinreports/basic_report/core/shape/text_block/formatter/basic.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Core module Shape module TextBlock module Formatter class Basic include Utils attr_reader :format def initialize(format) @format = format end def apply(value) value = apply_format_to(value) if applicable?(value) return value if blank_value?(format.format_base) format.format_base.gsub(/\{value\}/, value.to_s) end private def apply_format_to(value) value end def applicable?(_value) true end end end end end end end end ================================================ FILE: lib/thinreports/basic_report/core/shape/text_block/formatter/datetime.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Core module Shape module TextBlock module Formatter class Datetime < Formatter::Basic private def apply_format_to(value) value.strftime(format.format_datetime_format) end def applicable?(value) !blank_value?(format.format_datetime_format) && value.respond_to?(:strftime) end end end end end end end end ================================================ FILE: lib/thinreports/basic_report/core/shape/text_block/formatter/number.rb ================================================ # frozen_string_literal: true require 'bigdecimal' module Thinreports module BasicReport module Core module Shape module TextBlock module Formatter class Number < Formatter::Basic private def apply_format_to(value) precision = format.format_number_precision delimiter = format.format_number_delimiter if_applicable value do |val| val = number_with_precision(val, precision) unless blank_value?(precision) val = number_with_delimiter(val, delimiter) unless blank_value?(delimiter) val end end def if_applicable(value, &block) normalized_value = normalize(value) normalized_value.nil? ? value : block.call(normalized_value) end def normalize(value) if value.is_a?(String) convert_to_integer(value) || convert_to_float(value) else value end end def number_with_delimiter(value, delimiter = ',') value_int, value_float = value.to_s.split('.') [ value_int.gsub(/(\d)(?=(\d{3})+(?!\d))/) { "#{$1}#{delimiter}" }, value_float ].compact.join('.') end def number_with_precision(value, precision = 3) value = BigDecimal(value.to_s).round(precision) sprintf("%.#{precision}f", value) end def convert_to_integer(value) Integer(value) rescue ArgumentError nil end def convert_to_float(value) Float(value) rescue ArgumentError nil end end end end end end end end ================================================ FILE: lib/thinreports/basic_report/core/shape/text_block/formatter/padding.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Core module Shape module TextBlock module Formatter class Padding < Formatter::Basic private def apply_format_to(value) value.to_s.send(format.format_padding_rdir? ? :ljust : :rjust, format.format_padding_length, format.format_padding_char) end def applicable?(_value) !blank_value?(format.format_padding_char) && format.format_padding_length > 0 end end end end end end end end ================================================ FILE: lib/thinreports/basic_report/core/shape/text_block/formatter.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Core module Shape module TextBlock module Formatter include Utils # @param [Thinreports::BasicReport::Core::Shape::TextBlock::Format] format # @return [Thinreports::BasicReport::Core::Shape::TextBlock::Formatter::Base] def self.setup(format) klass = if blank_value?(format.format_type) Basic else case format.format_type when 'number' then Number when 'datetime' then Datetime when 'padding' then Padding else raise Thinreports::BasicReport::Errors::UnknownFormatterType end end klass.new(format) end end end end end end end require_relative 'formatter/basic' require_relative 'formatter/datetime' require_relative 'formatter/padding' require_relative 'formatter/number' ================================================ FILE: lib/thinreports/basic_report/core/shape/text_block/interface.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Core module Shape module TextBlock class Interface < Basic::BlockInterface internal_delegators :format_enabled? # @param [Boolean] enabled # @return [self] def format_enabled(enabled) internal.format_enabled(enabled) self end # @param [Object] val # @param [Hash] style_settings # @return [self] def set(val, style_settings = {}) value(val) styles(style_settings) #=> self end private # @see Thinreports::BasicReport::Core::Shape::Base::Interface#init_internal def init_internal(parent, format) TextBlock::Internal.new(parent, format) end end end end end end end ================================================ FILE: lib/thinreports/basic_report/core/shape/text_block/internal.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Core module Shape module TextBlock class Internal < Basic::BlockInternal format_delegators :multiple? attr_reader :style def initialize(*args) super(*args) @reference = nil @formatter = nil @style = Style::Text.new(format) @style.accessible_styles.delete(:valign) unless multiple? end def read_value if format.has_reference? @reference ||= parent.item(format.ref_id) @reference.value else super end end def write_value(val) if format.has_reference? warn 'The set value was not saved, ' \ "Because '#{format.id}' has reference to '#{format.ref_id}'." else super end end def real_value if format_enabled? formatter.apply(read_value) else super end end def format_enabled(enabled) states[:format_enabled] = enabled end def format_enabled? if states.key?(:format_enabled) states[:format_enabled] else !blank_value?(format.format_base) || format.has_format? end end def type_of?(type_name) type_name == TextBlock::TYPE_NAME || super end private def formatter @formatter ||= TextBlock::Formatter.setup(format) end end end end end end end ================================================ FILE: lib/thinreports/basic_report/core/shape/text_block.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Core module Shape module TextBlock TYPE_NAME = 'text-block' end end end end end require_relative 'text_block/format' require_relative 'text_block/internal' require_relative 'text_block/interface' require_relative 'text_block/formatter' ================================================ FILE: lib/thinreports/basic_report/core/shape.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Core module Shape def Interface(parent, format) find_by_type(format.type)::Interface.new(parent, format) end def Format(type) find_by_type(type)::Format end module_function :Interface, :Format def self.find_by_type(type) case type when TextBlock::TYPE_NAME then TextBlock when ImageBlock::TYPE_NAME then ImageBlock when List::TYPE_NAME then List when StackView::TYPE_NAME then StackView when Text::TYPE_NAME then Text when PageNumber::TYPE_NAME then PageNumber when *Basic::TYPE_NAMES then Basic else raise Thinreports::BasicReport::Errors::UnknownShapeType end end end end end end require_relative 'shape/style' require_relative 'shape/manager' require_relative 'shape/base' require_relative 'shape/basic' require_relative 'shape/text' require_relative 'shape/text_block' require_relative 'shape/image_block' require_relative 'shape/list' require_relative 'shape/stack_view' require_relative 'shape/page_number' ================================================ FILE: lib/thinreports/basic_report/core/utils.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Utils def self.included(klass) klass.extend self end def deep_copy(src) case src when Hash src.each_with_object({}) do |(k, v), h| h[k] = v.dup rescue v end when Array src.map do |a| a.dup rescue a end else raise ArgumentError end end def blank_value?(value) case value when String then value.empty? when NilClass then true else false end end def call_block_in(scope, &block) return scope unless block_given? if block.arity == 1 block.call(scope) else scope.instance_eval(&block) end scope end end extend Utils end end ================================================ FILE: lib/thinreports/basic_report/generator/pdf/document/draw_shape.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Generator class PDF module DrawShape # @param [Thinreports::BasicReport::Core::Shape::TextBlock::Internal] shape # @param [Numeric] height (nil) It will be used as rendering height if present. # Otherwise, the rendering height is the height of schema. # @param [:truncate, :shrink_to_fit, :expand] overflow (nil) It will be set the overflow attribute if present. def draw_shape_tblock(shape, height: nil, overflow: nil, &block) x, y, w = shape.format.attributes.values_at('x', 'y', 'width') h = height || shape.format.attributes['height'] content = shape.real_value.to_s return if content.empty? attrs = build_text_attributes(shape.style.finalized_styles) attrs[:overflow] = overflow if overflow unless shape.multiple? content = content.tr("\n", ' ') attrs[:single] = true end text_box(content, x, y, w, h, attrs, &block) end def draw_shape_pageno(shape, page_no, page_count) x, y, w, h = shape.format.attributes.values_at('x', 'y', 'width', 'height') attrs = build_text_attributes(shape.style.finalized_styles) text_box(shape.build_format(page_no, page_count), x, y, w, h, attrs) end # @param [Thinreports::BasicReport::Core::Shape::Basic::Internal] shape def draw_shape_image(shape) x, y, w, h = shape.format.attributes.values_at('x', 'y', 'width', 'height') image_data = shape.format.attributes['data'] base64image(image_data['base64'], x, y, w, h) end # @param [Thinreports::BasicReport::Core::Shape::ImageBlock::Internal] shape def draw_shape_iblock(shape) return if blank_value?(shape.src) x, y, w, h = shape.format.attributes.values_at('x', 'y', 'width', 'height') style = shape.style.finalized_styles image_box( shape.src, x, y, w, h, position_x: image_position_x(style['position-x']), position_y: image_position_y(style['position-y']), offset_x: style['offset-x'], offset_y: style['offset-y'] ) end def shape_iblock_dimenions(shape) return nil if blank_value?(shape.src) x, y, w, h = shape.format.attributes.values_at('x', 'y', 'width', 'height') style = shape.style.finalized_styles image_dimensions( shape.src, x, y, w, h, position_x: image_position_x(style['position-x']), position_y: image_position_y(style['position-y']) ) end # @param [Thinreports::BasicReport::Core::Shape::Text::Internal] shape def draw_shape_text(shape, dheight = 0) x, y, w, h = shape.format.attributes.values_at('x', 'y', 'width', 'height') text( shape.texts.join("\n"), x, y, w, h + dheight, build_text_attributes(shape.style.finalized_styles) ) end # @param [Thinreports::BasicReport::Core::Shape::Basic::Internal] shape def draw_shape_ellipse(shape) cx, cy, rx, ry = shape.format.attributes.values_at('cx', 'cy', 'rx', 'ry') ellipse(cx, cy, rx, ry, build_graphic_attributes(shape.style.finalized_styles)) end # @param [Thinreports::BasicReport::Core::Shape::Basic::Internal] shape def draw_shape_line(shape, dy1 = 0, dy2 = 0) x1, y1, x2, y2 = shape.format.attributes.values_at('x1', 'y1', 'x2', 'y2') line(x1, y1 + dy1, x2, y2 + dy2, build_graphic_attributes(shape.style.finalized_styles)) end # @param [Thinreports::BasicReport::Core::Shape::Basic::Internal] shape def draw_shape_rect(shape, dheight = 0) x, y, w, h = shape.format.attributes.values_at('x', 'y', 'width', 'height') rect_attributes = build_graphic_attributes(shape.style.finalized_styles) do |attrs| attrs[:radius] = shape.format.attributes['border-radius'] end rect(x, y, w, h + dheight, rect_attributes) end end end end end end ================================================ FILE: lib/thinreports/basic_report/generator/pdf/document/draw_template_items.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Generator class PDF module DrawTemplateItems # @param [Array] items def draw_template_items(items) items.each do |item_attributes| next unless drawable?(item_attributes) case item_attributes['type'] when 'text' then draw_text(item_attributes) when 'image' then draw_image(item_attributes) when 'rect' then draw_rect(item_attributes) when 'ellipse' then draw_ellipse(item_attributes) when 'line' then draw_line(item_attributes) end end end private # @param [Hash] item_attributes def draw_rect(item_attributes) x, y, w, h = item_attributes.values_at('x', 'y', 'width', 'height') graphic_attributes = build_graphic_attributes(item_attributes['style']) do |attrs| attrs[:radius] = item_attributes['border-radius'] end rect(x, y, w, h, graphic_attributes) end # @see #draw_rect def draw_ellipse(item_attributes) x, y, rx, ry = item_attributes.values_at('cx', 'cy', 'rx', 'ry') ellipse(x, y, rx, ry, build_graphic_attributes(item_attributes['style'])) end # @see #draw_rect def draw_line(item_attributes) x1, y1, x2, y2 = item_attributes.values_at('x1', 'y1', 'x2', 'y2') line(x1, y1, x2, y2, build_graphic_attributes(item_attributes['style'])) end # @see #draw_rect def draw_text(item_attributes) x, y, w, h = item_attributes.values_at('x', 'y', 'width', 'height') text( item_attributes['texts'].join("\n"), x, y, w, h, build_text_attributes(item_attributes['style']) ) end # @see #draw_rect def draw_image(item_attributes) x, y, w, h = item_attributes.values_at('x', 'y', 'width', 'height') image_data = item_attributes['data'] base64image(image_data['base64'], x, y, w, h) end def drawable?(item_attributes) item_attributes['id'].empty? && item_attributes['display'] end end end end end end ================================================ FILE: lib/thinreports/basic_report/generator/pdf/document/font.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Generator class PDF module Font FONT_STORE = Thinreports.root.join('fonts') BUILTIN_FONTS = { 'IPAMincho' => FONT_STORE.join('ipam.ttf').to_s, 'IPAPMincho' => FONT_STORE.join('ipamp.ttf').to_s, 'IPAGothic' => FONT_STORE.join('ipag.ttf').to_s, 'IPAPGothic' => FONT_STORE.join('ipagp.ttf').to_s }.freeze DEFAULT_FALLBACK_FONTS = %w[IPAMincho].freeze PRAWN_BUINTIN_FONT_ALIASES = { 'Courier New' => 'Courier', 'Times New Roman' => 'Times-Roman' }.freeze def setup_fonts # Install built-in fonts. BUILTIN_FONTS.each do |font_name, font_path| install_font(font_name, font_path) end # Create aliases from the font list provided by Prawn. PRAWN_BUINTIN_FONT_ALIASES.each do |alias_name, name| pdf.font_families[alias_name] = pdf.font_families[name] end # Setup custom fallback fonts fallback_fonts = Thinreports.config.fallback_fonts.uniq fallback_fonts.map!.with_index do |font, i| if pdf.font_families.key?(font) font else install_font "Custom-fallback-font#{i}", font end end # Set fallback fonts pdf.fallback_fonts(fallback_fonts + DEFAULT_FALLBACK_FONTS) end # @param [String] name # @param [String] file # @return [String] installed font name def install_font(name, file) raise Errors::FontFileNotFound unless File.exist?(file) pdf.font_families[name] = { normal: file, bold: file, italic: file, bold_italic: file } name end # @return [String] def default_family 'Helvetica' end # @param [String] family # @return [String] def default_family_if_missing(family) pdf.font_families.key?(family) ? family : default_family end # @param [String] font_name # @param [:bold, :italic] font_style # @return [Boolean] def font_has_style?(font_name, font_style) font = pdf.font_families[font_name] return false unless font return false unless font.key?(font_style) font[font_style] != font[:normal] end end end end end end ================================================ FILE: lib/thinreports/basic_report/generator/pdf/document/graphics/attributes.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Generator class PDF module Graphics # @param [Hash] style # @yield [attrs] # @yieldparam [Hash] attrs # @return [Hash] def build_graphic_attributes(style, &block) graphic_attributes = { stroke: style['border-color'], stroke_width: style['border-width'], stroke_type: style['border-style'], fill: style['fill-color'] } block.call(graphic_attributes) if block_given? graphic_attributes end # @param [Hash] style # @yield [attrs] # @yieldparam [Hash] attrs # @return [Hash] def build_text_attributes(style, &block) word_wrap = word_wrap(style['word-wrap']) text_attributes = { font: font_family(style['font-family']), size: style['font-size'], color: style['color'], align: text_align(style['text-align']), valign: text_valign(style['vertical-align']), styles: font_styles(style['font-style']), letter_spacing: letter_spacing(style['letter-spacing']), line_height: line_height(style['line-height']), overflow: text_overflow(style['overflow']), word_wrap: word_wrap, # Deprecated: Use overflow_wrap instead of word_wrap overflow_wrap: overflow_wrap(style['overflow-wrap'], word_wrap) } block.call(text_attributes) if block_given? text_attributes end def overflow_wrap(style, computed_word_wrap) case style || migrate_overflow_wrap_from_word_wrap(computed_word_wrap) when 'normal', nil then :normal when 'anywhere' then :anywhere # Deprecated: This is a temporary value for migrating from word_wrap. when 'disable-break-word-by-space' then :disable_break_word_by_space else :normal end end def migrate_overflow_wrap_from_word_wrap(computed_word_wrap) case computed_word_wrap when :none then 'disable-break-word-by-space' when :break_word then 'normal' else raise ArgumentError, 'Invalid computed word_wrap value' end end # @param [Array] font_names # @return [String] def font_family(font_names) font_name = font_names.first default_family_if_missing(font_name) end # @param [Array] styles # @return [Array] def font_styles(styles) styles.map do |font_style| case font_style when 'bold' then :bold when 'italic' then :italic when 'underline' then :underline when 'linethrough' then :strikethrough end end end # @param [Float, "", nil] spacing # @return [Float, nil] def letter_spacing(spacing) blank_value?(spacing) ? nil : spacing end # @param ["left", "center", "right", ""] align # @return [:left, :center, :right] def text_align(align) case align when 'left' then :left when 'center' then :center when 'right' then :right when '' then :left else :left end end # @param ["top", "middle", "bottom", "", nil] valign # @return [:top, :center, :bottom] def text_valign(valign) case valign when 'top' then :top when 'middle' then :center when 'bottom' then :bottom when '' then :top else :top end end # @param ["truncate", "fit", "expand", "", nil] overflow # @return [:truncate, :shrink_to_fit, :expand] def text_overflow(overflow) case overflow when 'truncate' then :truncate when 'fit' then :shrink_to_fit when 'expand' then :expand when '' then :truncate else :truncate end end # @param ["break-word", "none", "", nil] word_wrap # @return [:break_word, :none] def word_wrap(word_wrap) case word_wrap when 'break-word' then :break_word when 'none' then :none else :none end end # @param [Float, "", nil] height # @return [Float, nil] def line_height(height) blank_value?(height) ? nil : height end # @param ["left", "center", "right", ""] position # @return [:left, :center, :right] def image_position_x(position) case position when 'left' then :left when 'center' then :center when 'right' then :right when '' then :left else :left end end # @param ["top", "middle", "bottom", ""] position # @return [:left, :center, :right] def image_position_y(position) case position when 'top' then :top when 'middle' then :center when 'bottom' then :bottom when '' then :top else :top end end end end end end end ================================================ FILE: lib/thinreports/basic_report/generator/pdf/document/graphics/basic.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Generator class PDF module Graphics STROKE_DASH = { dashed: [2, 2], dotted: [1, 2] }.freeze # @param [Numeric, String] x1 # @param [Numeric, String] y1 # @param [Numeric, String] x2 # @param [Numeric, String] y2 # @param [Hash] attrs ({}) # @option attrs [String] :stroke # @option attrs [Numeric, String] :stroke_width # @option attrs ["solid", "dashed", "dotted"] :stroke_type def line(x1, y1, x2, y2, attrs = {}) with_graphic_styles(attrs) do pdf.line(pos(x1, y1), pos(x2, y2)) end end # @param [Numeric, String] x # @param [Numeric, String] y # @param [Numeric, String] w width # @param [Numeric, String] h height # @param [Hash] attrs ({}) # @option attrs [Integer, String] :radius # @option attrs [String] :stroke # @option attrs [Numeric, String] :stroke_width # @option attrs ["solid", "dashed", "dotted"] :stroke_type # @option attrs [String] :fill def rect(x, y, w, h, attrs = {}) w, h = s2f(w, h) radius = s2f(attrs[:radius]) with_graphic_styles(attrs) do if radius && !radius.zero? pdf.rounded_rectangle(pos(x, y), w, h, radius) else pdf.rectangle(pos(x, y), w, h) end end end # @param [Numeric, String] x center-x # @param [Numeric, String] y center-y # @param [Numeric, String] rx # @param [Numeric, String] ry # @param [Hash] attrs ({}) # @option attrs [String] :stroke # @option attrs [Numeric, String] :stroke_width # @option attrs [Array] :stroke_dash # @option attrs ["solid", "dashed", "dotted"] :stroke_type # @option attrs [String] :fill def ellipse(x, y, rx, ry, attrs = {}) rx, ry = s2f(rx, ry) with_graphic_styles(attrs) do pdf.ellipse(pos(x, y), rx, ry) end end # @param [Hash] attrs def with_graphic_styles(attrs, &block) stroke = build_stroke_styles(attrs) fill = build_fill_styles(attrs) # Do not draw if no colors given. return unless fill || stroke save_graphics_state # Apply stroke-dashed. if stroke && stroke[:dash] length, space = stroke[:dash] pdf.dash(length, space: space) end # Draw with fill and stroke. if fill && stroke pdf.fill_and_stroke do line_width(stroke[:width]) pdf.fill_color(fill[:color]) pdf.stroke_color(stroke[:color]) block.call end # Draw only with fill. elsif fill pdf.fill do pdf.fill_color(fill[:color]) block.call end # Draw only with stroke. elsif stroke pdf.stroke do line_width(stroke[:width]) pdf.stroke_color(stroke[:color]) block.call end end restore_graphics_state end # @param [Hash] styles # @return [Hash, nil] def build_stroke_styles(styles) color = styles[:stroke] width = styles[:stroke_width] return nil unless color && color != 'none' return nil unless width && width != 0 { color: parse_color(color), width: s2f(width), dash: STROKE_DASH[styles[:stroke_type].to_sym] } end # @param [Hash] styles # @return [Hash, nil] def build_fill_styles(styles) color = styles[:fill] return nil unless color && color != 'none' { color: parse_color(color) } end end end end end end ================================================ FILE: lib/thinreports/basic_report/generator/pdf/document/graphics/image.rb ================================================ # frozen_string_literal: true require 'tempfile' require 'base64' require 'digest/md5' module Thinreports module BasicReport module Generator class PDF module Graphics # @param [String, IO] filename_or_io # @param [Numeric, Strng] x # @param [Numeric, Strng] y # @param [Numeric, Strng] w # @param [Numeric, Strng] h def image(filename_or_io, x, y, w, h) w, h = s2f(w, h) pdf.image(filename_or_io, at: pos(x, y), width: w, height: h) end # @param [String] base64_data # @param [Numeric, Strng] x # @param [Numeric, Strng] y # @param [Numeric, Strng] w # @param [Numeric, Strng] h def base64image(base64_data, x, y, w, h) image_data = Base64.decode64(base64_data) image_id = Digest::MD5.hexdigest(base64_data) image_path = create_temp_imagefile(image_id, image_data) image(image_path, x, y, w, h) end # @param [String, IO] filename_or_io # @param [Numeric, Strng] x # @param [Numeric, Strng] y # @param [Numeric, Strng] w # @param [Numeric, Strng] h # @param [Hash] options # @option options [:left, :center, :right] :position_x (:left) # @option options [:top, :center, :bottom] :position_y (:top) # @option options [Numeric] :offset_x # @option options [Numeric] :offset_y def image_box(filename_or_io, x, y, w, h, options = {}) w, h = s2f(w, h) computed_position = pos( x + (options[:offset_x] || 0), y + (options[:offset_y] || 0) ) pdf.bounding_box(computed_position, width: w, height: h) do pdf.image( filename_or_io, position: options[:position_x] || :left, vposition: options[:position_y] || :top, auto_fit: [w, h] ) end end def image_dimensions(filename_or_io, x, y, w, h, options = {}) w, h = s2f(w, h) # XXX: Calling @private method _pdf_obj, info = pdf.build_image_object(filename_or_io) info.calc_image_dimensions( position: options[:position_x] || :left, vposition: options[:position_y] || :top, auto_fit: [w, h] ) end def clean_temp_images temp_image_registry.each_value(&:close!) temp_image_registry.clear end def temp_image_registry @temp_image_registry ||= {} end # @param [String] image_id # @param [String] image_data # @return [String] Path to imagefile def create_temp_imagefile(image_id, image_data) temp_image_registry[image_id] ||= begin file = Tempfile.new('temp-image') file.binmode file.write(image_data) file.open file end temp_image_registry[image_id].path end end end end end end ================================================ FILE: lib/thinreports/basic_report/generator/pdf/document/graphics/text.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Generator class PDF module Graphics # @param [String] content # @param [Numeric, String] x # @param [Numeric, String] y # @param [Numeric, String] w # @param [Numeric, String] h # @param [Hash] attrs ({}) # @option attrs [String] :font # @option attrs [Numeric, String] :size # @option attrs [String] :color # @option attrs [Array<:bold, :italic, :underline, :strikethrough>] # :styles (nil) # @option attrs [:left, :center, :right] :align (:left) # @option attrs [:top, :center, :bottom] :valign (:top) # @option attrs [Numeric, String] :line_height The total height of an text line. # @option attrs [Numeric, String] :letter_spacing # @option attrs [Boolean] :single (false) # @option attrs [:trancate, :shrink_to_fit, :expand] :overflow (:trancate) # @option attrs [:none, :break_word] :word_wrap (:none) # @option attrs [:normal, :anywhere, :disable_break_word_by_space] :overflow_wrap (:normal) def text_box(content, x, y, w, h, attrs = {}, &block) w, h = s2f(w, h) box_attrs = text_box_attrs( x, y, w, h, single: attrs.delete(:single), overflow: attrs[:overflow] ) content = replace_space_to_nbsp(content) if attrs[:overflow_wrap] == :disable_break_word_by_space with_text_styles(attrs) do |built_attrs, font_styles| if block block.call [{ text: content, styles: font_styles }], built_attrs.merge(box_attrs) else pdf.formatted_text_box( [{ text: content, styles: font_styles }], built_attrs.merge(box_attrs) ) end end rescue Prawn::Errors::CannotFit # Nothing to do. # # When the area is too small compared # with the content and the style of the text. # (See prawn/core/text/formatted/line_wrap.rb#L185) end # @see #text_box def text(content, x, y, w, h, attrs = {}) # Set the :overflow property to :shirink_to_fit. text_box(content, x, y, w, h, { overflow: :shirink_to_fit }.merge(attrs)) end private # @param x (see #text_box) # @param y (see #text_box) # @param w (see #text_box) # @param h (see #text_box) # @param [Hash] states # @option states [Boolean] :single # @option states [Symbold] :overflow # @return [Hash] def text_box_attrs(x, y, w, h, states = {}) attrs = { at: pos(x, y), width: s2f(w) } if states[:single] states[:overflow] != :expand ? attrs.merge(single_line: true) : attrs else attrs.merge(height: s2f(h)) end end # @param attrs (see #text) # @yield [built_attrs, font_styles] # @yieldparam [Hash] built_attrs The finalized attributes. # @yieldparam [Array] font_styles The finalized styles. def with_text_styles(attrs, &block) # When no color is given, do not draw. return unless attrs.key?(:color) && attrs[:color] != 'none' save_graphics_state fontinfo = { name: attrs.delete(:font).to_s, color: parse_color(attrs.delete(:color)), size: s2f(attrs.delete(:size)) } # Add the specified value to :leading option. line_height = attrs.delete(:line_height) if line_height attrs[:leading] = text_line_leading( s2f(line_height), name: fontinfo[:name], size: fontinfo[:size] ) end # Set the :character_spacing option. spacing = attrs.delete(:letter_spacing) attrs[:character_spacing] = s2f(spacing) if spacing # Disable line breaking on chars such as spaces and hyphens attrs[:disable_word_break] = true if attrs.delete(:overflow_wrap) == :anywhere # Delete unnecessary attributes attrs.delete(:word_wrap) # Or... with_font_styles(attrs, fontinfo, &block) with_font_styles(attrs, fontinfo) do |modified_attrs, styles| block.call(modified_attrs, styles) end restore_graphics_state end # @param [Numeric] line_height # @param [Hash] font # @option font [String] :name Name of font. # @option font [Numeric] :size Size of font. # @return [Numeric] def text_line_leading(line_height, font) line_height - pdf.font(font[:name], size: font[:size]).height end # @param [String] content # @return [String] def replace_space_to_nbsp(content) content.gsub(/ /, Prawn::Text::NBSP) end # @param [Hash] attrs # @param [Hash] font # @option font [String] :color # @option font [Numeric] :size # @option font [String] :name # @yield [attributes, styles] # @yieldparam [Hash] modified_attrs # @yieldparam [Array] styles def with_font_styles(attrs, font, &block) # Building font styles. styles = attrs.delete(:styles) if styles manual, styles = styles.partition do |style| %i[bold italic].include?(style) && !font_has_style?(font[:name], style) end end # Emulate bold style. if manual && manual.include?(:bold) pdf.stroke_color(font[:color]) pdf.line_width(font[:size] * 0.025) # Change rendering mode to :fill_stroke. attrs[:mode] = :fill_stroke end # Emulate italic style. if manual && manual.include?(:italic) # FIXME # pdf.transformation_matrix(1, 0, 0.26, 1, 0, 0) end pdf.font(font[:name], size: font[:size]) do pdf.fill_color(font[:color]) block.call(attrs, styles || []) end end end end end end end ================================================ FILE: lib/thinreports/basic_report/generator/pdf/document/graphics.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Generator class PDF module Graphics BASE_LINE_WIDTH = 0.9 private # Change the default graphic states defined by Prawn. def setup_custom_graphic_states pdf.line_width(BASE_LINE_WIDTH) end # @param [Numeric] width def line_width(width) pdf.line_width(width * BASE_LINE_WIDTH) end # Delegate to Prawn::Document#save_graphic_state # @see Prawn::Document#save_graphics_state def save_graphics_state pdf.save_graphics_state end # Delegate to Prawn::Document#restore_graphic_state # @see Prawn::Document#restore_graphics_state def restore_graphics_state pdf.restore_graphics_state end end end end end end require_relative 'graphics/attributes' require_relative 'graphics/basic' require_relative 'graphics/image' require_relative 'graphics/text' ================================================ FILE: lib/thinreports/basic_report/generator/pdf/document/page.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Generator class PDF module Page JIS_SIZES = { 'B4' => [728.5, 1031.8], 'B5' => [515.9, 728.5] }.freeze # @param [Thinreports::BasicReport::Layout::Format] format def start_new_page(format) format_id = if change_page_format?(format) pdf.start_new_page(new_basic_page_options(format)) @current_page_format = format create_format_stamp(format) unless format_stamp_registry.include?(format.identifier) format.identifier else pdf.start_new_page(new_basic_page_options(current_page_format)) current_page_format.identifier end stamp(format_id.to_s) end def start_new_page_for_section_report(format) @current_page_format = format pdf.start_new_page(new_basic_page_options(current_page_format).merge( top_margin: current_page_format.page_margin[0], bottom_margin: current_page_format.page_margin[2] )) end def max_content_height pdf.margin_box.height end def add_blank_page pdf.start_new_page(pdf.page_count.zero? ? { size: 'A4' } : {}) end private # @return [Thinreports::BasicReport::Layout::Format] attr_reader :current_page_format # @param [Thinreports::BasicReport::Layout::Format] new_format # @return [Boolean] def change_page_format?(new_format) !current_page_format || current_page_format.identifier != new_format.identifier end # @param [Thinreports::BasicReport::Layout::Format] format def create_format_stamp(format) create_stamp(format.identifier.to_s) do draw_template_items(format.attributes['items']) end format_stamp_registry << format.identifier end # @return [Array] def format_stamp_registry @format_stamp_registry ||= [] end # @param [Thinreports::BasicReport::Layout::Format] format # @return [Hash] def new_basic_page_options(format) options = { layout: format.page_orientation.to_sym } options[:size] = if format.user_paper_type? [format.page_width.to_f, format.page_height.to_f] else case format.page_paper_type # Convert B4(5)_ISO to B4(5) when 'B4_ISO', 'B5_ISO' format.page_paper_type.delete('_ISO') when 'B4', 'B5' JIS_SIZES[format.page_paper_type] else format.page_paper_type end end options end end end end end end ================================================ FILE: lib/thinreports/basic_report/generator/pdf/document/parse_color.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Generator class PDF module ParseColor # @param [String] color # @return [String] def parse_color(color) color = color.downcase if color =~ /^#?[\da-f]{6}$/ color.delete('#') else find_color_from_name(color) end end private # Supported only SAFE COLORS. SUPPORTED_COLOR_NAMES = { 'red' => 'ff0000', 'yellow' => 'fff000', 'lime' => '00ff00', 'aqua' => '00ffff', 'blue' => '0000ff', 'fuchsia' => 'ff00ff', 'maroon' => '800000', 'olive' => '808000', 'green' => '008800', 'teal' => '008080', 'navy' => '000080', 'purple' => '800080', 'black' => '000000', 'gray' => '808080', 'silver' => 'c0c0c0', 'white' => 'ffffff' }.freeze def find_color_from_name(name) color = SUPPORTED_COLOR_NAMES[name] raise Thinreports::BasicReport::Errors::UnsupportedColorName, name unless color color end end end end end end ================================================ FILE: lib/thinreports/basic_report/generator/pdf/document.rb ================================================ # frozen_string_literal: true require_relative 'document/font' require_relative 'document/parse_color' require_relative 'document/graphics' require_relative 'document/draw_shape' require_relative 'document/draw_template_items' require_relative 'document/page' module Thinreports module BasicReport module Generator class PDF class Document include Utils include PDF::Font include PDF::ParseColor include PDF::Graphics include PDF::DrawShape include PDF::DrawTemplateItems include PDF::Page # @return [Prawn::Document] attr_reader :pdf # @param [String] title (nil) # @param [Hash] security (nil) def initialize(title: nil, security: nil) @pdf = Prawn::Document.new( skip_page_creation: true, margin: [0, 0], info: { CreationDate: Time.now, Creator: 'Thinreports Generator for Ruby ' + Thinreports::VERSION, Title: title } ) # Setup to Prawn::Document. setup_fonts setup_custom_graphic_states # Encrypts the document. @pdf.encrypt_document(security) if security end # Delegate to Prawn::Document#render # @see Prawn::Document#render def render result = pdf.render finalize result end # Delegate to Prawn::Document#render_file # @see Prawn::Document#render_file def render_file(*args) finalize pdf.render_file(*args) end # @param [Numeric, String] x # @param [Numeric, String] y def translate(x, y, &block) x, y = rpos(x, y) pdf.translate(x, y, &block) end # @param [String] stamp_id # @param [Array] at (nil) def stamp(stamp_id, at = nil) if at.nil? pdf.stamp(stamp_id) else pdf.stamp_at(stamp_id, rpos(*at)) end end # Delegate to Prawn::Document#create_stamp # @param [String] id # @see Prawn::Document#create_stamp def create_stamp(id, &block) pdf.create_stamp(id, &block) end # @see #pdf def internal @pdf end private def finalize clean_temp_images end # @param [Array] values # @return [Numeric, Array, nil] def s2f(*values) return nil if values.empty? if values.size == 1 value = values.first return nil unless value value.is_a?(::Numeric) ? value : value.to_f else values.map { |v| s2f(v) } end end # @param [Numeric, String] x # @param [Numeric, String] y # @return [Array] def map_to_upper_left_relative_position(x, y) x, y = s2f(x, y) [x, -y] end alias rpos map_to_upper_left_relative_position # @param [Numeric, String] x # @param [Numeric, String] y # @return [Array] def map_to_upper_left_position(x, y) x, y = s2f(x, y) [x, pdf.bounds.height - y] end alias pos map_to_upper_left_position end end end end end ================================================ FILE: lib/thinreports/basic_report/generator/pdf/drawer/base.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Generator class PDF module Drawer # @abstract class Base # @param [Thinreports::BasicReport::Generator::PDF::Document] pdf # @param [Thinreports::BasicReport::Core::Shape::Manager::Format] format def initialize(pdf, format) @pdf = pdf @format = format @stamps = [] @draw_at = nil end # @abstract def draw raise NotImplementedError end private # @param [Thinreports::BasicReport::Core::Shape::Base::Internal] shape # @return [String] def pdf_stamp_id(shape) "#{@format.identifier}#{shape.identifier}" end # @overload pdf_stamp(shape_id) # @param [String] shape_id # @overload pdf_stamp(shape) # @param [Thinreports::BasicReport::Core::Shape::Base::Internal] shape def pdf_stamp(shape) shape = pdf_stamp_id(shape) unless shape.is_a?(::String) @pdf.stamp(shape, @draw_at) end # @param [Thinreports::BasicReport::Core::Shape::Base::Internal] shape def create_pdf_stamp(shape, &block) @pdf.create_stamp(pdf_stamp_id(shape), &block) end end end end end end end ================================================ FILE: lib/thinreports/basic_report/generator/pdf/drawer/list.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Generator class PDF module Drawer class List < Base # @param (see PDF::Drawer::Base#initialize) def initialize(pdf, format) super @sections = {} end # @param [Thinreports::BasicReport::Core::Shape::List::PageState] list_page def draw(list_page) draw_section(list_page.header) if list_page.header list_page.rows.each do |row| draw_section(row) end # Returns Thinreports::BasicReport::Report::Page object manager = list_page.parent.manager list_id = list_page.id.to_s manager.format.shapes.each do |id, shape| next unless list_pageno?(list_id, shape) shape = manager.final_shape(id) @pdf.draw_shape_pageno(shape.internal, list_page.no, list_page.manager.page_count) end end private # @param [String] list_id # @param [Thinreports::BasicReport::Core::Shape::Base::Format] shape # @return [Boolean] def list_pageno?(list_id, shape) shape.type == Thinreports::BasicReport::Core::Shape::PageNumber::TYPE_NAME && shape.target == list_id end # @param [Thinreports::BasicReport::Core::Shape::List::SectionInterface] section def draw_section(section) internal = section.internal base_top = @format.section_base_position_top(internal.section_name) position = [internal.relative_left, base_top + internal.relative_top] drawer(internal).draw(section, position) end # @param [Thinreports::BasicReport::Core::Shape::List::SectionInternal] section # @return [Thinreports::BasicReport::Generator::PDF::Drawer::ListSection] def drawer(section) @sections[section.section_name] ||= ListSection.new(@pdf, section) end end end end end end end ================================================ FILE: lib/thinreports/basic_report/generator/pdf/drawer/list_section.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Generator class PDF module Drawer class ListSection < Page # @param pdf (see PDF::Drawer::Page#initialize) # @param section [Thinreports::BasicReport::Core::Shape::List::SectionInternal] section def initialize(pdf, section) super(pdf, section.format) @section = section @stamp_created = false end # @param [Thinreports::BasicReport::Core::Shape::List::SectionInternal] section # @param [Array] at def draw(section, at) @draw_at = at draw_section super(section) end private def draw_section id = @format.identifier.to_s unless @stamp_created @pdf.create_stamp(id) { @pdf.draw_template_items(@format.attributes['items']) } @stamp_created = true end pdf_stamp(id) end # @see Thinreports::BasicReport::Generator::PDF::Drawer::Page#draw_tblock_shape def draw_tblock_shape(shape) @pdf.translate(*@draw_at) { super } end # @see Thinreports::BasicReport::Generator::PDF::Drawer::Page#draw_iblock_shape def draw_iblock_shape(shape) @pdf.translate(*@draw_at) { super } end end end end end end end ================================================ FILE: lib/thinreports/basic_report/generator/pdf/drawer/page.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Generator class PDF module Drawer class Page < Base # @param (see PDF::Drawer::Base#initialize) def initialize(pdf, format) super @lists = {} end # @param [Thinreports::BasicReport::Report::Page] page def draw(page) manager = page.manager manager.format.shapes.each_key do |id| shape = manager.final_shape(id) next unless shape shape = shape.internal if shape.type_of?(Core::Shape::PageNumber::TYPE_NAME) # Do not draw pageno if is not for Report draw_pageno_shape(shape, page) if page.count? && shape.for_report? else draw_shape(shape) end end end private def draw_shape(shape) if shape.type_of?(Core::Shape::TextBlock::TYPE_NAME) draw_tblock_shape(shape) elsif shape.type_of?(Core::Shape::List::TYPE_NAME) draw_list_shape(shape) elsif shape.type_of?(Core::Shape::ImageBlock::TYPE_NAME) draw_iblock_shape(shape) else id = shape.identifier unless @stamps.include?(id) create_basic_shape_stamp(shape) @stamps << id end pdf_stamp(shape) end end def draw_pageno_shape(shape, page) @pdf.draw_shape_pageno(shape, page.no, page.report.page_count) end # @see #draw_shape def draw_list_shape(shape) drawer = @lists[shape.id] ||= List.new(@pdf, shape.format) drawer.draw(shape) end # @see #draw_shape def draw_tblock_shape(shape) @pdf.draw_shape_tblock(shape) end # @see #draw_shape def draw_iblock_shape(shape) @pdf.draw_shape_iblock(shape) end # @param [Thinreports::BasicReport::Core::Shape::Base::Internal] shape def create_basic_shape_stamp(shape) if shape.type_of?('text') create_text_stamp(shape) elsif shape.type_of?('image') create_image_stamp(shape) elsif shape.type_of?('ellipse') create_ellipse_stamp(shape) elsif shape.type_of?('rect') create_rect_stamp(shape) elsif shape.type_of?('line') create_line_stamp(shape) end end # @see #create_basic_shape_stamp def create_image_stamp(shape) create_pdf_stamp(shape) do @pdf.draw_shape_image(shape) end end # @see #create_basic_shape_stamp def create_rect_stamp(shape) create_pdf_stamp(shape) do @pdf.draw_shape_rect(shape) end end # @see #create_basic_shape_stamp def create_ellipse_stamp(shape) create_pdf_stamp(shape) do @pdf.draw_shape_ellipse(shape) end end # @see #create_basic_shape_stamp def create_line_stamp(shape) create_pdf_stamp(shape) do @pdf.draw_shape_line(shape) end end # @see #create_basic_shape_stamp def create_text_stamp(shape) create_pdf_stamp(shape) do @pdf.draw_shape_text(shape) end end end end end end end end ================================================ FILE: lib/thinreports/basic_report/generator/pdf/prawn_ext/calc_image_dimensions.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Generator module PrawnExt module CalcImageDimensions # Implement :auto_fit option for image size calculation. # # When the image is larger than the box, the original: fit option does not change # the image size. The :auto_fit option changes the image size to fit in the box # while maintaining the aspect ratio. # # Usage: # image '/path/to/image.png', at: [100, 100], auto_fit: [100, 100] # def calc_image_dimensions(options) if options[:auto_fit] image_width = options[:width] || width image_height = options[:height] || height box_width, box_height = options.delete(:auto_fit) options[:fit] = [box_width, box_height] if image_width > box_width || image_height > box_height end super(options) end end end end end end Prawn::Images::Image.prepend Thinreports::BasicReport::Generator::PrawnExt::CalcImageDimensions ================================================ FILE: lib/thinreports/basic_report/generator/pdf/prawn_ext/width_of.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Generator module PrawnExt module WidthOf # This patch fixes the character_spacing effect on text width calculation. # # The original #width_of: # # width_of('abcd') #=> 4 + 4 = 8 # # The #width_of in this patch: # # width_of('abcd') #=> 4 + 3 = 7 # def width_of(*) width = super - character_spacing width > 0 ? width : 0 end end end end end end # Prawn v2.3 and later includes this patch by https://github.com/prawnpdf/prawn/pull/1117. if Prawn::VERSION < '2.3.0' Prawn::Document.prepend Thinreports::BasicReport::Generator::PrawnExt::WidthOf end ================================================ FILE: lib/thinreports/basic_report/generator/pdf/prawn_ext.rb ================================================ # frozen_string_literal: true require_relative 'prawn_ext/width_of' require_relative 'prawn_ext/calc_image_dimensions' Prawn::Font::AFM.hide_m17n_warning = true ================================================ FILE: lib/thinreports/basic_report/generator/pdf.rb ================================================ # frozen_string_literal: true require 'prawn' require 'prawn/disable_word_break' Prawn::DisableWordBreak.config.default = false module Thinreports module BasicReport module Generator class PDF # @return [Thinreports::BasicReport::Report::Base] attr_reader :report # @param [Thinreports::BasicReport::Report::Base] report # @param [Hash] security (nil) # @param [String] title (nil) def initialize(report, security: nil, title: nil) report.finalize @report = report.internal title ||= default_layout ? default_layout.format.report_title : nil @document = Document.new(title: title, security: security) @drawers = {} end # @param [String, nil] filename # @return [String, nil] def generate(filename = nil) draw_report filename ? @document.render_file(filename) : @document.render end def default_layout report.default_layout end private def draw_report report.pages.each do |page| draw_page(page) end end def draw_page(page) return @document.add_blank_page if page.blank? format = page.layout.format @document.start_new_page(format) drawer(format).draw(page) end def drawer(format) @drawers[format.identifier] ||= Drawer::Page.new(@document, format) end end end end end require_relative 'pdf/prawn_ext' require_relative 'pdf/document' require_relative 'pdf/drawer/base' require_relative 'pdf/drawer/page' require_relative 'pdf/drawer/list' require_relative 'pdf/drawer/list_section' ================================================ FILE: lib/thinreports/basic_report/layout/base.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Layout class Base EXT_NAME = 'tlf' include Utils class << self # @param [String] filename # @return [Thinreports::BasicReport::Layout::Format] # @raise [Thinreports::BasicReport::Errors::InvalidLayoutFormat] # @raise [Thinreports::BasicReport::Errors::IncompatibleLayoutFormat] def load_format(filename) filename += ".#{EXT_NAME}" unless filename =~ /\.#{EXT_NAME}$/ raise Thinreports::BasicReport::Errors::LayoutFileNotFound unless File.exist?(filename) # Build format. Thinreports::BasicReport::Layout::Format.build(filename) end end attr_reader :format # @return [String] attr_reader :filename # @return [Symbol] attr_reader :id # @param [String] filename # @param [Hash] options # @option options [Symbol] :id (nil) def initialize(filename, options = {}) @filename = filename @format = self.class.load_format(filename) @id = options[:id] end # @return [Boolean] Return the true if is default layout. def default? @id.nil? end end end end end ================================================ FILE: lib/thinreports/basic_report/layout/format.rb ================================================ # frozen_string_literal: true require 'json' module Thinreports module BasicReport module Layout class Format < Core::Shape::Manager::Format config_reader last_version: %w[version] config_reader report_title: %w[title] config_reader page_paper_type: %w[report paper-type], page_width: %w[report width], page_height: %w[report height], page_orientation: %w[report orientation] class << self def build(filename) schema = JSON.parse(read_file(filename)) schema_version = Layout::Version.new(schema['version']) unless schema_version.compatible? raise Errors::IncompatibleLayoutFormat.new( filename, schema['version'], Layout::Version.compatible_rules.join(' and ') ) end if schema_version.legacy? warn '[DEPRECATION] Support for the layout file with old format' \ ' that generated with Editor 0.8 or lower will be dropped in Thinreports 1.1.' \ ' Please convert to new layout format using Thinreports Editor 0.9 or 1.0.' schema = Layout::LegacySchema.new(schema).upgrade end new schema end def read_file(filename) File.read(filename, encoding: 'UTF-8') end end def initialize(*) super initialize_items(attributes['items']) end def user_paper_type? page_paper_type == 'user' end private def initialize_items(item_schemas) item_schemas.each do |item_schema| id, type = item_schema.values_at 'id', 'type' next if id.empty? && type != 'page-number' item = Core::Shape::Format(type).new(item_schema) shapes[item.id.to_sym] = item end end end end end end ================================================ FILE: lib/thinreports/basic_report/layout/legacy_schema.rb ================================================ # frozen_string_literal: true require 'json' require 'rexml/document' module Thinreports module BasicReport module Layout class LegacySchema include Utils def initialize(legacy_schema) @legacy_schema = legacy_schema @legacy_svg = legacy_schema['svg'].dup @legacy_item_schemas = extract_legacy_item_schemas(legacy_svg) @legacy_svg = cleanup_svg(@legacy_svg) end def upgrade config = legacy_schema['config'] page_config = config['page'] { 'version' => legacy_schema['version'], 'title' => legacy_schema['config']['title'], 'report' => { 'paper-type' => page_config['paper-type'], 'width' => page_config['width'].to_f, 'height' => page_config['height'].to_f, 'orientation' => page_config['orientation'], 'margin' => page_config.values_at( 'margin-top', 'margin-right', 'margin-bottom', 'margin-left' ).map(&:to_f) }, 'items' => item_schemas } end attr_reader :legacy_schema, :legacy_svg, :legacy_item_schemas def item_schemas svg = REXML::Document.new(legacy_svg) build_item_schemas_from_svg(svg.elements['/svg/g']) end def build_item_schemas_from_svg(svg_elements) return [] unless svg_elements items = [] svg_elements.each do |item_element| item_attributes = item_element.attributes items << case item_element.attributes['class'] when 's-text' then text_item_schema(item_attributes, extract_texts_from(item_element)) when 's-image' then image_item_schema(item_attributes) when 's-rect' then rect_item_schema(item_attributes) when 's-ellipse' then ellipse_item_schema(item_attributes) when 's-line' then line_item_schema(item_attributes) when 's-tblock' then text_block_item_schema(item_attributes) when 's-iblock' then image_block_item_schema(item_attributes) when 's-pageno' then page_number_item_schema(item_attributes) when 's-list' then list_item_schema(item_element) else raise 'Unknown item type' end end items end def text_item_schema(attributes, texts) { 'id' => attributes['x-id'], 'type' => Core::Shape::Text::TYPE_NAME, 'x' => attributes['x-left'].to_f, 'y' => attributes['x-top'].to_f, 'width' => attributes['x-width'].to_f, 'height' => attributes['x-height'].to_f, 'display' => display(attributes['x-display']), 'texts' => texts, 'style' => { 'font-family' => [attributes['font-family']], 'font-size' => attributes['font-size'].to_f, 'color' => attributes['fill'], 'font-style' => font_style(attributes), 'text-align' => text_align(attributes['text-anchor']), 'vertical-align' => vertical_align(attributes['x-valign']), 'line-height' => line_height(attributes['x-line-height']), 'letter-spacing' => letter_spacing(attributes['kerning']) } } end def rect_item_schema(attributes) { 'id' => attributes['x-id'], 'type' => 'rect', 'x' => attributes['x'].to_f, 'y' => attributes['y'].to_f, 'width' => attributes['width'].to_f, 'height' => attributes['height'].to_f, 'display' => display(attributes['x-display']), 'border-radius' => attributes['rx'].to_i, 'style' => { 'border-width' => attributes['stroke-width'].to_f, 'border-color' => attributes['stroke'], 'border-style' => attributes['x-stroke-type'], 'fill-color' => attributes['fill'] } } end def line_item_schema(attributes) { 'id' => attributes['x-id'], 'type' => 'line', 'x1' => attributes['x1'].to_f, 'y1' => attributes['y1'].to_f, 'x2' => attributes['x2'].to_f, 'y2' => attributes['y2'].to_f, 'display' => display(attributes['x-display']), 'style' => { 'border-width' => attributes['stroke-width'].to_f, 'border-color' => attributes['stroke'], 'border-style' => attributes['x-stroke-type'] } } end def ellipse_item_schema(attributes) { 'id' => attributes['x-id'], 'type' => 'ellipse', 'cx' => attributes['cx'].to_f, 'cy' => attributes['cy'].to_f, 'rx' => attributes['rx'].to_f, 'ry' => attributes['ry'].to_f, 'display' => display(attributes['x-display']), 'style' => { 'border-width' => attributes['stroke-width'].to_f, 'border-color' => attributes['stroke'], 'border-style' => attributes['x-stroke-type'], 'fill-color' => attributes['fill'] } } end def image_item_schema(attributes) _, image_type, image_data = attributes['xlink:href'].match(%r{^data:(image/[a-z]+?);base64,(.+)}).to_a { 'id' => attributes['x-id'], 'type' => 'image', 'x' => attributes['x'].to_f, 'y' => attributes['y'].to_f, 'width' => attributes['width'].to_f, 'height' => attributes['height'].to_f, 'display' => display(attributes['x-display']), 'data' => { 'mime-type' => image_type, 'base64' => image_data } } end def page_number_item_schema(attributes) { 'id' => attributes['x-id'], 'type' => Core::Shape::PageNumber::TYPE_NAME, 'x' => attributes['x-left'].to_f, 'y' => attributes['x-top'].to_f, 'width' => attributes['x-width'].to_f, 'height' => attributes['x-height'].to_f, 'format' => attributes['x-format'], 'target' => attributes['x-target'], 'display' => display(attributes['x-display']), 'style' => { 'font-family' => [attributes['font-family']], 'font-size' => attributes['font-size'].to_f, 'color' => attributes['fill'], 'font-style' => font_style(attributes), 'text-align' => text_align(attributes['text-anchor']), 'overflow' => attributes['x-overflow'] } } end def image_block_item_schema(attributes) { 'id' => attributes['x-id'], 'type' => Core::Shape::ImageBlock::TYPE_NAME, 'x' => attributes['x-left'].to_f, 'y' => attributes['x-top'].to_f, 'width' => attributes['x-width'].to_f, 'height' => attributes['x-height'].to_f, 'display' => display(attributes['x-display']), 'style' => { 'position-x' => attributes['x-position-x'], 'position-y' => image_position_y(attributes['x-position-y']) } } end def text_block_item_schema(attributes) text_format = { 'base' => attributes['x-format-base'], 'type' => attributes['x-format-type'] } case text_format['type'] when 'datetime' text_format['datetime'] = { 'format' => attributes['x-format-datetime-format'] } when 'number' text_format['number'] = { 'delimiter' => attributes['x-format-number-delimiter'], 'precision' => attributes['x-format-number-precision'].to_i } when 'padding' text_format['padding'] = { 'length' => attributes['x-format-padding-length'].to_i, 'char' => attributes['x-format-padding-char'], 'direction' => attributes['x-format-padding-direction'] } end { 'id' => attributes['x-id'], 'type' => Core::Shape::TextBlock::TYPE_NAME, 'x' => attributes['x-left'].to_f, 'y' => attributes['x-top'].to_f, 'width' => attributes['x-width'].to_f, 'height' => attributes['x-height'].to_f, 'display' => display(attributes['x-display']), 'value' => attributes['x-value'], 'multiple-line' => attributes['x-multiple'] == 'true', 'format' => text_format, 'reference-id' => attributes['x-ref-id'], 'style' => { 'font-family' => [attributes['font-family']], 'font-size' => attributes['font-size'].to_f, 'color' => attributes['fill'], 'font-style' => font_style(attributes), 'text-align' => text_align(attributes['text-anchor']), 'vertical-align' => vertical_align(attributes['x-valign']), 'line-height' => line_height(attributes['x-line-height']), 'letter-spacing' => letter_spacing(attributes['kerning']), 'overflow' => attributes['x-overflow'], 'word-wrap' => attributes['x-word-wrap'] || '' } } end def list_item_schema(legacy_element) legacy_schema = legacy_item_schemas[legacy_element.attributes['x-id']] header = list_section_schema('header', legacy_element, legacy_schema) detail = list_section_schema('detail', legacy_element, legacy_schema) page_footer = list_section_schema('page-footer', legacy_element, legacy_schema) footer = list_section_schema('footer', legacy_element, legacy_schema) schema = { 'id' => legacy_schema['id'], 'type' => Core::Shape::List::TYPE_NAME, 'content-height' => legacy_schema['content-height'].to_f, 'auto-page-break' => legacy_schema['page-break'] == 'true', 'display' => display(legacy_schema['display']), 'header' => header, 'detail' => detail, 'page-footer' => page_footer, 'footer' => footer } page_footer['translate']['y'] += detail['height'] if page_footer['enabled'] if footer['enabled'] footer['translate']['y'] += detail['height'] footer['translate']['y'] += page_footer['height'] if page_footer['enabled'] end schema end def list_section_schema(section_name, legacy_list_element, legacy_list_schema) legacy_section_schema = legacy_list_schema[section_name] return {} if legacy_section_schema.empty? section_item_elements = legacy_list_element.elements["g[@class='s-list-#{section_name}']"] section_schema = { 'height' => legacy_section_schema['height'].to_f, 'translate' => { 'x' => legacy_section_schema['translate']['x'].to_f, 'y' => legacy_section_schema['translate']['y'].to_f }, 'items' => build_item_schemas_from_svg(section_item_elements) } unless section_name == 'detail' section_schema['enabled'] = legacy_list_schema["#{section_name}-enabled"] == 'true' end section_schema end def extract_texts_from(text_item_element) [].tap do |texts| text_item_element.each_element('text') { |e| texts << e.text } end end def image_position_y(legacy_position_y) case legacy_position_y when 'top' then 'top' when 'center' then 'middle' when 'bottom' then 'bottom' end end def display(legacy_display) legacy_display == 'true' end def font_style(attributes) style = [] style << 'bold' if attributes['font-weight'] == 'bold' style << 'italic' if attributes['font-style'] == 'italic' style << 'underline' if attributes['text-decoration'].include?('underline') style << 'linethrough' if attributes['text-decoration'].include?('line-through') style end def text_align(legacy_text_align) case legacy_text_align when 'start' then 'left' when 'middle' then 'center' when 'end' then 'right' else 'left' end end def vertical_align(legacy_vertical_align) return '' unless legacy_vertical_align case legacy_vertical_align when 'top' then 'top' when 'center' then 'middle' when 'bottom' then 'bottom' else 'top' end end def line_height(legacy_line_height) blank_value?(legacy_line_height) ? '' : legacy_line_height.to_f end def letter_spacing(legacy_letter_spacing) case legacy_letter_spacing when 'auto', '' then '' else legacy_letter_spacing.to_f end end def extract_legacy_item_schemas(svg) items = {} svg.scan(//) do |(item_schema_json)| item_schema = JSON.parse(item_schema_json) items[item_schema['id']] = item_schema end items end def cleanup_svg(svg) cleaned_svg = svg.gsub(//, '') cleaned_svg.gsub(//) { $1 } end end end end end ================================================ FILE: lib/thinreports/basic_report/layout/version.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Layout class Version COMPATIBLE_RULES = ['>= 0.8.0', '< 1.0.0'].freeze NEW_SCHEMA_FROM = '0.9.0' class << self def compatible_rules COMPATIBLE_RULES end end def initialize(schema_version) @schema_version = normalize_version(schema_version) end def compatible? self.class.compatible_rules.all? do |rule| op, ver = rule.split(' ') schema_version.send(op.to_sym, normalize_version(ver)) end end def legacy? @schema_version < normalize_version(NEW_SCHEMA_FROM) end private attr_reader :schema_version def normalize_version(version) Gem::Version.create(version) end end end end end ================================================ FILE: lib/thinreports/basic_report/layout.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Layout # @see Thinreports::BasicReport::Layout::Base#initialize def self.new(filename, options = {}) Base.new(filename, options) end end end end require_relative 'layout/version' require_relative 'layout/base' require_relative 'layout/format' require_relative 'layout/legacy_schema' ================================================ FILE: lib/thinreports/basic_report/report/base.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Report class Base extend Forwardable include Utils class << self # @param options (see #initialize) # @option options (see #initialize) # @yield [report] # @yieldparam [Thinreports::BasicReport::Report::Base] report # @return [Thinreports::BasicReport::Report::Base] def create(options = {}, &block) raise ArgumentError, '#create requires a block' unless block_given? report = new(options) call_block_in(report, &block) report.finalize report end # @param layout (see #initialize) # @param filename (see #generate) # @param security (see #generate) # @param title (see #generate) # @param [Hash] report ({}) DEPRECATED. Options for Report. # @param [Hash] generator ({}) DEPRECATED. Options for Generator. # @yield (see .create) # @yieldparam (see .create) # @return [String] def generate(layout: nil, filename: nil, security: nil, title: nil, report: {}, generator: {}, &block) raise ArgumentError, '#generate requires a block' unless block_given? if report.any? || generator.any? warn '[DEPRECATION] :report and :generator argument has been deprecated. ' \ 'Use :layout and :filename, :security argument instead.' end layout ||= report[:layout] filename ||= generator[:filename] security ||= generator[:security] report = create(layout: layout, &block) report.generate(filename: filename, security: security, title: title) end end # @return [Thinreports::BasicReport::Report::Internal] attr_reader :internal # @return [Integer] attr_reader :start_page_number # @return [Thinreports::BasicReport::Report::Page] def_delegator :internal, :page # @return [Integer] def_delegator :internal, :page_count # @return [Array] def_delegator :internal, :pages # @return [Thinreports::BasicReport::Layout::Base] def_delegator :internal, :default_layout # @param [Hash] options # @option options [String, nil] :layout (nil) def initialize(options = {}) @internal = Report::Internal.new(self, options) @start_page_number = 1 end # @yield [page] # @yieldparam [Thinreports::BasicReport::Report::Page] page # @example # report.on_page_create do |page| # page.item(:header_title).value = 'Title' # end def on_page_create(&block) internal.page_create_handler = block end # @param [Integer] page_number def start_page_number_from(page_number) @start_page_number = page_number end # @param [String] layout filename of layout file # @param [Hash] options # @option options [Boolean] :default (true) # @option options [Symbol] :id (nil) # @example # report.use_layout '/path/to/default_layout.tlf' # Default layout # report.use_layout '/path/to/default_layout.tlf', default: true # report.use_layout '/path/to/other_layout', id: :other_layout def use_layout(layout, options = {}) internal.register_layout(layout, options) end # @example # page = report.start_new_page # # report.start_new_page do |page| # page.item(:text).value = 'value' # end # # report.use_layout 'foo.tlf', default: true # report.use_layout 'bar.tlf', id: :bar # # report.start_new_page # Use 'foo.tlf' # report.start_new_page layout: :bar # Use 'bar.tlf' # report.start_new_page layout: 'boo.tlf' # Use 'boo.tlf' # @param [Hash] options # @option options [String, Symbol] :layout (nil) # @option options [Boolean] :count (true) # @yield [page] # @yieldparam [Thinreports::BasicReport::Report::Page] page # @return [Thinreports::BasicReport::Report::Page] def start_new_page(options = {}, &block) layout = internal.load_layout(options.delete(:layout)) raise Thinreports::BasicReport::Errors::NoRegisteredLayoutFound unless layout page = internal.add_page(Report::Page.new(self, layout, options)) call_block_in(page, &block) end # @param [Hash] options # @option options [Boolean] :count (true) # @return [Thinreports::BasicReport::Report::BlankPage] def add_blank_page(options = {}) internal.add_page(Report::BlankPage.new(options[:count])) end alias blank_page add_blank_page # @param [Symbol, nil] id # @return [Thinreports::BasicReport::Layout::Base] def layout(id = nil) if id internal.layout_registry[id] || raise(Thinreports::BasicReport::Errors::UnknownLayoutId) else internal.default_layout end end # @param [String] filename # @param [Hash] security (see http://prawnpdf.org/api-docs/2.0/Prawn/Document/Security.html#encrypt_document-instance_method) # @param [String] title Value of the title attribute of the PDF document metadata. # if nil, the title of the default layout file is set. # @return [String] # @example Generate PDF data # report.generate # => "%PDF-1.4...." # @example Create a PDF file # report.generate(filename: 'foo.pdf') def generate(filename: nil, security: nil, title: nil) Thinreports::BasicReport::Generator::PDF.new(self, security: security, title: title).generate(filename) end # @see Thinreports::BasicReport::Core::Shape::Manager::Target#list def list(id = nil, &block) start_new_page if page.nil? || page.finalized? page.list(id, &block) end def_delegators :internal, :finalize, :finalized? end end end end ================================================ FILE: lib/thinreports/basic_report/report/internal.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Report class Internal attr_reader :pages attr_reader :page attr_reader :page_count attr_reader :default_layout attr_reader :layout_registry attr_accessor :page_create_handler # @param [Thinreports::BasicReport::Report::Base] report # @param options (see Thinreports::BasicReport::Report::Base#initialize) def initialize(report, options) @report = report @default_layout = options[:layout] ? init_layout(options[:layout]) : nil @layout_registry = {} @finalized = false @pages = [] @page = nil @page_count = 0 @page_create_handler = nil end # @see Thinreports::BasicReport::Report::Base#use_layout def register_layout(layout, options = {}) if options.empty? || options[:default] @default_layout = init_layout(layout) else id = options[:id].to_sym raise ArgumentError, "Id :#{id} is already in use." if layout_registry.key?(id) layout_registry[id] = init_layout(layout, id) end end def add_page(new_page) finalize_current_page insert_page(new_page) end def copy_page finalize_current_page(at: :copy) insert_page(page.copy) end def finalize return if finalized? finalize_current_page @finalized = true end def finalized? @finalized end def load_layout(id_or_filename) return @default_layout if id_or_filename.nil? layout = case id_or_filename when Symbol layout_registry[id_or_filename] when String init_layout(id_or_filename) else raise ArgumentError, 'Invalid argument for layout.' end @default_layout ||= layout layout end private def insert_page(new_page) @pages << new_page if new_page.count? @page_count += 1 new_page.no = @page_count end @page_create_handler.call(new_page) if !new_page.blank? && @page_create_handler @page = new_page end # @param (see Thinreports::BasicReport::Report::Page#finalize) def finalize_current_page(options = {}) page.finalize(options) unless page.nil? || page.blank? end def init_layout(filename, id = nil) filename = filename.to_path if filename.is_a?(Pathname) Thinreports::BasicReport::Layout.new(filename, id: id) end end end end end ================================================ FILE: lib/thinreports/basic_report/report/page.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Report class BlankPage # @example # 3.times do # page = report.start_new_page # puts page.no # end # # => 1, 2, 3 # @return [Integer] attr_accessor :no # @param [Boolean] count (nil) def initialize(count = nil) @count = count.nil? ? true : count end # @example # page = report.start_new_page # page.count? # => true # # page = report.start_new_page count: false # page.count? # => false # @return [Boolean] def count? @count end # @return [Boolean] (true) def blank? true end end class Page < BlankPage include Core::Shape::Manager::Target # @return [Thinreports::BasicReport::Report::Base] attr_reader :report # @return [Thinreports::BasicReport::Layout::Base] attr_reader :layout # @param [Thinreports::BasicReport::Report::Base] report # @param [Thinreports::BasicReport::Layout::Base] layout # @param [Hash] options ({}) # @option options [Boolean] :count (true) def initialize(report, layout, options = {}) super(options.key?(:count) ? options[:count] : true) @report = report @layout = layout @finalized = false initialize_manager(layout.format) do |f| Core::Shape::Interface(self, f) end end # @return [Boolean] (false) def blank? false end def copy new_page = self.class.new(report, layout, count: count?) manager.shapes.each do |id, shape| new_shape = shape.copy(new_page) new_page.manager.shapes[id] = new_shape new_page.manager.lists[id] = new_shape if new_shape.internal.type_of?(Core::Shape::List::TYPE_NAME) end new_page end # @param [Hash] options # @option options [:create, :copy] :at (:create) def finalize(options = {}) at = options[:at] || :create # For list shapes. manager.lists.each_value { |list| list.manager.finalize } if at == :create @finalized = true end def finalized? @finalized end end end end end ================================================ FILE: lib/thinreports/basic_report/report.rb ================================================ # frozen_string_literal: true module Thinreports module BasicReport module Report # @see Thinreports::BasicReport::Report::Base#initialize def self.new(*args) Base.new(*args) end # @see Thinreports::BasicReport::Report::Base#create def self.create(*args, &block) Base.create(*args, &block) end # @see Thinreports::BasicReport::Report::Base#generate def self.generate(**args, &block) Base.generate(**args, &block) end end end end require_relative 'report/base' require_relative 'report/internal' require_relative 'report/page' ================================================ FILE: lib/thinreports/basic_report.rb ================================================ # frozen_string_literal: true require_relative 'basic_report/core/utils' require_relative 'basic_report/core/errors' require_relative 'basic_report/core/format/base' require_relative 'basic_report/core/shape' require_relative 'basic_report/core/utils' require_relative 'basic_report/report' require_relative 'basic_report/layout' require_relative 'basic_report/generator/pdf' module Thinreports Report = BasicReport::Report Layout = BasicReport::Layout end ================================================ FILE: lib/thinreports/config.rb ================================================ # frozen_string_literal: true module Thinreports # @yield [config] # @yieldparam [Thinreports::Configuration] config def self.configure(&block) block.call(config) end # @return [Thinreports::Configuration] def self.config @config ||= Thinreports::Configuration.new end class Configuration def initialize @fallback_fonts = [] end # @return [Array] # @example # config.fallback_fonts # => ['Times New Roman', '/path/to/font.ttf'] def fallback_fonts @fallback_fonts ||= [] end # @param [Array,String] font_names # @example # config.fallback_fonts = 'Times New Roman' # config.fallback_fonts = '/path/to/font.ttf' # config.fallback_fonts = ['/path/to/font.ttf', 'IPAGothic'] def fallback_fonts=(font_names) @fallback_fonts = font_names.is_a?(Array) ? font_names : [font_names] end end end ================================================ FILE: lib/thinreports/section_report/build.rb ================================================ # frozen_string_literal: true require_relative 'schema/loader' require_relative 'builder/report_builder' module Thinreports module SectionReport class Build def call(report_params) schema = load_schema(report_params) params = report_params[:params] || {} Builder::ReportBuilder.new(schema).build(params) end private def load_schema(report_params) loader = Schema::Loader.new case when report_params[:layout_file] loader.load_from_file(report_params[:layout_file]) when report_params[:layout_data] loader.load_from_data(report_params[:layout_data]) else raise Errors::LayoutFileNotFound end end end end end ================================================ FILE: lib/thinreports/section_report/builder/item_builder.rb ================================================ # frozen_string_literal: true require_relative 'stack_view_builder' module Thinreports module SectionReport module Builder class ItemBuilder Context = Struct.new(:parent_schema) def initialize(item_schema, parent_schema) @item = Core::Shape::Interface(nil, item_schema) @parent_schema = parent_schema end def build(item_params) params = build_params(item_params) item.visible(params[:display]) if params.key?(:display) item.value(params[:value]) if params.key?(:value) item.styles(params[:styles]) if params.key?(:styles) if item.internal.format.attributes['type'] == Core::Shape::StackView::TYPE_NAME StackViewBuilder.new(item).update(params) end item end private attr_reader :item, :parent_schema def build_params(params) return {} unless params case params when Hash params when Proc params.call(Context.new(parent_schema)) else { value: params } end end end end end end ================================================ FILE: lib/thinreports/section_report/builder/report_builder.rb ================================================ # frozen_string_literal: true require_relative 'report_data' require_relative 'item_builder' module Thinreports module SectionReport module Builder class ReportBuilder def initialize(schema) @schema = schema end def build(params) ReportData::Main.new( schema, build_groups(params[:groups]) ) end private attr_reader :schema def build_groups(groups_params) return [] unless groups_params groups_params.map do |group_params| ReportData::Group.new( build_sections(:header, group_params[:headers] || {}), build_detail_sections(group_params[:details] || []), build_sections(:footer, group_params[:footers] || {}) ) end end def build_sections(section_type, sections_params) sections_schemas = case section_type when :header then schema.headers when :footer then schema.footers end sections_schemas.each_with_object([]) do |(section_id, section_schema), sections| section_params = sections_params[section_id.to_sym] || {} next unless section_enabled?(section_schema, section_params) items = build_items(section_schema, section_params[:items] || {}) sections << ReportData::Section.new(section_schema, items, section_params[:min_height]) end end def build_detail_sections(details_params) details_params.each_with_object([]) do |detail_params, details| detail_id = detail_params[:id].to_sym detail_schema = schema.details[detail_id] next unless detail_schema items = build_items(detail_schema, detail_params[:items] || {}) details << ReportData::Section.new(detail_schema, items, detail_params[:min_height]) end end def build_items(section_schema, items_params) section_schema.items.each_with_object([]) do |item_schema, items| item = ItemBuilder.new(item_schema, section_schema).build(items_params[item_schema.id&.to_sym]) items << item if item.visible? end end def section_enabled?(section_schema, section_params) if section_params.key?(:display) section_params[:display] else section_schema.display? end end end end end end ================================================ FILE: lib/thinreports/section_report/builder/report_data.rb ================================================ # frozen_string_literal: true module Thinreports module SectionReport module Builder module ReportData Main = Struct.new :schema, :groups Group = Struct.new :headers, :details, :footers Section = Struct.new :schema, :items, :min_height end end end end ================================================ FILE: lib/thinreports/section_report/builder/stack_view_builder.rb ================================================ # frozen_string_literal: true require_relative 'stack_view_data' module Thinreports module SectionReport module Builder class StackViewBuilder def initialize(item) @item = item end def update(params) rows_params = params[:rows] || {} rows_schema = item.internal.format.rows rows = [] rows_schema.each do |row_schema| row_params = rows_params[row_schema.id.to_sym] || {} next unless row_enabled?(row_schema, row_params) items = build_row_items( row_schema, row_params[:items] || {} ) rows << StackViewData::Row.new(row_schema, items, row_params[:min_height]) end item.internal.rows = rows end private attr_reader :item def build_row_items(row_schema, items_params) row_schema.items.each_with_object([]) do |item_schema, items| item = ItemBuilder.new(item_schema, row_schema).build(items_params[item_schema.id&.to_sym]) items << item if item.visible? end end def row_enabled?(row_schema, row_params) if row_params.key?(:display) row_params[:display] else row_schema.display? end end end end end end ================================================ FILE: lib/thinreports/section_report/builder/stack_view_data.rb ================================================ # frozen_string_literal: true module Thinreports module SectionReport module Builder module StackViewData Row = Struct.new :schema, :items, :min_height end end end end ================================================ FILE: lib/thinreports/section_report/generate.rb ================================================ # frozen_string_literal: true require_relative 'build' require_relative 'pdf/render' module Thinreports module SectionReport class Generate def initialize @pdf = Thinreports::Generator::PDF::Document.new end def call(report_params, filename: nil) report = Build.new.call(report_params) PDF::Render.new(pdf).call!(report) filename ? pdf.render_file(filename) : pdf.render end private attr_reader :pdf end end end ================================================ FILE: lib/thinreports/section_report/pdf/render.rb ================================================ # frozen_string_literal: true require_relative 'renderer/group_renderer' module Thinreports module SectionReport module PDF class Render def initialize(pdf) @group_renderer = Renderer::GroupRenderer.new(pdf) end def call!(report) report.groups.each { |group| group_renderer.render(report, group) } end private attr_reader :group_renderer end end end end ================================================ FILE: lib/thinreports/section_report/pdf/renderer/draw_item.rb ================================================ # frozen_string_literal: true module Thinreports module SectionReport module Renderer module DrawItem def draw_item(item, expanded_height = 0) shape = item.internal if shape.type_of?(Core::Shape::TextBlock::TYPE_NAME) computed_height = shape.format.attributes['height'] computed_height += expanded_height if shape.format.follow_stretch == 'height' if shape.style.finalized_styles['overflow'] == 'expand' # When overflow is "expand", the value of the height argument is ignored and the shape is expanded to # the bottom of the outer bounding box. # That causes a position shift problem if vertical-align is "middle" or "bottom". # To solve it, we overwrite the overflow to "truncate" when drawing. # To emulate the "expand" behavior in the "truncate" mode, # here we pass the greater value of the computed_height and the text height as text block height. pdf.draw_shape_tblock(shape, height: [computed_height, calc_text_block_height(shape)].max, overflow: :truncate) else pdf.draw_shape_tblock(shape, height: computed_height) end elsif shape.type_of?(Core::Shape::ImageBlock::TYPE_NAME) pdf.draw_shape_iblock(shape) elsif shape.type_of?('text') case shape.format.follow_stretch when 'height' pdf.draw_shape_text(shape, expanded_height) else pdf.draw_shape_text(shape) end elsif shape.type_of?('image') pdf.draw_shape_image(shape) elsif shape.type_of?('ellipse') pdf.draw_shape_ellipse(shape) elsif shape.type_of?('rect') case shape.format.follow_stretch when 'height' pdf.draw_shape_rect(shape, expanded_height) else pdf.draw_shape_rect(shape) end elsif shape.type_of?('line') case shape.format.follow_stretch when 'height' y1, y2 = shape.format.attributes.values_at('y1', 'y2') if y1 < y2 pdf.draw_shape_line(shape, 0, expanded_height) else pdf.draw_shape_line(shape, expanded_height, 0) end when 'y' pdf.draw_shape_line(shape, expanded_height, expanded_height) else pdf.draw_shape_line(shape) end elsif shape.type_of?(Core::Shape::StackView::TYPE_NAME) stack_view_renderer.render(shape) else raise Thinreports::Errors::UnknownShapeType end end end end end end ================================================ FILE: lib/thinreports/section_report/pdf/renderer/group_renderer.rb ================================================ # frozen_string_literal: true require_relative 'section_renderer' module Thinreports module SectionReport module Renderer class GroupRenderer def initialize(pdf) @pdf = pdf @section_renderer = Renderer::SectionRenderer.new(pdf) end def render(report, group) pdf.start_new_page_for_section_report report.schema current_page_height = 0 max_page_height = pdf.max_content_height group.headers.each do |header| section_renderer.render(header) current_page_height += section_renderer.section_height(header) end group.details.each do |detail| if current_page_height + section_renderer.section_height(detail) > max_page_height pdf.start_new_page_for_section_report report.schema current_page_height = 0 group.headers.each do |header| if header.schema.every_page? section_renderer.render(header) current_page_height += section_renderer.section_height(header) end end end section_renderer.render(detail) current_page_height += section_renderer.section_height(detail) end group.footers.each do |footer| if current_page_height + section_renderer.section_height(footer) > max_page_height pdf.start_new_page_for_section_report report.schema current_page_height = 0 end section_renderer.render(footer) current_page_height += section_renderer.section_height(footer) end end private attr_reader :pdf, :section_renderer end end end end ================================================ FILE: lib/thinreports/section_report/pdf/renderer/section_height.rb ================================================ # frozen_string_literal: true module Thinreports module SectionReport module Renderer module SectionHeight LayoutInfo = Struct.new(:shape, :content_height, :top_margin, :bottom_margin) def section_height(section) return [section.min_height || 0, section.schema.height].max if !section.schema.auto_stretch? || section.items.empty? item_layouts = section.items.map { |item| item_layout(section, item.internal) }.compact min_bottom_margin = item_layouts.each_with_object([]) do |l, margins| margins << l.bottom_margin if l.shape.format.affect_bottom_margin? end.min.to_f max_content_bottom = item_layouts.each_with_object([]) do |l, bottoms| bottoms << l.top_margin + l.content_height if l.shape.format.affect_bottom_margin? end.max.to_f [section.min_height || 0, max_content_bottom + min_bottom_margin].max end def calc_float_content_bottom(section) item_layouts = section.items.map { |item| item_layout(section, item.internal) }.compact item_layouts .map { |l| l.top_margin + l.content_height } .max.to_f end def item_layout(section, shape) if shape.type_of?(Core::Shape::TextBlock::TYPE_NAME) text_layout(section, shape) elsif shape.type_of?(Core::Shape::StackView::TYPE_NAME) stack_view_layout(section, shape) elsif shape.type_of?(Core::Shape::ImageBlock::TYPE_NAME) image_block_layout(section, shape) elsif shape.type_of?('ellipse') cy, ry = shape.format.attributes.values_at('cy', 'ry') static_layout(section, shape, cy - ry, ry * 2) elsif shape.type_of?('line') y1, y2 = shape.format.attributes.values_at('y1', 'y2') static_layout(section, shape, [y1, y2].min, (y2 - y1).abs) else y, height = shape.format.attributes.values_at('y', 'height') raise ArgumentError.new("Unknown layout for #{shape}") if height == nil || y == nil static_layout(section, shape, y, height) end end def static_layout(section, shape, y, height) LayoutInfo.new(shape, height, y, section.schema.height - height - y) end def image_block_layout(section, shape) y, height = shape.format.attributes.values_at('y', 'height') if shape.style.finalized_styles['position-y'] == 'top' dimensions = pdf.shape_iblock_dimenions(shape) content_height = dimensions ? dimensions[1] : 0 LayoutInfo.new(shape, content_height, y, section.schema.height - height - y) else static_layout(section, shape, y, height) end end def calc_text_block_height(shape) height = 0 pdf.draw_shape_tblock(shape) do |array, options| modified_options = options.merge(at: [0, 10_000], height: 10_000) height = pdf.pdf.height_of_formatted(array, modified_options) end height end def text_layout(section, shape) y, schema_height = shape.format.attributes.values_at('y', 'height') content_height = if shape.style.finalized_styles['overflow'] == 'expand' [schema_height, calc_text_block_height(shape)].max else schema_height end LayoutInfo.new(shape, content_height, y, section.schema.height - schema_height - y) end def stack_view_layout(section, shape) schema_height = 0 shape.format.rows.each {|row| schema_height += row.attributes['height']} y = shape.format.attributes['y'] LayoutInfo.new(shape, stack_view_renderer.section_height(shape), y, section.schema.height - schema_height - y) end end end end end ================================================ FILE: lib/thinreports/section_report/pdf/renderer/section_renderer.rb ================================================ # frozen_string_literal: true require_relative 'stack_view_renderer' require_relative 'section_height' require_relative 'draw_item' module Thinreports module SectionReport module Renderer class SectionRenderer include SectionHeight include DrawItem def initialize(pdf) @pdf = pdf end def render(section) doc = pdf.pdf actual_height = section_height(section) doc.bounding_box([0, doc.cursor], width: doc.bounds.width, height: actual_height) do section.items.each do |item| draw_item(item, (actual_height - section.schema.height)) end end end private attr_reader :pdf def stack_view_renderer @stack_view_renderer ||= Renderer::StackViewRenderer.new(pdf) end end end end end ================================================ FILE: lib/thinreports/section_report/pdf/renderer/stack_view_renderer.rb ================================================ # frozen_string_literal: true require_relative 'stack_view_row_renderer' module Thinreports module SectionReport module Renderer class StackViewRenderer def initialize(pdf) @pdf = pdf @row_renderer = Renderer::StackViewRowRenderer.new(pdf) end RowLayout = Struct.new(:row, :height, :top) def section_height(shape) row_layouts = build_row_layouts(shape.rows) total_row_height = row_layouts.sum(0, &:height) float_content_bottom = row_layouts .map { |l| row_renderer.calc_float_content_bottom(l.row) + l.top } .max.to_f [total_row_height, float_content_bottom].max end def render(shape) doc = pdf.pdf x, y, w = shape.format.attributes.values_at('x', 'y', 'width') doc.bounding_box([x, doc.bounds.height - y], width: w, height: section_height(shape)) do shape.rows.each do |row| row_renderer.render(row) end end end private attr_reader :pdf, :row_renderer def build_row_layouts(rows) row_layouts = rows.map { |row| RowLayout.new(row, row_renderer.section_height(row)) } row_layouts.inject(0) do |top, row_layout| row_layout.top = top top + row_layout.height end row_layouts end end end end end ================================================ FILE: lib/thinreports/section_report/pdf/renderer/stack_view_row_renderer.rb ================================================ # frozen_string_literal: true require_relative 'section_height' require_relative 'draw_item' module Thinreports module SectionReport module Renderer class StackViewRowRenderer include SectionHeight include DrawItem def initialize(pdf) @pdf = pdf end def render(row) doc = pdf.pdf actual_height = section_height(row) doc.bounding_box([0, doc.cursor], width: doc.bounds.width, height: actual_height) do row.items.each do |item| draw_item(item, (actual_height - row.schema.height)) end end end private attr_reader :pdf def stack_view_renderer raise Thinreports::Errors::InvalidLayoutFormat, 'nested StackView does not supported' end end end end end ================================================ FILE: lib/thinreports/section_report/schema/loader.rb ================================================ # frozen_string_literal: true require_relative 'parser' module Thinreports module SectionReport module Schema class Loader def initialize @parser = Schema::Parser.new end def load_from_file(filename) data = File.read(filename, encoding: 'UTF-8') load_from_data(data) end def load_from_data(data) parser.parse(data) end private attr_reader :parser end end end end ================================================ FILE: lib/thinreports/section_report/schema/parser.rb ================================================ # frozen_string_literal: true require 'json' require_relative 'report' require_relative 'section' module Thinreports module SectionReport module Schema class Parser def parse(schema_json_data) schema_data = JSON.parse(schema_json_data) section_schema_datas = schema_data['sections'].group_by { |section| section['type'] } Schema::Report.new( schema_data, headers: parse_sections(:header, section_schema_datas['header']), details: parse_sections(:detail, section_schema_datas['detail']), footers: parse_sections(:footer, section_schema_datas['footer']) ) end private attr_reader :schema_data def parse_sections(section_type, section_schema_datas = nil) return {} if section_schema_datas.nil? section_schema_datas.each_with_object({}) do |section_schema_data, section_schemas| id = section_schema_data['id'] section_schemas[id.to_sym] = parse_section(section_type, section_schema_data) end end def parse_section(type, section_schema_data) items = section_schema_data['items'].map do |item_schema_data| item_type = item_schema_data['type'] Core::Shape::Format(item_type).new(item_schema_data) end section_schema_class_for(type).new(section_schema_data, items: items) end def section_schema_class_for(section_type) Schema::Section.const_get(section_type.capitalize) end end end end end ================================================ FILE: lib/thinreports/section_report/schema/report.rb ================================================ # frozen_string_literal: true module Thinreports module SectionReport module Schema class Report < Core::Shape::Manager::Format config_reader last_version: %w( version ) config_reader report_title: %w( title ) config_reader page_paper_type: %w( report paper-type ), page_orientation: %w( report orientation ), page_margin: %w( report margin ), page_width: %w[report width], page_height: %w[report height] attr_reader :headers, :details, :footers def user_paper_type? page_paper_type == 'user' end def initialize(schema_data, headers:, details:, footers:) super(schema_data) @headers = headers @details = details @footers = footers end end end end end ================================================ FILE: lib/thinreports/section_report/schema/section.rb ================================================ # frozen_string_literal: true module Thinreports module SectionReport module Schema module Section class Base < Core::Shape::Manager::Format config_reader :id, :type config_reader :height config_checker true, :display config_checker true, auto_stretch: 'auto-stretch' attr_reader :items def initialize(schema_data, items:) super(schema_data) initialize_items(items) end def find_item(id) @item_with_ids[id.to_sym] end private def initialize_items(items) @items = items @item_with_ids = items.each_with_object({}) do |item, item_with_ids| next if item.id.empty? item_with_ids[item.id.to_sym] = item end end end class Header < Base config_checker true, every_page: 'every-page' end class Footer < Base end class Detail < Base end end end end end ================================================ FILE: lib/thinreports/section_report.rb ================================================ # frozen_string_literal: true module Thinreports Core = BasicReport::Core Generator = BasicReport::Generator def self.generate(report_params, filename: nil) SectionReport::Generate.new.call(report_params, filename: filename) end end require_relative 'section_report/generate' ================================================ FILE: lib/thinreports/version.rb ================================================ # frozen_string_literal: true module Thinreports VERSION = '0.14.2' end ================================================ FILE: lib/thinreports.rb ================================================ # frozen_string_literal: true require 'pathname' module Thinreports def self.root @root ||= Pathname.new(__FILE__).join('..', '..') end end require_relative 'thinreports/version' require_relative 'thinreports/config' require_relative 'thinreports/basic_report' require_relative 'thinreports/section_report' ================================================ FILE: test/basic_report/data/legacy_layout/all-items.tlf ================================================ {"version":"0.8.2","config":{"title":"Report Title","option":{},"page":{"paper-type":"A4","orientation":"portrait","margin-top":"20","margin-bottom":"30","margin-left":"40","margin-right":"50"}},"svg":"text","state":{"layout-guide":[]}} ================================================ FILE: test/basic_report/features/dynamic_style/templates/styles.tlf ================================================ { "version": "0.9.0", "items": [ { "id": "", "type": "text", "display": true, "description": "", "x": 20, "y": 42, "width": 374.1, "height": 23, "style": { "font-family": [ "Helvetica" ], "font-size": 20, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [ "underline" ] }, "texts": [ "The Basic Style" ] }, { "id": "", "type": "text", "display": true, "description": "", "x": 20, "y": 72, "width": 280, "height": 14, "style": { "font-family": [ "Courier New" ], "font-size": 12, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "Visibility: To true from false." ] }, { "id": "basic_show1", "type": "rect", "display": false, "description": "", "x": 20, "y": 90, "width": 52, "height": 34, "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid", "fill-color": "#FFFFFF" }, "border-radius": 0 }, { "id": "basic_show2", "type": "ellipse", "display": false, "description": "", "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid", "fill-color": "#FFFFFF" }, "cx": 118.45, "cy": 108.5, "rx": 31.55, "ry": 21.5 }, { "id": "basic_show3", "type": "text", "display": false, "description": "", "x": 165.1, "y": 97, "width": 36, "height": 20.5, "style": { "font-family": [ "Helvetica" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "Text" ] }, { "id": "basic_show4", "type": "image", "display": false, "description": "", "x": 213.1, "y": 91, "width": 41, "height": 41, "data": { "mime-type": "image/png", "base64": "iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAOdAAADnQBaySz1gAAABZ0RVh0Q3JlYXRpb24gVGltZQAwOS8xNS8xMe/neuYAAAAfdEVYdFNvZnR3YXJlAE1hY3JvbWVkaWEgRmlyZXdvcmtzIDi1aNJ4AAAAV0lEQVRoge3PAQ3AMAzAsH78OfckLi26bATJM7M7P3BuB3zFSI2RGiM1RmqM1BipMVJjpMZIjZEaIzVGaozUGKkxUmOkxkiNkRojNUZqjNQYqTFSY6TmBQWzAmLkoN8LAAAAAElFTkSuQmCC" } }, { "id": "basic_show5", "type": "text-block", "display": false, "description": "", "x": 267.1, "y": 97, "width": 106, "height": 20, "style": { "font-family": [ "Helvetica" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "break-word" }, "reference-id": "", "value": "", "multiple-line": false, "format": { "base": "", "type": "" } }, { "id": "", "type": "text", "display": true, "description": "", "x": 20, "y": 147, "width": 280, "height": 14, "style": { "font-family": [ "Courier New" ], "font-size": 12, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "Visibility: To false from true." ] }, { "id": "basic_hide1", "type": "rect", "display": true, "description": "", "x": 20, "y": 165, "width": 52, "height": 34, "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid", "fill-color": "#FFFFFF" }, "border-radius": 0 }, { "id": "basic_hide2", "type": "ellipse", "display": true, "description": "", "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid", "fill-color": "#FFFFFF" }, "cx": 118.45, "cy": 183.5, "rx": 31.55, "ry": 21.5 }, { "id": "basic_hide3", "type": "text", "display": true, "description": "", "x": 165.1, "y": 172, "width": 36, "height": 20.5, "style": { "font-family": [ "Helvetica" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "Text" ] }, { "id": "basic_hide4", "type": "image", "display": true, "description": "", "x": 213.1, "y": 166, "width": 41, "height": 41, "data": { "mime-type": "image/png", "base64": "iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAOdAAADnQBaySz1gAAABZ0RVh0Q3JlYXRpb24gVGltZQAwOS8xNS8xMe/neuYAAAAfdEVYdFNvZnR3YXJlAE1hY3JvbWVkaWEgRmlyZXdvcmtzIDi1aNJ4AAAAV0lEQVRoge3PAQ3AMAzAsH78OfckLi26bATJM7M7P3BuB3zFSI2RGiM1RmqM1BipMVJjpMZIjZEaIzVGaozUGKkxUmOkxkiNkRojNUZqjNQYqTFSY6TmBQWzAmLkoN8LAAAAAElFTkSuQmCC" } }, { "id": "basic_hide5", "type": "text-block", "display": true, "description": "", "x": 267.1, "y": 172, "width": 106, "height": 20, "style": { "font-family": [ "Helvetica" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "break-word" }, "reference-id": "", "value": "", "multiple-line": false, "format": { "base": "", "type": "" } }, { "id": "", "type": "text", "display": true, "description": "", "x": 20, "y": 222, "width": 374.1, "height": 23, "style": { "font-family": [ "Helvetica" ], "font-size": 20, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [ "underline" ] }, "texts": [ "The Graphic Style" ] }, { "id": "graphic_bcolor1", "type": "rect", "display": true, "description": "", "x": 20, "y": 269, "width": 89, "height": 44, "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid", "fill-color": "#FFFFFF" }, "border-radius": 0 }, { "id": "", "type": "text", "display": true, "description": "", "x": 20, "y": 252, "width": 245, "height": 14, "style": { "font-family": [ "Courier New" ], "font-size": 12, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "Border Color: To red from black." ] }, { "id": "graphic_bcolor2", "type": "ellipse", "display": true, "description": "", "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid", "fill-color": "#FFFFFF" }, "cx": 167.05, "cy": 291, "rx": 38.05, "ry": 23 }, { "id": "graphic_bcolor3", "type": "line", "display": true, "description": "", "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid" }, "x1": 227.1, "y1": 276, "x2": 286.1, "y2": 302 }, { "id": "graphic_bcolor4", "type": "rect", "display": true, "description": "", "x": 311.1, "y": 268, "width": 89, "height": 44, "style": { "border-color": "#ff0000", "border-width": 1, "border-style": "solid", "fill-color": "#e5b9b7" }, "border-radius": 0 }, { "id": "", "type": "text", "display": true, "description": "", "x": 311, "y": 316.9, "width": 182.1, "height": 14, "style": { "font-family": [ "Courier New" ], "font-size": 12, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "To none from red." ] }, { "id": "", "type": "text", "display": true, "description": "", "x": 20, "y": 332, "width": 245, "height": 14, "style": { "font-family": [ "Courier New" ], "font-size": 12, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "Border Width: To 5 from 1." ] }, { "id": "graphic_bwidth1", "type": "rect", "display": true, "description": "", "x": 20, "y": 350, "width": 89, "height": 44, "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid", "fill-color": "#FFFFFF" }, "border-radius": 0 }, { "id": "graphic_bwidth2", "type": "ellipse", "display": true, "description": "", "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid", "fill-color": "#FFFFFF" }, "cx": 167.05, "cy": 370.9, "rx": 38.05, "ry": 23 }, { "id": "graphic_bwidth3", "type": "line", "display": true, "description": "", "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid" }, "x1": 227.1, "y1": 355.9, "x2": 286.1, "y2": 381.9 }, { "id": "graphic_bwidth4", "type": "rect", "display": true, "description": "", "x": 311.1, "y": 343, "width": 89, "height": 44, "style": { "border-color": "#ff0000", "border-width": 0, "border-style": "solid", "fill-color": "#e5b9b7" }, "border-radius": 0 }, { "id": "", "type": "text", "display": true, "description": "", "x": 311, "y": 391.9, "width": 182.1, "height": 14, "style": { "font-family": [ "Courier New" ], "font-size": 12, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "To 2 from none(0)." ] }, { "id": "graphic_bwidth5", "type": "rect", "display": true, "description": "", "x": 448.1, "y": 343, "width": 89, "height": 44, "style": { "border-color": "#ff0000", "border-width": 1, "border-style": "solid", "fill-color": "#e5b9b7" }, "border-radius": 0 }, { "id": "", "type": "text", "display": true, "description": "", "x": 448, "y": 391.9, "width": 129.6, "height": 14, "style": { "font-family": [ "Courier New" ], "font-size": 12, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "To none(0) from 1." ] }, { "id": "", "type": "text", "display": true, "description": "", "x": 20, "y": 412, "width": 245, "height": 14, "style": { "font-family": [ "Courier New" ], "font-size": 12, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "Fill Color: To red from white." ] }, { "id": "graphic_fcolor1", "type": "rect", "display": true, "description": "", "x": 20, "y": 428.8, "width": 89, "height": 44, "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid", "fill-color": "#FFFFFF" }, "border-radius": 0 }, { "id": "graphic_fcolor2", "type": "ellipse", "display": true, "description": "", "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid", "fill-color": "#FFFFFF" }, "cx": 167.05, "cy": 450.8, "rx": 38.05, "ry": 23 }, { "id": "", "type": "text", "display": true, "description": "", "x": 226, "y": 476.7, "width": 182.1, "height": 14, "style": { "font-family": [ "Courier New" ], "font-size": 12, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "To none from black." ] }, { "id": "", "type": "ellipse", "display": true, "description": "", "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid", "fill-color": "#FFFFFF" }, "cx": 302.5, "cy": 434.5, "rx": 37.5, "ry": 15.5 }, { "id": "graphic_fcolor3", "type": "rect", "display": true, "description": "", "x": 226, "y": 427.8, "width": 73, "height": 35.2, "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid", "fill-color": "#000000" }, "border-radius": 0 }, { "id": "", "type": "text", "display": true, "description": "", "x": 371, "y": 476.7, "width": 182.1, "height": 14, "style": { "font-family": [ "Courier New" ], "font-size": 12, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "To red from none." ] }, { "id": "", "type": "ellipse", "display": true, "description": "", "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid", "fill-color": "#FFFFFF" }, "cx": 447.5, "cy": 434.5, "rx": 37.5, "ry": 15.5 }, { "id": "graphic_fcolor4", "type": "rect", "display": true, "description": "", "x": 371, "y": 427.8, "width": 73, "height": 35.2, "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid", "fill-color": "none" }, "border-radius": 0 }, { "id": "", "type": "text", "display": true, "description": "", "x": 20, "y": 497, "width": 374.1, "height": 23, "style": { "font-family": [ "Helvetica" ], "font-size": 20, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [ "underline" ] }, "texts": [ "The Text Style" ] }, { "id": "", "type": "text", "display": true, "description": "", "x": 20, "y": 527, "width": 122, "height": 14, "style": { "font-family": [ "Courier New" ], "font-size": 12, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "Bold Style:" ] }, { "id": "text_b1", "type": "text", "display": true, "description": "", "x": 20, "y": 549.2, "width": 131, "height": 15, "style": { "font-family": [ "Helvetica" ], "font-size": 12, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "To bold from normal." ] }, { "id": "text_b2", "type": "text", "display": true, "description": "", "x": 20, "y": 567, "width": 144, "height": 15, "style": { "font-family": [ "Helvetica" ], "font-size": 12, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [ "bold" ] }, "texts": [ "To normal from bold." ] }, { "id": "", "type": "text", "display": true, "description": "", "x": 150, "y": 527, "width": 122, "height": 14, "style": { "font-family": [ "Courier New" ], "font-size": 12, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "Italic Style:" ] }, { "id": "text_i1", "type": "text", "display": true, "description": "", "x": 150, "y": 549.2, "width": 132, "height": 15, "style": { "font-family": [ "Helvetica" ], "font-size": 12, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "To italic from normal." ] }, { "id": "text_i2", "type": "text", "display": true, "description": "", "x": 150, "y": 567, "width": 146, "height": 15, "style": { "font-family": [ "Helvetica" ], "font-size": 12, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [ "italic" ] }, "texts": [ "To normal from italic." ] }, { "id": "", "type": "text", "display": true, "description": "", "x": 280, "y": 527, "width": 122, "height": 14, "style": { "font-family": [ "Courier New" ], "font-size": 12, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "Underline Style:" ] }, { "id": "text_u1", "type": "text", "display": true, "description": "", "x": 280, "y": 549.2, "width": 132, "height": 15, "style": { "font-family": [ "Helvetica" ], "font-size": 12, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "To underline from none." ] }, { "id": "text_u2", "type": "text", "display": true, "description": "", "x": 280, "y": 567, "width": 146, "height": 15, "style": { "font-family": [ "Helvetica" ], "font-size": 12, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [ "underline" ] }, "texts": [ "To none from underline." ] }, { "id": "", "type": "text", "display": true, "description": "", "x": 425, "y": 527, "width": 136.8, "height": 14, "style": { "font-family": [ "Courier New" ], "font-size": 12, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "Line-Through Style:" ] }, { "id": "text_l1", "type": "text", "display": true, "description": "", "x": 425, "y": 549.2, "width": 145, "height": 15, "style": { "font-family": [ "Helvetica" ], "font-size": 12, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "To line-through from none." ] }, { "id": "text_l2", "type": "text", "display": true, "description": "", "x": 425, "y": 567, "width": 146, "height": 15, "style": { "font-family": [ "Helvetica" ], "font-size": 12, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [ "linethrough" ] }, "texts": [ "To none from line-through." ] }, { "id": "text_b3", "type": "text-block", "display": true, "description": "", "x": 20, "y": 586.2, "width": 119, "height": 14, "style": { "font-family": [ "Helvetica" ], "font-size": 12, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "break-word" }, "reference-id": "", "value": "", "multiple-line": false, "format": { "base": "", "type": "" } }, { "id": "text_b4", "type": "text-block", "display": true, "description": "", "x": 20, "y": 606, "width": 119, "height": 14, "style": { "font-family": [ "Helvetica" ], "font-size": 12, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [ "bold" ], "overflow": "truncate", "word-wrap": "break-word" }, "reference-id": "", "value": "", "multiple-line": false, "format": { "base": "", "type": "" } }, { "id": "text_i3", "type": "text-block", "display": true, "description": "", "x": 150, "y": 586.2, "width": 119, "height": 14, "style": { "font-family": [ "Helvetica" ], "font-size": 12, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "break-word" }, "reference-id": "", "value": "", "multiple-line": false, "format": { "base": "", "type": "" } }, { "id": "text_i4", "type": "text-block", "display": true, "description": "", "x": 150, "y": 606, "width": 119, "height": 14, "style": { "font-family": [ "Helvetica" ], "font-size": 12, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [ "italic" ], "overflow": "truncate", "word-wrap": "break-word" }, "reference-id": "", "value": "", "multiple-line": false, "format": { "base": "", "type": "" } }, { "id": "text_u3", "type": "text-block", "display": true, "description": "", "x": 280, "y": 586.2, "width": 119, "height": 14, "style": { "font-family": [ "Helvetica" ], "font-size": 12, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "break-word" }, "reference-id": "", "value": "", "multiple-line": false, "format": { "base": "", "type": "" } }, { "id": "text_u4", "type": "text-block", "display": true, "description": "", "x": 280, "y": 606, "width": 119, "height": 14, "style": { "font-family": [ "Helvetica" ], "font-size": 12, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [ "underline" ], "overflow": "truncate", "word-wrap": "break-word" }, "reference-id": "", "value": "", "multiple-line": false, "format": { "base": "", "type": "" } }, { "id": "text_l3", "type": "text-block", "display": true, "description": "", "x": 425, "y": 586.2, "width": 119, "height": 14, "style": { "font-family": [ "Helvetica" ], "font-size": 12, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "break-word" }, "reference-id": "", "value": "", "multiple-line": false, "format": { "base": "", "type": "" } }, { "id": "text_l4", "type": "text-block", "display": true, "description": "", "x": 425, "y": 606, "width": 119, "height": 14, "style": { "font-family": [ "Helvetica" ], "font-size": 12, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [ "linethrough" ], "overflow": "truncate", "word-wrap": "break-word" }, "reference-id": "", "value": "", "multiple-line": false, "format": { "base": "", "type": "" } }, { "id": "", "type": "text", "display": true, "description": "", "x": 20, "y": 637, "width": 223.1, "height": 14, "style": { "font-family": [ "Courier New" ], "font-size": 12, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "Text Align: To right from left." ] }, { "id": "text_a1", "type": "text", "display": true, "description": "", "x": 20, "y": 659, "width": 155.1, "height": 23.2, "style": { "font-family": [ "Helvetica" ], "font-size": 12, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "To right from left." ] }, { "id": "", "type": "rect", "display": true, "description": "", "x": 20, "y": 659, "width": 155.1, "height": 23.2, "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid", "fill-color": "none" }, "border-radius": 0 }, { "id": "text_a2", "type": "text-block", "display": true, "description": "", "x": 192.1, "y": 660.2, "width": 138, "height": 14, "style": { "font-family": [ "Helvetica" ], "font-size": 12, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "break-word" }, "reference-id": "", "value": "", "multiple-line": false, "format": { "base": "", "type": "" } }, { "id": "text_a3", "type": "text-block", "display": true, "description": "日本語", "x": 344.1, "y": 660.2, "width": 153.1, "height": 44.1, "style": { "font-family": [ "IPAMincho" ], "font-size": 12, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "break-word" }, "reference-id": "", "value": "", "multiple-line": true, "format": { "base": "", "type": "" } }, { "id": "", "type": "rect", "display": true, "description": "", "x": 192.1, "y": 660.2, "width": 138, "height": 22, "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid", "fill-color": "none" }, "border-radius": 0 }, { "id": "", "type": "rect", "display": true, "description": "", "x": 344.1, "y": 660.2, "width": 153.1, "height": 44.1, "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid", "fill-color": "none" }, "border-radius": 0 }, { "id": "", "type": "text", "display": true, "description": "", "x": 20, "y": 712, "width": 251.9, "height": 14, "style": { "font-family": [ "Courier New" ], "font-size": 12, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "Vertical Align: To bottom from top." ] }, { "id": "text_va1", "type": "text", "display": true, "description": "", "x": 20, "y": 729, "width": 155.1, "height": 50.3, "style": { "font-family": [ "Helvetica" ], "font-size": 12, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "To bottom from top." ] }, { "id": "", "type": "rect", "display": true, "description": "", "x": 20, "y": 729, "width": 155.1, "height": 50.3, "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid", "fill-color": "none" }, "border-radius": 0 }, { "id": "text_va2", "type": "text-block", "display": true, "description": "日本語", "x": 189, "y": 730, "width": 153.1, "height": 50.3, "style": { "font-family": [ "IPAMincho" ], "font-size": 12, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "break-word" }, "reference-id": "", "value": "", "multiple-line": true, "format": { "base": "", "type": "" } }, { "id": "", "type": "rect", "display": true, "description": "", "x": 189, "y": 730, "width": 153.1, "height": 50.3, "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid", "fill-color": "none" }, "border-radius": 0 }, { "id": "", "type": "text", "display": true, "description": "", "x": 20, "y": 789, "width": 245, "height": 14, "style": { "font-family": [ "Courier New" ], "font-size": 12, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "Text Color: To red from black." ] }, { "id": "text_color1", "type": "text", "display": true, "description": "", "x": 20, "y": 809, "width": 131, "height": 15, "style": { "font-family": [ "Helvetica" ], "font-size": 12, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "To red from black." ] }, { "id": "text_color2", "type": "text-block", "display": true, "description": "日本語", "x": 136, "y": 808.1, "width": 119, "height": 12, "style": { "font-family": [ "IPAMincho" ], "font-size": 12, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "break-word" }, "reference-id": "", "value": "", "multiple-line": false, "format": { "base": "", "type": "" } }, { "id": "title", "type": "text-block", "display": true, "description": "", "x": 183.1, "y": 22, "width": 208, "height": 19, "style": { "font-family": [ "Times New Roman" ], "font-size": 18, "color": "#000000", "text-align": "center", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [ "bold", "underline" ], "overflow": "truncate", "word-wrap": "break-word" }, "reference-id": "", "value": "", "multiple-line": false, "format": { "base": "", "type": "" } }, { "id": "basic_show6", "type": "image-block", "display": false, "description": "", "x": 387, "y": 87, "width": 74, "height": 58, "style": { "position-x": "left", "position-y": "top" } }, { "id": "basic_hide6", "type": "image-block", "display": true, "description": "", "x": 387, "y": 162, "width": 74, "height": 58, "style": { "position-x": "left", "position-y": "top" } } ], "state": { "layout-guides": [] }, "title": "Dynamic Style", "report": { "paper-type": "A4", "orientation": "portrait", "margin": [ 20, 20, 20, 20 ] } } ================================================ FILE: test/basic_report/features/dynamic_style/templates/styles_in_list.tlf ================================================ { "version": "0.9.0", "items": [ { "id": "list", "type": "list", "display": true, "description": "", "x": 20, "y": 77, "width": 555.2, "height": 415.2, "header": { "enabled": true, "height": 51, "translate": { "x": 0, "y": 57 }, "items": [ { "id": "rect", "type": "rect", "display": true, "description": "", "x": 110.1, "y": 31, "width": 77, "height": 31, "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid", "fill-color": "#FFFFFF" }, "border-radius": 0 }, { "id": "text", "type": "text", "display": true, "description": "", "x": 223.1, "y": 21, "width": 147, "height": 45.1, "style": { "font-family": [ "Helvetica" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "Header Text" ] }, { "id": "", "type": "rect", "display": true, "description": "", "x": 223.1, "y": 21, "width": 147, "height": 45.1, "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid", "fill-color": "none" }, "border-radius": 0 }, { "id": "", "type": "rect", "display": true, "description": "", "x": 389.1, "y": 23, "width": 144.1, "height": 42.1, "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid", "fill-color": "none" }, "border-radius": 0 }, { "id": "", "type": "line", "display": true, "description": "", "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid" }, "x1": 20, "y1": 71, "x2": 575.2, "y2": 71 }, { "id": "tblock", "type": "text-block", "display": true, "description": "", "x": 389.1, "y": 23, "width": 144.1, "height": 42.1, "style": { "font-family": [ "Helvetica" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "break-word" }, "reference-id": "", "value": "Header Tblock", "multiple-line": true, "format": { "base": "", "type": "" } } ] }, "detail": { "height": 57, "translate": { "x": 0, "y": 1.2 }, "items": [ { "id": "rect", "type": "rect", "display": true, "description": "", "x": 111.1, "y": 137.8, "width": 77, "height": 31, "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid", "fill-color": "#FFFFFF" }, "border-radius": 0 }, { "id": "text", "type": "text", "display": true, "description": "", "x": 223.1, "y": 132.8, "width": 147, "height": 45.1, "style": { "font-family": [ "Helvetica" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "Detail Text" ] }, { "id": "", "type": "rect", "display": true, "description": "", "x": 223.1, "y": 132.8, "width": 147, "height": 45.1, "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid", "fill-color": "none" }, "border-radius": 0 }, { "id": "tblock", "type": "text-block", "display": true, "description": "", "x": 389.1, "y": 134.8, "width": 144.1, "height": 42.1, "style": { "font-family": [ "Helvetica" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "break-word" }, "reference-id": "", "value": "Detail Tblock", "multiple-line": true, "format": { "base": "", "type": "" } }, { "id": "", "type": "rect", "display": true, "description": "", "x": 389.1, "y": 134.8, "width": 144.1, "height": 42.1, "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid", "fill-color": "none" }, "border-radius": 0 }, { "id": "", "type": "line", "display": true, "description": "", "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid" }, "x1": 20, "y1": 183.8, "x2": 575.2, "y2": 183.8 } ] }, "page-footer": { "enabled": false, "height": 0, "translate": { "x": 0, "y": -48.6 }, "items": [] }, "footer": { "enabled": false, "height": 0, "translate": { "x": 0, "y": -128.7 }, "items": [] }, "content-height": 364.2, "auto-page-break": true }, { "id": "", "type": "text", "display": true, "description": "", "x": 197.6, "y": 36, "width": 200, "height": 20, "style": { "font-family": [ "Times New Roman" ], "font-size": 18, "color": "#000000", "text-align": "center", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [ "underline" ] }, "texts": [ "Dynamic Style in List" ] } ], "state": { "layout-guides": [] }, "title": "Dynamic Style in List", "report": { "paper-type": "A4", "orientation": "portrait", "margin": [ 20, 20, 20, 20 ] } } ================================================ FILE: test/basic_report/features/dynamic_style/test_feature.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::TestDynamicStyleFeature < Thinreports::FeatureTest[__dir__] feature do image = path_of('image.png') setup_values = Proc.new do |page| # The Basic Style page.values(basic_show5: 'TextBlock', basic_show6: image, basic_hide5: 'TextBlock', basic_hide6: image) # The Text Style page.values(text_b3: 'To bold', text_b4: 'To normal', text_i3: 'To italic', text_i4: 'To normal', text_u3: 'To underline', text_u4: 'To none', text_l3: 'To line-through', text_l4: 'To none') page.values(text_a2: 'To right align', text_a3: "To right align\n右寄せ", text_va2: "To bottom align\n下揃え", text_color2: '赤色') end report = Thinreports::BasicReport::Report.new layout: template_path('templates/styles.tlf') # Create raw-page. report.start_new_page report.page.item(:title).value('Original Page') setup_values.call(report.page) # Create styled-page. report.start_new_page setup_values.call(report.page) report.page.item(:title).value('Styled Page') # The Basic Style 6.times do |i| report.page.item("basic_show#{i + 1}").style(:visible, true) report.page.item("basic_hide#{i + 1}").style(:visible, false) end # The Graphic Style report.page.item(:graphic_bcolor1).style(:border_color, '#ff0000') report.page[:graphic_bcolor2].style(:border_color, 'red') # nil or 'none'. report.page.item(:graphic_bcolor4).style(:border_color, nil) report.page.item(:graphic_bwidth1).style(:border_width, 5) report.page.item(:graphic_bwidth2).style(:border_width, 5) report.page.item(:graphic_bwidth3).style(:border_width, 5) report.page.item(:graphic_bwidth4).style(:border_width, 2) report.page.item(:graphic_bwidth5).style(:border_width, 0) report.page.item(:graphic_fcolor1).style(:fill_color, '#ff0000') # nil or 'none' report.page.item(:graphic_fcolor3).style(:fill_color, 'none') report.page.item(:graphic_fcolor4).style(:fill_color, 'ff0000') # The Text Style [1, 3].each do |i| report.page["text_b#{i}"].style(:bold, true) report.page.item("text_i#{i}").style(:italic, true) report.page.item("text_u#{i}").style(:underline, true) report.page.item("text_l#{i}").style(:linethrough, true) end [2, 4].each do |i| report.page.item("text_b#{i}").style(:bold, false) report.page.item("text_i#{i}").style(:italic, false) report.page.item("text_u#{i}").style(:underline, false) report.page.item("text_l#{i}").style(:linethrough, false) end 3.times do |i| report.page.item("text_a#{i + 1}").style(:align, :right) end 2.times do |i| report.page.item("text_va#{i + 1}").style(:valign, :bottom) end 2.times do |i| report.page["text_color#{i + 1}"].style(:color, 'ff0000') end report.start_new_page(layout: template_path('templates/styles_in_list.tlf')) # Settings for Header. report.page.list(:list).header do |header| header.item(:rect).styles(border_color: nil, fill_color: 'ff00ff') header.item(:text).styles(align: :center, valign: :middle, bold: true) header[:tblock].styles(align: :center, valign: :middle, color: 'red', linethrough: true) end 1.step(10, 1) do |i| # Flag of overflowed list-page. is_overflowed = report.page.list(:list).overflow? # Add details. report.page.list(:list).add_row do |row| case when i % 2 == 0 row.item(:rect).styles(border_color: 'ff0000', border_width: 3, fill_color: '0000ff') row.item(:text).styles(color: 'red', align: :left, valign: :middle) row.item(:tblock).styles(color: 'blue', align: :left, valign: :middle) when i % 3 == 0 row.item(:rect).style(:visible, false) row.item(:text).styles(color: '0000ff', align: :right, valign: :bottom) row.item(:tblock).styles(color: 'ff0000', align: :right, valign: :bottom) end end # Change header styles when list-page was overflowed. if is_overflowed report.page.list(:list).header do |header| header.item(:rect).styles(border_color: 'black', fill_color: '#ffffff') header.item(:text).styles(align: :left, valign: :top, bold: false) header.item(:tblock).styles(align: :left, valign: :top, color: '#000000', linethrough: false) end end end assert_pdf report.generate end end ================================================ FILE: test/basic_report/features/eudc/template.tlf ================================================ { "version": "0.9.0", "items": [ { "id": "", "type": "text", "display": true, "description": "", "x": 105.4, "y": 60, "width": 631, "height": 100, "style": { "font-family": [ "IPAMincho" ], "font-size": 36, "color": "#000000", "text-align": "center", "vertical-align": "middle", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "日本で生まれ世界が育てた言語 Ruby" ] }, { "id": "eudc", "type": "text-block", "display": true, "description": "", "x": 20, "y": 154.3, "width": 801.8, "height": 61, "style": { "font-family": [ "IPAMincho" ], "font-size": 36, "color": "#000000", "text-align": "center", "vertical-align": "middle", "line-height": "", "line-height-ratio": "", "letter-spacing": null, "font-style": [], "overflow": "truncate", "word-wrap": "break-word" }, "reference-id": "", "value": "", "multiple-line": true, "format": { "base": "", "type": "" } }, { "id": "eudc_bold", "type": "text-block", "display": true, "description": "", "x": 249.2, "y": 249, "width": 343.3, "height": 61, "style": { "font-family": [ "IPAMincho" ], "font-size": 36, "color": "#000000", "text-align": "center", "vertical-align": "middle", "line-height": "", "line-height-ratio": "", "letter-spacing": null, "font-style": [ "bold" ], "overflow": "truncate", "word-wrap": "break-word" }, "reference-id": "", "value": "", "multiple-line": true, "format": { "base": "", "type": "" } }, { "id": "eudc_italic", "type": "text-block", "display": true, "description": "", "x": 249.2, "y": 329.1, "width": 343.3, "height": 61, "style": { "font-family": [ "IPAMincho" ], "font-size": 36, "color": "#000000", "text-align": "center", "vertical-align": "middle", "line-height": "", "line-height-ratio": "", "letter-spacing": null, "font-style": [ "italic" ], "overflow": "truncate", "word-wrap": "break-word" }, "reference-id": "", "value": "", "multiple-line": true, "format": { "base": "", "type": "" } }, { "id": "eudc_bold_italic", "type": "text-block", "display": true, "description": "", "x": 249.2, "y": 409, "width": 343.3, "height": 61, "style": { "font-family": [ "IPAMincho" ], "font-size": 36, "color": "#000000", "text-align": "center", "vertical-align": "middle", "line-height": "", "line-height-ratio": "", "letter-spacing": null, "font-style": [ "bold", "italic" ], "overflow": "truncate", "word-wrap": "break-word" }, "reference-id": "", "value": "", "multiple-line": true, "format": { "base": "", "type": "" } } ], "state": { "layout-guides": [] }, "title": "EUDC", "report": { "paper-type": "A4", "orientation": "landscape", "margin": [ 20, 20, 20, 20 ] } } ================================================ FILE: test/basic_report/features/eudc/test_feature.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::TestEudcFeature < Thinreports::FeatureTest[__dir__] feature do Thinreports.configure do |config| config.fallback_fonts = path_of('eudc.ttf') end report = Thinreports::BasicReport::Report.new layout: template_path report.start_new_page do |page| page.item(:eudc).value('日本で生まれ世界が育てた言語 Ruby') page.values( eudc_bold: '太字', eudc_italic: '斜体', eudc_bold_italic: '太字斜体' ) end assert_pdf report.generate end def teardown Thinreports.config.fallback_fonts = [] end end ================================================ FILE: test/basic_report/features/graphics/template.tlf ================================================ { "version": "0.10.0", "items": [ { "id": "", "type": "rect", "display": true, "description": "", "x": 20, "y": 59, "width": 120.1, "height": 62, "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid", "fill-color": "#FFFFFF" }, "border-radius": 0 }, { "id": "", "type": "rect", "display": true, "description": "", "x": 155, "y": 59, "width": 120.1, "height": 62, "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid", "fill-color": "#FFFFFF" }, "border-radius": 10 }, { "id": "", "type": "text", "display": true, "description": "", "x": 20, "y": 20, "width": 103.1, "height": 21, "style": { "font-family": [ "Helvetica" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "Rect (static)" ] }, { "id": "", "type": "rect", "display": true, "description": "", "x": 290, "y": 59, "width": 120.1, "height": 62, "style": { "border-color": "#000000", "border-width": 1, "border-style": "dashed", "fill-color": "#FFFFFF" }, "border-radius": 10 }, { "id": "", "type": "rect", "display": true, "description": "", "x": 425, "y": 59, "width": 120.1, "height": 62, "style": { "border-color": "#000000", "border-width": 1, "border-style": "dotted", "fill-color": "#FFFFFF" }, "border-radius": 10 }, { "id": "", "type": "rect", "display": true, "description": "", "x": 20, "y": 140, "width": 120.1, "height": 62, "style": { "border-color": "#ff0000", "border-width": 1, "border-style": "solid", "fill-color": "#FFFFFF" }, "border-radius": 0 }, { "id": "", "type": "rect", "display": true, "description": "", "x": 155, "y": 140, "width": 120.1, "height": 62, "style": { "border-color": "none", "border-width": 1, "border-style": "solid", "fill-color": "#ff0000" }, "border-radius": 0 }, { "id": "", "type": "rect", "display": true, "description": "", "x": 250, "y": 140, "width": 97.1, "height": 43, "style": { "border-color": "#0070c0", "border-width": 1, "border-style": "solid", "fill-color": "none" }, "border-radius": 0 }, { "id": "rect1", "type": "rect", "display": true, "description": "", "x": 20, "y": 265, "width": 120.1, "height": 62, "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid", "fill-color": "#FFFFFF" }, "border-radius": 0 }, { "id": "rect2", "type": "rect", "display": true, "description": "", "x": 155, "y": 265, "width": 120.1, "height": 62, "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid", "fill-color": "#FFFFFF" }, "border-radius": 10 }, { "id": "rect3", "type": "rect", "display": true, "description": "", "x": 290, "y": 265, "width": 120.1, "height": 62, "style": { "border-color": "#000000", "border-width": 1, "border-style": "dashed", "fill-color": "#FFFFFF" }, "border-radius": 10 }, { "id": "rect4", "type": "rect", "display": true, "description": "", "x": 425, "y": 265, "width": 120.1, "height": 62, "style": { "border-color": "#000000", "border-width": 1, "border-style": "dotted", "fill-color": "#FFFFFF" }, "border-radius": 10 }, { "id": "rect5", "type": "rect", "display": true, "description": "", "x": 20, "y": 346, "width": 120.1, "height": 62, "style": { "border-color": "#ff0000", "border-width": 1, "border-style": "solid", "fill-color": "#FFFFFF" }, "border-radius": 0 }, { "id": "rect6", "type": "rect", "display": true, "description": "", "x": 155, "y": 346, "width": 120.1, "height": 62, "style": { "border-color": "none", "border-width": 1, "border-style": "solid", "fill-color": "#ff0000" }, "border-radius": 0 }, { "id": "rect7", "type": "rect", "display": true, "description": "", "x": 250, "y": 346, "width": 97.1, "height": 43, "style": { "border-color": "#0070c0", "border-width": 1, "border-style": "solid", "fill-color": "none" }, "border-radius": 0 }, { "id": "", "type": "text", "display": true, "description": "", "x": 20, "y": 225, "width": 125.1, "height": 21, "style": { "font-family": [ "Helvetica" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "Rect (dynamic)" ] } ], "state": { "layout-guides": [] }, "title": "", "report": { "paper-type": "A4", "orientation": "portrait", "margin": [ 20, 20, 20, 20 ] } } ================================================ FILE: test/basic_report/features/graphics/test_feature.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::TestGraphicsFeature < Thinreports::FeatureTest[__dir__] feature do report = Thinreports::BasicReport::Report.new layout: template_path report.start_new_page assert_pdf report.generate end end ================================================ FILE: test/basic_report/features/hidden_item/template.tlf ================================================ { "version": "0.9.0", "items": [ { "id": "", "type": "rect", "display": true, "description": "", "x": 69, "y": 75, "width": 172.1, "height": 84, "style": { "border-color": "none", "border-width": 1, "border-style": "solid", "fill-color": "#ff0000" }, "border-radius": 0 }, { "id": "", "type": "text", "display": true, "description": "", "x": 20, "y": 19, "width": 279.1, "height": 40, "style": { "font-family": [ "Times New Roman" ], "font-size": 36, "color": "#000000", "text-align": "center", "vertical-align": "middle", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "Visible" ] }, { "id": "", "type": "line", "display": true, "description": "", "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid" }, "x1": 297.6, "y1": 20, "x2": 297.6, "y2": 821.8 }, { "id": "", "type": "rect", "display": false, "description": "", "x": 349, "y": 74, "width": 172.1, "height": 84, "style": { "border-color": "none", "border-width": 1, "border-style": "solid", "fill-color": "#ff0000" }, "border-radius": 0 }, { "id": "", "type": "text", "display": true, "description": "", "x": 297, "y": 18, "width": 279.1, "height": 40, "style": { "font-family": [ "Times New Roman" ], "font-size": 36, "color": "#000000", "text-align": "center", "vertical-align": "middle", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "Hidden" ] }, { "id": "", "type": "ellipse", "display": true, "description": "", "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid", "fill-color": "#FFFFFF" }, "cx": 205.55, "cy": 161.55, "rx": 57.55, "ry": 50.55 }, { "id": "", "type": "ellipse", "display": false, "description": "", "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid", "fill-color": "#FFFFFF" }, "cx": 485.55, "cy": 161.55, "rx": 57.55, "ry": 50.55 }, { "id": "", "type": "image", "display": true, "description": "", "x": 96.1, "y": 177, "width": 123.6, "height": 142, "data": { "mime-type": "image/jpeg", "base64": "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" } }, { "id": "", "type": "image", "display": false, "description": "", "x": 376, "y": 177, "width": 123.6, "height": 142, "data": { "mime-type": "image/jpeg", "base64": "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" } }, { "id": "", "type": "text", "display": true, "description": "", "x": 64, "y": 333.1, "width": 200, "height": 40, "style": { "font-family": [ "Times New Roman" ], "font-size": 36, "color": "#c00000", "text-align": "center", "vertical-align": "middle", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "Ruby" ] }, { "id": "", "type": "text", "display": false, "description": "", "x": 339, "y": 333.1, "width": 200, "height": 40, "style": { "font-family": [ "Times New Roman" ], "font-size": 36, "color": "#c00000", "text-align": "center", "vertical-align": "middle", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "Ruby" ] }, { "id": "v_thinreports", "type": "text-block", "display": true, "description": "", "x": 45, "y": 376.1, "width": 236.1, "height": 19, "style": { "font-family": [ "Times New Roman" ], "font-size": 18, "color": "#000000", "text-align": "center", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "break-word" }, "reference-id": "", "value": "Thinreports", "multiple-line": false, "format": { "base": "", "type": "" } }, { "id": "h_thinreports", "type": "text-block", "display": false, "description": "", "x": 320, "y": 376.1, "width": 236.1, "height": 19, "style": { "font-family": [ "Times New Roman" ], "font-size": 18, "color": "#000000", "text-align": "center", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "break-word" }, "reference-id": "", "value": "Thinreports", "multiple-line": false, "format": { "base": "", "type": "" } }, { "id": "List", "type": "list", "display": true, "description": "", "x": 20, "y": 430.1, "width": 555.2, "height": 306.1, "header": { "enabled": true, "height": 51.1, "translate": { "x": 0, "y": 0 }, "items": [ { "id": "", "type": "text", "display": true, "description": "", "x": 63, "y": 443, "width": 200, "height": 24.3, "style": { "font-family": [ "IPAMincho" ], "font-size": 24, "color": "#000000", "text-align": "center", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "表示" ] }, { "id": "", "type": "text", "display": false, "description": "", "x": 338, "y": 443, "width": 200, "height": 24.3, "style": { "font-family": [ "IPAMincho" ], "font-size": 24, "color": "#000000", "text-align": "center", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "非表示" ] } ] }, "detail": { "height": 46, "translate": { "x": 0, "y": -22.5 }, "items": [ { "id": "", "type": "text", "display": true, "description": "", "x": 63, "y": 516.5, "width": 200, "height": 24.3, "style": { "font-family": [ "IPAMincho" ], "font-size": 24, "color": "#000000", "text-align": "center", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "表示" ] }, { "id": "", "type": "text", "display": false, "description": "", "x": 338, "y": 516.5, "width": 200, "height": 24.3, "style": { "font-family": [ "IPAMincho" ], "font-size": 24, "color": "#000000", "text-align": "center", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "非表示" ] } ] }, "page-footer": { "enabled": false, "height": 0, "translate": { "x": 0, "y": -50.1 }, "items": [] }, "footer": { "enabled": false, "height": 0, "translate": { "x": 0, "y": -105.3 }, "items": [] }, "content-height": 255.00000000000003, "auto-page-break": true } ], "state": { "layout-guides": [] }, "title": "", "report": { "paper-type": "A4", "orientation": "portrait", "margin": [ 20, 20, 20, 20 ] } } ================================================ FILE: test/basic_report/features/hidden_item/test_feature.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::TestHiddenItemFeature < Thinreports::FeatureTest[__dir__] feature do report = Thinreports::BasicReport::Report.new layout: template_path 2.times { report.list(:List).add_row } assert_pdf report.generate end end ================================================ FILE: test/basic_report/features/image_block/template.tlf ================================================ { "version": "0.9.0", "items": [ { "id": "", "type": "rect", "display": true, "description": "", "x": 20, "y": 51, "width": 100, "height": 100, "style": { "border-color": "#ff0000", "border-width": 1, "border-style": "solid", "fill-color": "none" }, "border-radius": 0 }, { "id": "", "type": "rect", "display": true, "description": "", "x": 20, "y": 443, "width": 150, "height": 100, "style": { "border-color": "#ff0000", "border-width": 1, "border-style": "solid", "fill-color": "none" }, "border-radius": 0 }, { "id": "", "type": "text", "display": true, "description": "", "x": 20, "y": 20, "width": 200, "height": 26.7, "style": { "font-family": [ "Times New Roman" ], "font-size": 24, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "Positioning" ] }, { "id": "", "type": "rect", "display": true, "description": "", "x": 130, "y": 51, "width": 100, "height": 100, "style": { "border-color": "#ff0000", "border-width": 1, "border-style": "solid", "fill-color": "none" }, "border-radius": 0 }, { "id": "", "type": "rect", "display": true, "description": "", "x": 240, "y": 51, "width": 100, "height": 100, "style": { "border-color": "#ff0000", "border-width": 1, "border-style": "solid", "fill-color": "none" }, "border-radius": 0 }, { "id": "", "type": "text", "display": true, "description": "", "x": 20, "y": 385, "width": 200, "height": 26.7, "style": { "font-family": [ "Times New Roman" ], "font-size": 24, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "Overflowing" ] }, { "id": "", "type": "text", "display": true, "description": "", "x": 25, "y": 411, "width": 288, "height": 26, "style": { "font-family": [ "Helvetica" ], "font-size": 14, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "Resize automatically to 150x75 from 200x100." ] }, { "id": "", "type": "text", "display": true, "description": "", "x": 20, "y": 565, "width": 200, "height": 26.7, "style": { "font-family": [ "Times New Roman" ], "font-size": 24, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "Open URI" ] }, { "id": "", "type": "rect", "display": true, "description": "", "x": 20, "y": 628, "width": 555.2, "height": 92.3, "style": { "border-color": "#ff0000", "border-width": 1, "border-style": "solid", "fill-color": "none" }, "border-radius": 0 }, { "id": "", "type": "text", "display": true, "description": "", "x": 25, "y": 595, "width": 369.4, "height": 26, "style": { "font-family": [ "Helvetica" ], "font-size": 14, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "http://www.thinreports.org/assets/logos/thinreports-logo.png" ] }, { "id": "", "type": "rect", "display": true, "description": "", "x": 20, "y": 443, "width": 150, "height": 76, "style": { "border-color": "#f79646", "border-width": 1, "border-style": "dashed", "fill-color": "none" }, "border-radius": 0 }, { "id": "list", "type": "list", "display": true, "description": "", "x": 361.1, "y": 55, "width": 199.1, "height": 312.1, "header": { "enabled": true, "height": 33.2, "translate": { "x": 10, "y": -395.2 }, "items": [ { "id": "", "type": "text", "display": true, "description": "", "x": 351.1, "y": 450.2, "width": 199.1, "height": 33.2, "style": { "font-family": [ "Times New Roman" ], "font-size": 18, "color": "#000000", "text-align": "center", "vertical-align": "middle", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "In List" ] }, { "id": "", "type": "line", "display": true, "description": "", "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid" }, "x1": 353.1, "y1": 483.4, "x2": 560.2, "y2": 483.4 } ] }, "detail": { "height": 60, "translate": { "x": 10, "y": -395.2 }, "items": [ { "id": "", "type": "line", "display": true, "description": "", "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid" }, "x1": 351.1, "y1": 543.2, "x2": 560.2, "y2": 543.2 }, { "id": "in_list", "type": "image-block", "display": true, "description": "", "x": 351.1, "y": 483.4, "width": 198.1, "height": 58.8, "style": { "position-x": "center", "position-y": "middle" } } ] }, "page-footer": { "enabled": false, "height": 0, "translate": { "x": 10, "y": -368.4 }, "items": [] }, "footer": { "enabled": false, "height": 0, "translate": { "x": 10, "y": -393.3 }, "items": [] }, "content-height": 278.90000000000003, "auto-page-break": false }, { "id": "", "type": "text", "display": true, "description": "", "x": 359, "y": 20, "width": 200, "height": 26.7, "style": { "font-family": [ "Times New Roman" ], "font-size": 24, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "Use in List" ] }, { "id": "pos_top_left", "type": "image-block", "display": true, "description": "", "x": 20, "y": 51, "width": 100, "height": 100, "style": { "position-x": "left", "position-y": "top" } }, { "id": "pos_top_center", "type": "image-block", "display": true, "description": "", "x": 130, "y": 51, "width": 100, "height": 100, "style": { "position-x": "center", "position-y": "top" } }, { "id": "pos_top_right", "type": "image-block", "display": true, "description": "", "x": 240, "y": 51, "width": 100, "height": 100, "style": { "position-x": "right", "position-y": "top" } }, { "id": "pos_center_left", "type": "image-block", "display": true, "description": "", "x": 20, "y": 161, "width": 100, "height": 100, "style": { "position-x": "left", "position-y": "middle" } }, { "id": "", "type": "rect", "display": true, "description": "", "x": 20, "y": 161, "width": 100, "height": 100, "style": { "border-color": "#ff0000", "border-width": 1, "border-style": "solid", "fill-color": "none" }, "border-radius": 0 }, { "id": "pos_center_center", "type": "image-block", "display": true, "description": "", "x": 130, "y": 161, "width": 100, "height": 100, "style": { "position-x": "center", "position-y": "middle" } }, { "id": "", "type": "rect", "display": true, "description": "", "x": 130, "y": 161, "width": 100, "height": 100, "style": { "border-color": "#ff0000", "border-width": 1, "border-style": "solid", "fill-color": "none" }, "border-radius": 0 }, { "id": "pos_center_right", "type": "image-block", "display": true, "description": "", "x": 240, "y": 161, "width": 100, "height": 100, "style": { "position-x": "right", "position-y": "middle" } }, { "id": "", "type": "rect", "display": true, "description": "", "x": 240, "y": 161, "width": 100, "height": 100, "style": { "border-color": "#ff0000", "border-width": 1, "border-style": "solid", "fill-color": "none" }, "border-radius": 0 }, { "id": "pos_bottom_left", "type": "image-block", "display": true, "description": "", "x": 20, "y": 271, "width": 100, "height": 100, "style": { "position-x": "left", "position-y": "bottom" } }, { "id": "", "type": "rect", "display": true, "description": "", "x": 20, "y": 271, "width": 100, "height": 100, "style": { "border-color": "#ff0000", "border-width": 1, "border-style": "solid", "fill-color": "none" }, "border-radius": 0 }, { "id": "pos_bottom_center", "type": "image-block", "display": true, "description": "", "x": 130, "y": 271, "width": 100, "height": 100, "style": { "position-x": "center", "position-y": "bottom" } }, { "id": "", "type": "rect", "display": true, "description": "", "x": 130, "y": 271, "width": 100, "height": 100, "style": { "border-color": "#ff0000", "border-width": 1, "border-style": "solid", "fill-color": "none" }, "border-radius": 0 }, { "id": "pos_bottom_right", "type": "image-block", "display": true, "description": "", "x": 240, "y": 271, "width": 100, "height": 100, "style": { "position-x": "right", "position-y": "bottom" } }, { "id": "", "type": "rect", "display": true, "description": "", "x": 240, "y": 271, "width": 100, "height": 100, "style": { "border-color": "#ff0000", "border-width": 1, "border-style": "solid", "fill-color": "none" }, "border-radius": 0 }, { "id": "overflow", "type": "image-block", "display": true, "description": "", "x": 20, "y": 443, "width": 150, "height": 100, "style": { "position-x": "left", "position-y": "top" } }, { "id": "thinreports_logo", "type": "image-block", "display": true, "description": "", "x": 20, "y": 628, "width": 555.2, "height": 92.3, "style": { "position-x": "center", "position-y": "middle" } } ], "state": { "layout-guides": [ { "type": "x", "position": 340.1 }, { "type": "y", "position": 371.1 } ] }, "title": "", "report": { "paper-type": "A4", "orientation": "portrait", "margin": [ 20, 20, 20, 20 ] } } ================================================ FILE: test/basic_report/features/image_block/test_feature.rb ================================================ # frozen_string_literal: true require 'test_helper' require 'open-uri' require 'openssl' class Thinreports::BasicReport::TestImageBlockFeature < Thinreports::FeatureTest[__dir__] feature do image50x50 = path_of('img50x50.png') image200x100 = path_of('img200x100.png') report = Thinreports::BasicReport::Report.new layout: template_path report.start_new_page report.page.values( pos_top_left: image50x50, pos_top_center: image50x50, pos_top_right: image50x50, pos_center_left: image50x50, pos_center_center: image50x50, pos_center_right: image50x50, pos_bottom_left: image50x50, pos_bottom_center: image50x50, pos_bottom_right: image50x50 ) report.page.item(:overflow).src = image200x100 report.page[:thinreports_logo] = StringIO.new(dir.join('thinreports-logo.png').binread) report.page.list(:list) do |list| 3.times { list.add_row in_list: image50x50 } end assert_pdf report.generate end end ================================================ FILE: test/basic_report/features/list_events/template.tlf ================================================ { "version": "0.9.0", "items": [ { "id": "default", "type": "list", "display": true, "description": "", "x": 20, "y": 103, "width": 555.2, "height": 300, "header": { "enabled": true, "height": 50, "translate": { "x": 0, "y": -61.1 }, "items": [ { "id": "", "type": "line", "display": true, "description": "", "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid" }, "x1": 20, "y1": 214.1, "x2": 575.2, "y2": 214.1 }, { "id": "title", "type": "text-block", "display": true, "description": "", "x": 169.2, "y": 180.1, "width": 249, "height": 18, "style": { "font-family": [ "Helvetica" ], "font-size": 18, "color": "#000000", "text-align": "center", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "break-word" }, "reference-id": "", "value": "", "multiple-line": false, "format": { "base": "", "type": "" } } ] }, "detail": { "height": 50, "translate": { "x": 0, "y": -86.5 }, "items": [ { "id": "price", "type": "text-block", "display": true, "description": "", "x": 169.2, "y": 255.5, "width": 249, "height": 18, "style": { "font-family": [ "Helvetica" ], "font-size": 18, "color": "#000000", "text-align": "right", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "break-word" }, "reference-id": "", "value": "", "multiple-line": false, "format": { "base": "", "type": "number", "number": { "delimiter": ",", "precision": 0 } } }, { "id": "", "type": "line", "display": true, "description": "", "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid" }, "x1": 20, "y1": 289.5, "x2": 575.2, "y2": 289.5 } ] }, "page-footer": { "enabled": true, "height": 50, "translate": { "x": 0, "y": -111.9 }, "items": [ { "id": "price", "type": "text-block", "display": true, "description": "", "x": 169.2, "y": 330.9, "width": 249, "height": 18, "style": { "font-family": [ "Helvetica" ], "font-size": 18, "color": "#000000", "text-align": "right", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "break-word" }, "reference-id": "", "value": "", "multiple-line": false, "format": { "base": "", "type": "number", "number": { "delimiter": ",", "precision": 0 } } }, { "id": "", "type": "line", "display": true, "description": "", "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid" }, "x1": 20, "y1": 364.9, "x2": 575.2, "y2": 364.9 }, { "id": "", "type": "text", "display": true, "description": "", "x": 74.1, "y": 332.9, "width": 77.4, "height": 18.5, "style": { "font-family": [ "Helvetica" ], "font-size": 16, "color": "#000000", "text-align": "right", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "Page Total" ] } ] }, "footer": { "enabled": true, "height": 50, "translate": { "x": 0, "y": -118.5 }, "items": [ { "id": "", "type": "line", "display": true, "description": "", "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid" }, "x1": 20, "y1": 421.5, "x2": 575.2, "y2": 421.5 }, { "id": "price", "type": "text-block", "display": true, "description": "", "x": 169.2, "y": 387.5, "width": 249, "height": 18, "style": { "font-family": [ "Helvetica" ], "font-size": 18, "color": "#000000", "text-align": "right", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "break-word" }, "reference-id": "", "value": "", "multiple-line": false, "format": { "base": "", "type": "number", "number": { "delimiter": ",", "precision": 0 } } }, { "id": "", "type": "text", "display": true, "description": "", "x": 74.1, "y": 389.5, "width": 77.4, "height": 18.5, "style": { "font-family": [ "Helvetica" ], "font-size": 16, "color": "#000000", "text-align": "right", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "Total" ] } ] }, "content-height": 250, "auto-page-break": true }, { "id": "", "type": "text", "display": true, "description": "", "x": 140.1, "y": 31, "width": 315.1, "height": 48, "style": { "font-family": [ "Times New Roman" ], "font-size": 26, "color": "#000000", "text-align": "center", "vertical-align": "middle", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "List Events for 0.8 or higher" ] } ], "state": { "layout-guides": [ { "type": "x", "position": 418.2 }, { "type": "x", "position": 169.2 }, { "type": "y", "position": 84 } ] }, "title": "List Events", "report": { "paper-type": "A4", "orientation": "portrait", "margin": [ 20, 20, 20, 20 ] } } ================================================ FILE: test/basic_report/features/list_events/test_feature.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::TestListEventsFeature < Thinreports::FeatureTest[__dir__] feature do report = Thinreports::BasicReport::Report.new layout: template_path report.list.header title: 'Prices' report.list do |list| price_for = { page: 0, all: 0 } list.on_page_finalize do price_for[:all] += price_for[:page] price_for[:page] = 0 end list.on_page_footer_insert do |footer| footer.item(:price).value(price_for[:page]) end list.on_footer_insert do |footer| footer.item(:price).value(price_for[:all]) end [100, 200, 250, 50, 100, 20, 30, 50, 100, 100].each do |price| list.add_row price: price # Calculate sum price for each page of list price_for[:page] += price end end assert_pdf report.generate end end ================================================ FILE: test/basic_report/features/list_manually/template.tlf ================================================ { "version": "0.9.0", "items": [ { "id": "default", "type": "list", "display": true, "description": "", "x": 20, "y": 22, "width": 555.2, "height": 320.1, "header": { "enabled": true, "height": 46.3, "translate": { "x": 0, "y": -35 }, "items": [ { "id": "header", "type": "text-block", "display": true, "description": "", "x": 160.6, "y": 62, "width": 274.1, "height": 36, "style": { "font-family": [ "Helvetica" ], "font-size": 36, "color": "#000000", "text-align": "center", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "break-word" }, "reference-id": "", "value": "", "multiple-line": false, "format": { "base": "Header#{value}", "type": "" } } ] }, "detail": { "height": 57.7, "translate": { "x": 0, "y": -45.3 }, "items": [ { "id": "detail", "type": "text-block", "display": true, "description": "", "x": 211.4, "y": 124.5, "width": 172.5, "height": 36, "style": { "font-family": [ "Helvetica" ], "font-size": 36, "color": "#000000", "text-align": "center", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "break-word" }, "reference-id": "", "value": "", "multiple-line": false, "format": { "base": "Detail#{value}", "type": "" } } ] }, "page-footer": { "enabled": false, "height": 0, "translate": { "x": 0, "y": -44.2 }, "items": [] }, "footer": { "enabled": false, "height": 0, "translate": { "x": 0, "y": -86.7 }, "items": [] }, "content-height": 273.8, "auto-page-break": false } ], "state": { "layout-guides": [] }, "title": "List Manual Generation", "report": { "paper-type": "A4", "orientation": "portrait", "margin": [ 20, 20, 20, 20 ] } } ================================================ FILE: test/basic_report/features/list_manually/test_feature.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::TestListManuallyFeature < Thinreports::FeatureTest[__dir__] feature do report = Thinreports::BasicReport::Report.new layout: template_path report.list.header do |h| h.item(:header).value(report.page.no) end 25.times do |row_index| if report.list.overflow? report.start_new_page report.list.header header: report.page.no end report.list.page_break if row_index == 15 report.list.add_row detail: row_index end assert_pdf report.generate end end ================================================ FILE: test/basic_report/features/page_number/template.tlf ================================================ { "version": "0.9.0", "items": [ { "id": "pageno", "type": "page-number", "display": true, "description": "", "x": 50, "y": 205, "width": 139.1, "height": 17, "style": { "font-family": [ "Helvetica" ], "font-size": 18, "color": "#000000", "text-align": "center", "letter-spacing": "", "font-style": [], "overflow": "truncate" }, "format": "{page} / {total}", "target": "" }, { "id": "", "type": "text", "display": true, "description": "", "x": 20, "y": 46, "width": 176.1, "height": 28, "style": { "font-family": [ "Helvetica" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "Basis" ] }, { "id": "", "type": "text", "display": true, "description": "", "x": 195, "y": 114, "width": 199.1, "height": 17, "style": { "font-family": [ "Helvetica" ], "font-size": 14, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "(Center Alignment)" ] }, { "id": "", "type": "text", "display": true, "description": "", "x": 195, "y": 143, "width": 199.1, "height": 17, "style": { "font-family": [ "Helvetica" ], "font-size": 14, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "(Right Alignment)" ] }, { "id": "", "type": "text", "display": true, "description": "", "x": 20, "y": 180, "width": 176.1, "height": 28, "style": { "font-family": [ "Helvetica" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "With ID" ] }, { "id": "", "type": "page-number", "display": true, "description": "", "x": 50, "y": 113, "width": 130.1, "height": 17, "style": { "font-family": [ "Helvetica" ], "font-size": 18, "color": "#000000", "text-align": "center", "letter-spacing": "", "font-style": [], "overflow": "truncate" }, "format": "{page}", "target": "" }, { "id": "", "type": "page-number", "display": true, "description": "", "x": 50, "y": 84, "width": 130.1, "height": 18.9, "style": { "font-family": [ "Helvetica" ], "font-size": 18, "color": "#000000", "text-align": "left", "letter-spacing": 10, "font-style": [], "overflow": "truncate" }, "format": "{page}", "target": "" }, { "id": "", "type": "page-number", "display": true, "description": "", "x": 50, "y": 140, "width": 130.1, "height": 17, "style": { "font-family": [ "Helvetica" ], "font-size": 18, "color": "#000000", "text-align": "right", "letter-spacing": "", "font-style": [], "overflow": "truncate" }, "format": "{page}", "target": "" } ], "state": { "layout-guides": [] }, "title": "", "report": { "paper-type": "A4", "orientation": "portrait", "margin": [ 20, 20, 20, 20 ] } } ================================================ FILE: test/basic_report/features/page_number/test_feature.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::TestPageNumberFeature < Thinreports::FeatureTest[__dir__] feature do report = Thinreports::BasicReport::Report.new layout: template_path report.start_new_page do |page| page.item(:pageno).hide end report.start_new_page do |page| page.item(:pageno).styles(color: 'red', bold: true, underline: true, linethrough: true) end report.start_new_page count: false report.start_new_page count: true assert_pdf report.generate end end ================================================ FILE: test/basic_report/features/page_number_with_list/template.tlf ================================================ { "version": "0.9.0", "items": [ { "id": "default", "type": "list", "display": true, "description": "", "x": 20, "y": 109, "width": 555.2, "height": 390.2, "header": { "enabled": true, "height": 58.1, "translate": { "x": 0, "y": 0 }, "items": [ { "id": "", "type": "line", "display": true, "description": "", "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid" }, "x1": 20, "y1": 167.1, "x2": 575.2, "y2": 167.1 }, { "id": "", "type": "text", "display": true, "description": "", "x": 197.6, "y": 125.6, "width": 200, "height": 25.1, "style": { "font-family": [ "Helvetica" ], "font-size": 18, "color": "#000000", "text-align": "center", "vertical-align": "middle", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "Header" ] } ] }, "detail": { "height": 52.9, "translate": { "x": 0, "y": -19.9 }, "items": [ { "id": "", "type": "line", "display": true, "description": "", "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid" }, "x1": 20, "y1": 239.9, "x2": 575.2, "y2": 239.9 }, { "id": "", "type": "text", "display": true, "description": "", "x": 197.6, "y": 203.6, "width": 200, "height": 25.1, "style": { "font-family": [ "Helvetica" ], "font-size": 18, "color": "#000000", "text-align": "center", "vertical-align": "middle", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "Detail" ] } ] }, "page-footer": { "enabled": false, "height": 0, "translate": { "x": 0, "y": -45 }, "items": [] }, "footer": { "enabled": false, "height": 0, "translate": { "x": 0, "y": -103.5 }, "items": [] }, "content-height": 332.09999999999997, "auto-page-break": true }, { "id": "", "type": "page-number", "display": true, "description": "", "x": 273.1, "y": 756.3, "width": 186, "height": 17, "style": { "font-family": [ "Helvetica" ], "font-size": 18, "color": "#000000", "text-align": "left", "letter-spacing": "", "font-style": [], "overflow": "truncate" }, "format": "{page} / {total}", "target": "" }, { "id": "", "type": "page-number", "display": true, "description": "", "x": 434.1, "y": 69, "width": 141.1, "height": 17, "style": { "font-family": [ "Helvetica" ], "font-size": 18, "color": "#000000", "text-align": "right", "letter-spacing": "", "font-style": [], "overflow": "truncate" }, "format": "{page} / {total}", "target": "default" }, { "id": "", "type": "text", "display": true, "description": "", "x": 115, "y": 755, "width": 150.1, "height": 18.5, "style": { "font-family": [ "Helvetica" ], "font-size": 16, "color": "#000000", "text-align": "right", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "Report PageNo:" ] }, { "id": "group_no", "type": "text-block", "display": true, "description": "", "x": 20, "y": 69, "width": 142.1, "height": 17, "style": { "font-family": [ "Helvetica" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "break-word" }, "reference-id": "", "value": "", "multiple-line": false, "format": { "base": "", "type": "" } } ], "state": { "layout-guides": [] }, "title": "", "report": { "paper-type": "A4", "orientation": "portrait", "margin": [ 20, 20, 20, 20 ] } } ================================================ FILE: test/basic_report/features/page_number_with_list/test_feature.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::TestPageNumberWithListFeature < Thinreports::FeatureTest[__dir__] feature do report = Thinreports::BasicReport::Report.new layout: template_path report.start_new_page report.page.item(:group_no).value('Group A') 10.times { report.list.add_row } report.start_new_page report.page.item(:group_no).value('Group B') 20.times { report.list.add_row } assert_pdf report.generate end end ================================================ FILE: test/basic_report/features/palleted_png/template.tlf ================================================ { "version": "0.9.0", "items": [ { "id": "", "type": "image", "display": true, "description": "", "x": 20, "y": 20, "width": 200, "height": 200, "data": { "mime-type": "image/png", "base64": "iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAMAAACahl6sAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAD1BMVEX/AAD/AAD/AAD/AAD////Icf73AAAAA3RSTlO/fz8qtTX3AAAAAWJLR0QEj2jZUQAAAAlwSFlzAAALEwAACxMBAJqcGAAAAMtJREFUeNrtzwERgEAAw7AH/HtGBr2RKGjPM+J8HWDESJyRGiM1RmqM1BipMVJjpMZIjZEaIzVGaozUGKkxUmOkxkiNkRojNUZqjNQYqTFSszMCAAAAAAAAAAAA/3SNMFJjpMZIjZEaIzVGaozUGKkxUmOkxkiNkRojNUZqjNQYqTFSY6TGSI2RGiM1RmqM1Bip2Rm5RxipMVJjpMZIjZEaIzVGaozUGKkxUmOkxkiNkRojNUZqjNQYqTFSY6TGSI2RGiM1RmqM1MyMvMkE6mEsuD8jAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE0LTEwLTA5VDEwOjM1OjI1KzAyOjAwxhRVNQAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxNC0xMC0wOVQxMDozNToyNSswMjowMLdJ7YkAAAAASUVORK5CYII=" } }, { "id": "image", "type": "image-block", "display": true, "description": "", "x": 278.2, "y": 20, "width": 266.7, "height": 223.7, "style": { "position-x": "left", "position-y": "top" } } ], "state": { "layout-guides": [] }, "title": "", "report": { "paper-type": "A4", "orientation": "portrait", "margin": [ 20, 20, 20, 20 ] } } ================================================ FILE: test/basic_report/features/palleted_png/test_feature.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::TestPalletedPngFeature < Thinreports::FeatureTest[__dir__] feature do report = Thinreports::BasicReport::Report.new layout: template_path report.start_new_page do |page| page.item(:image).src = path_of('palleted_png.png') end assert_pdf report.generate end end ================================================ FILE: test/basic_report/features/report_callbacks/template.tlf ================================================ { "version": "0.9.0", "items": [ { "id": "text1", "type": "text-block", "display": true, "description": "", "x": 20, "y": 45, "width": 357.2, "height": 18, "style": { "font-family": [ "Helvetica" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "break-word" }, "reference-id": "", "value": "", "multiple-line": false, "format": { "base": "", "type": "" } }, { "id": "text2", "type": "text-block", "display": true, "description": "", "x": 20, "y": 75, "width": 357.2, "height": 18, "style": { "font-family": [ "Helvetica" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "none" }, "reference-id": "", "value": "", "multiple-line": false, "format": { "base": "", "type": "" } }, { "id": "text3", "type": "text-block", "display": true, "description": "", "x": 20, "y": 105, "width": 357.2, "height": 18, "style": { "font-family": [ "Helvetica" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "none" }, "reference-id": "", "value": "", "multiple-line": false, "format": { "base": "", "type": "" } }, { "id": "text4", "type": "text-block", "display": true, "description": "", "x": 20, "y": 135, "width": 357.2, "height": 18, "style": { "font-family": [ "Helvetica" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "none" }, "reference-id": "", "value": "", "multiple-line": false, "format": { "base": "", "type": "" } } ], "state": { "layout-guides": [] }, "title": "", "report": { "paper-type": "A4", "orientation": "portrait", "margin": [ 20, 20, 20, 20 ] } } ================================================ FILE: test/basic_report/features/report_callbacks/test_feature.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::TestReportCallbacksFeature < Thinreports::FeatureTest[__dir__] feature do report = Thinreports::BasicReport::Report.new layout: template_path report.on_page_create do |page| page.item(:text2).value('Rendered by on_page_create') end report.start_new_page report.start_new_page assert_pdf report.generate end end ================================================ FILE: test/basic_report/features/text_align/template.tlf ================================================ { "version": "0.9.0", "items": [ { "id": "", "type": "text", "display": true, "description": "", "x": 20, "y": 19, "width": 555.2, "height": 84.3, "style": { "font-family": [ "Helvetica" ], "font-size": 36, "color": "#000000", "text-align": "right", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "Align Right", "Helvetica" ] }, { "id": "", "type": "line", "display": true, "description": "", "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid" }, "x1": 297.6, "y1": 20, "x2": 297.6, "y2": 821.8 }, { "id": "", "type": "text", "display": true, "description": "", "x": 20, "y": 105, "width": 555.2, "height": 72, "style": { "font-family": [ "IPAMincho" ], "font-size": 36, "color": "#000000", "text-align": "right", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "右寄せ", "IPA明朝" ] }, { "id": "", "type": "line", "display": true, "description": "", "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid" }, "x1": 297.6, "y1": 20, "x2": 297.6, "y2": 821.8 }, { "id": "", "type": "line", "display": true, "description": "", "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid" }, "x1": 575.2, "y1": 20, "x2": 575.2, "y2": 821.8 }, { "id": "", "type": "text", "display": true, "description": "", "x": 20, "y": 241, "width": 555.2, "height": 84.3, "style": { "font-family": [ "Helvetica" ], "font-size": 36, "color": "#000000", "text-align": "center", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "Align Center", "Helvetica" ] }, { "id": "", "type": "text", "display": true, "description": "", "x": 20, "y": 330, "width": 555.2, "height": 72, "style": { "font-family": [ "IPAMincho" ], "font-size": 36, "color": "#000000", "text-align": "center", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "中央寄せ", "IPA明朝" ] }, { "id": "", "type": "text", "display": true, "description": "", "x": 20, "y": 490, "width": 555.2, "height": 84.3, "style": { "font-family": [ "Helvetica" ], "font-size": 36, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "Align Left", "Helvetica" ] }, { "id": "", "type": "text", "display": true, "description": "", "x": 20, "y": 580, "width": 555.2, "height": 72, "style": { "font-family": [ "IPAMincho" ], "font-size": 36, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "左寄せ", "IPA明朝" ] }, { "id": "", "type": "line", "display": true, "description": "", "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid" }, "x1": 20, "y1": 20, "x2": 20, "y2": 821.8 } ], "state": { "layout-guides": [] }, "title": "Case: Text Align", "report": { "paper-type": "A4", "orientation": "portrait", "margin": [ 20, 20, 20, 20 ] } } ================================================ FILE: test/basic_report/features/text_align/test_feature.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::TestTextAlignFeature < Thinreports::FeatureTest[__dir__] feature do report = Thinreports::BasicReport::Report.new layout: template_path report.start_new_page assert_pdf report.generate end end ================================================ FILE: test/basic_report/features/text_block_formatting/template.tlf ================================================ { "version": "0.10.0", "items": [ { "id": "basic", "type": "text-block", "display": true, "description": "", "x": 20, "y": 110, "width": 205.1, "height": 16.1, "style": { "font-family": [ "Helvetica" ], "font-size": 14, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "none" }, "reference-id": "", "value": "", "multiple-line": false, "format": { "base": "Price: {value}", "type": "" } }, { "id": "date_with_not_a_date", "type": "text-block", "display": true, "description": "", "x": 20, "y": 225, "width": 205.1, "height": 16.1, "style": { "font-family": [ "Helvetica" ], "font-size": 14, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "none" }, "reference-id": "", "value": "", "multiple-line": false, "format": { "base": "", "type": "datetime", "datetime": { "format": "%Y/%m/%d %H:%M:%S" } } }, { "id": "number_with_delimiter", "type": "text-block", "display": true, "description": "", "x": 20, "y": 285, "width": 205.1, "height": 16.1, "style": { "font-family": [ "Helvetica" ], "font-size": 14, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "none" }, "reference-id": "", "value": "", "multiple-line": false, "format": { "base": "", "type": "number", "number": { "delimiter": "," } } }, { "id": "number_with_precision", "type": "text-block", "display": true, "description": "", "x": 20, "y": 310, "width": 205.1, "height": 16.1, "style": { "font-family": [ "Helvetica" ], "font-size": 14, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "none" }, "reference-id": "", "value": "", "multiple-line": false, "format": { "base": "", "type": "number", "number": { "precision": 3 } } }, { "id": "number_with_delimiter_and_precision", "type": "text-block", "display": true, "description": "", "x": 20, "y": 335, "width": 205.1, "height": 16.1, "style": { "font-family": [ "Helvetica" ], "font-size": 14, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "none" }, "reference-id": "", "value": "", "multiple-line": false, "format": { "base": "", "type": "number", "number": { "delimiter": "_", "precision": 5 } } }, { "id": "number_with_invalid_string_number", "type": "text-block", "display": true, "description": "", "x": 20, "y": 385, "width": 205.1, "height": 16.1, "style": { "font-family": [ "Helvetica" ], "font-size": 14, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "none" }, "reference-id": "", "value": "", "multiple-line": false, "format": { "base": "", "type": "number", "number": { "delimiter": "_", "precision": 5 } } }, { "id": "padding_left", "type": "text-block", "display": true, "description": "", "x": 20, "y": 450, "width": 205.1, "height": 16.1, "style": { "font-family": [ "Helvetica" ], "font-size": 14, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "none" }, "reference-id": "", "value": "", "multiple-line": false, "format": { "base": "", "type": "padding", "padding": { "length": 10, "char": "0", "direction": "L" } } }, { "id": "padding_right", "type": "text-block", "display": true, "description": "", "x": 20, "y": 475, "width": 205.1, "height": 16.1, "style": { "font-family": [ "Helvetica" ], "font-size": 14, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "none" }, "reference-id": "", "value": "", "multiple-line": false, "format": { "base": "", "type": "padding", "padding": { "length": 10, "char": "_", "direction": "R" } } }, { "id": "padding_with_blank_value", "type": "text-block", "display": true, "description": "", "x": 20, "y": 550, "width": 205.1, "height": 16.1, "style": { "font-family": [ "Helvetica" ], "font-size": 14, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "none" }, "reference-id": "", "value": "", "multiple-line": false, "format": { "base": "", "type": "padding", "padding": { "length": 10, "char": "0", "direction": "L" } } }, { "id": "date_with_date", "type": "text-block", "display": true, "description": "", "x": 20, "y": 175, "width": 205.1, "height": 16.1, "style": { "font-family": [ "Helvetica" ], "font-size": 14, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "none" }, "reference-id": "", "value": "", "multiple-line": false, "format": { "base": "", "type": "datetime", "datetime": { "format": "%Y/%m/%d" } } }, { "id": "date_with_time", "type": "text-block", "display": true, "description": "", "x": 20, "y": 200, "width": 205.1, "height": 16.1, "style": { "font-family": [ "Helvetica" ], "font-size": 14, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "none" }, "reference-id": "", "value": "", "multiple-line": false, "format": { "base": "", "type": "datetime", "datetime": { "format": "%Y/%m/%d %H:%M:%S" } } }, { "id": "", "type": "text", "display": true, "description": "", "x": 20, "y": 20, "width": 149.1, "height": 20.5, "style": { "font-family": [ "Helvetica" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [ "bold" ] }, "texts": [ "Formatting" ] }, { "id": "", "type": "text", "display": true, "description": "", "x": 20, "y": 85, "width": 149.1, "height": 20.5, "style": { "font-family": [ "Helvetica" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [ "underline" ] }, "texts": [ "Basic Format" ] }, { "id": "", "type": "text", "display": true, "description": "", "x": 20, "y": 150, "width": 149.1, "height": 20.5, "style": { "font-family": [ "Helvetica" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [ "underline" ] }, "texts": [ "Date Format" ] }, { "id": "", "type": "text", "display": true, "description": "", "x": 20, "y": 260, "width": 149.1, "height": 20.5, "style": { "font-family": [ "Helvetica" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [ "underline" ] }, "texts": [ "Number Format" ] }, { "id": "", "type": "text", "display": true, "description": "", "x": 20, "y": 425, "width": 149.1, "height": 20.5, "style": { "font-family": [ "Helvetica" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [ "underline" ] }, "texts": [ "Padding Format" ] }, { "id": "multiline_basic", "type": "text-block", "display": true, "description": "", "x": 320, "y": 110, "width": 205.1, "height": 35, "style": { "font-family": [ "Helvetica" ], "font-size": 14, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "none" }, "reference-id": "", "value": "", "multiple-line": true, "format": { "base": "Address: {value}", "type": "" } }, { "id": "multiline_number", "type": "text-block", "display": true, "description": "", "x": 320, "y": 285, "width": 205.1, "height": 35, "style": { "font-family": [ "Helvetica" ], "font-size": 14, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "none" }, "reference-id": "", "value": "", "multiple-line": true, "format": { "base": "", "type": "number", "number": { "delimiter": ",", "precision": 5 } } }, { "id": "multiline_padding", "type": "text-block", "display": true, "description": "", "x": 320, "y": 450, "width": 205.1, "height": 35, "style": { "font-family": [ "Helvetica" ], "font-size": 14, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "none" }, "reference-id": "", "value": "", "multiple-line": true, "format": { "base": "", "type": "padding", "padding": { "length": 10, "char": "0", "direction": "L" } } }, { "id": "multiline_date", "type": "text-block", "display": true, "description": "", "x": 320, "y": 175, "width": 205.1, "height": 35, "style": { "font-family": [ "Helvetica" ], "font-size": 14, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "none" }, "reference-id": "", "value": "", "multiple-line": true, "format": { "base": "", "type": "datetime", "datetime": { "format": "%Y/%m/%d %H:%M:%S" } } }, { "id": "", "type": "text", "display": true, "description": "", "x": 320, "y": 85, "width": 195, "height": 20.5, "style": { "font-family": [ "Helvetica" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [ "underline" ] }, "texts": [ "Basic Format (multi-line)" ] }, { "id": "", "type": "text", "display": true, "description": "", "x": 320, "y": 150, "width": 189, "height": 20.5, "style": { "font-family": [ "Helvetica" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [ "underline" ] }, "texts": [ "Date Format (multi-line)" ] }, { "id": "", "type": "text", "display": true, "description": "", "x": 320, "y": 260, "width": 215, "height": 20.5, "style": { "font-family": [ "Helvetica" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [ "underline" ] }, "texts": [ "Number Format (multi-line)" ] }, { "id": "", "type": "text", "display": true, "description": "", "x": 320, "y": 425, "width": 217, "height": 20.5, "style": { "font-family": [ "Helvetica" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [ "underline" ] }, "texts": [ "Padding Format (multi-line)" ] }, { "id": "number_with_valid_string_number", "type": "text-block", "display": true, "description": "", "x": 20, "y": 360, "width": 205.1, "height": 16.1, "style": { "font-family": [ "Helvetica" ], "font-size": 14, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "none" }, "reference-id": "", "value": "", "multiple-line": false, "format": { "base": "", "type": "number", "number": { "delimiter": "_", "precision": 5 } } }, { "id": "padding_with_number_value", "type": "text-block", "display": true, "description": "", "x": 20, "y": 525, "width": 205.1, "height": 16.1, "style": { "font-family": [ "Helvetica" ], "font-size": 14, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "none" }, "reference-id": "", "value": "", "multiple-line": false, "format": { "base": "", "type": "padding", "padding": { "length": 10, "char": "0", "direction": "R" } } }, { "id": "padding_with_value_longer_than_length", "type": "text-block", "display": true, "description": "", "x": 20, "y": 500, "width": 205.1, "height": 16.1, "style": { "font-family": [ "Helvetica" ], "font-size": 14, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "none" }, "reference-id": "", "value": "", "multiple-line": false, "format": { "base": "", "type": "padding", "padding": { "length": 10, "char": "0", "direction": "L" } } } ], "state": { "layout-guides": [ { "type": "x", "position": 225.1 }, { "type": "y", "position": 84 } ] }, "title": "", "report": { "paper-type": "A4", "orientation": "portrait", "margin": [ 20, 20, 20, 20 ] } } ================================================ FILE: test/basic_report/features/text_block_formatting/test_feature.rb ================================================ # frozen_string_literal: true require 'test_helper' require 'date' class Thinreports::BasicReport::TestTextBlockFormattingFeature < Thinreports::FeatureTest[__dir__] feature do report = Thinreports::BasicReport::Report.new report.start_new_page layout: template_path do |page| # Basic page.item(:basic).value = 1980 # Date page.values( date_with_date: Date.new(2019, 1, 1), date_with_time: Time.new(2019, 1, 1, 12, 30, 45), date_with_not_a_date: 'Not applied' ) # Number page.values( number_with_delimiter: 9_999_999, number_with_precision: 1_000.1, number_with_delimiter_and_precision: 198_000, number_with_valid_string_number: '198000', number_with_invalid_string_number: 'Not applied' ) # Padding page.values( padding_left: '1234', padding_right: 'abcd', padding_with_value_longer_than_length: '9' * 11, padding_with_number_value: 9_999, padding_with_blank_value: '' ) # Basic (multi-line) page.item(:multiline_basic).value = 'TitleTitle' # Date (multi-line) page.item(:multiline_date).value = Date.new(2019, 2, 1) # Number (multi-line) page.item(:multiline_number).value = 123_456 # Padding (multi-line) page.item(:multiline_padding).value = '123' end assert_pdf report.generate end end ================================================ FILE: test/basic_report/features/text_block_overflow/template.tlf ================================================ { "version": "0.9.0", "items": [ { "id": "", "type": "rect", "display": true, "description": "", "x": 262.1, "y": 76, "width": 206.1, "height": 41, "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid", "fill-color": "#FFFFFF" }, "border-radius": 0 }, { "id": "", "type": "rect", "display": true, "description": "", "x": 57.9, "y": 74, "width": 164.2, "height": 17, "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid", "fill-color": "#FFFFFF" }, "border-radius": 0 }, { "id": "truncate", "type": "text-block", "display": true, "description": "", "x": 57.9, "y": 74, "width": 164.2, "height": 17, "style": { "font-family": [ "Helvetica" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "break-word" }, "reference-id": "", "value": "", "multiple-line": false, "format": { "base": "", "type": "" } }, { "id": "", "type": "text", "display": true, "description": "", "x": 43, "y": 46, "width": 200, "height": 20.5, "style": { "font-family": [ "Helvetica" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "Overflow: \"truncate\"" ] }, { "id": "", "type": "rect", "display": true, "description": "", "x": 57.9, "y": 99, "width": 164.2, "height": 18, "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid", "fill-color": "#FFFFFF" }, "border-radius": 0 }, { "id": "truncate_ja", "type": "text-block", "display": true, "description": "", "x": 57.9, "y": 99, "width": 164.2, "height": 18, "style": { "font-family": [ "IPAPMincho" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "break-word" }, "reference-id": "", "value": "", "multiple-line": false, "format": { "base": "", "type": "" } }, { "id": "", "type": "text", "display": true, "description": "", "x": 43, "y": 140, "width": 200, "height": 20.5, "style": { "font-family": [ "Helvetica" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "Overflow: \"fit\"" ] }, { "id": "", "type": "rect", "display": true, "description": "", "x": 57.9, "y": 169, "width": 164.2, "height": 30.7, "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid", "fill-color": "#FFFFFF" }, "border-radius": 0 }, { "id": "fit", "type": "text-block", "display": true, "description": "", "x": 57.9, "y": 169, "width": 164.2, "height": 30.7, "style": { "font-family": [ "Courier New" ], "font-size": 28, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "fit", "word-wrap": "break-word" }, "reference-id": "", "value": "", "multiple-line": false, "format": { "base": "", "type": "" } }, { "id": "", "type": "rect", "display": true, "description": "", "x": 57.9, "y": 209, "width": 164.2, "height": 28, "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid", "fill-color": "#FFFFFF" }, "border-radius": 0 }, { "id": "fit_ja", "type": "text-block", "display": true, "description": "", "x": 57.9, "y": 209, "width": 164.2, "height": 28, "style": { "font-family": [ "IPAPGothic" ], "font-size": 28, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "fit", "word-wrap": "break-word" }, "reference-id": "", "value": "", "multiple-line": false, "format": { "base": "", "type": "" } }, { "id": "", "type": "text", "display": true, "description": "", "x": 43, "y": 250, "width": 200, "height": 20.5, "style": { "font-family": [ "Helvetica" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "Overflow: \"expand\"" ] }, { "id": "", "type": "rect", "display": true, "description": "", "x": 57.9, "y": 279, "width": 164.2, "height": 18.9, "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid", "fill-color": "#FFFFFF" }, "border-radius": 0 }, { "id": "expand", "type": "text-block", "display": true, "description": "", "x": 57.9, "y": 279, "width": 164.2, "height": 18.9, "style": { "font-family": [ "Times New Roman" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "expand", "word-wrap": "break-word" }, "reference-id": "", "value": "", "multiple-line": false, "format": { "base": "", "type": "" } }, { "id": "", "type": "rect", "display": true, "description": "", "x": 57.9, "y": 370, "width": 164.2, "height": 18, "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid", "fill-color": "#FFFFFF" }, "border-radius": 0 }, { "id": "expand_ja", "type": "text-block", "display": true, "description": "", "x": 57.9, "y": 370, "width": 164.2, "height": 18, "style": { "font-family": [ "IPAMincho" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "expand", "word-wrap": "break-word" }, "reference-id": "", "value": "", "multiple-line": false, "format": { "base": "", "type": "" } }, { "id": "truncate_m", "type": "text-block", "display": true, "description": "", "x": 262.1, "y": 76, "width": 206.1, "height": 41, "style": { "font-family": [ "IPAPMincho" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "break-word" }, "reference-id": "", "value": "", "multiple-line": true, "format": { "base": "", "type": "" } }, { "id": "", "type": "rect", "display": true, "description": "", "x": 262.9, "y": 169, "width": 206.1, "height": 71.1, "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid", "fill-color": "#FFFFFF" }, "border-radius": 0 }, { "id": "fit_m", "type": "text-block", "display": true, "description": "", "x": 262.9, "y": 169, "width": 206.1, "height": 71.1, "style": { "font-family": [ "Helvetica" ], "font-size": 26, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "fit", "word-wrap": "break-word" }, "reference-id": "", "value": "", "multiple-line": true, "format": { "base": "", "type": "" } }, { "id": "", "type": "rect", "display": true, "description": "", "x": 262.9, "y": 279, "width": 206.1, "height": 41, "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid", "fill-color": "#FFFFFF" }, "border-radius": 0 }, { "id": "expand_m", "type": "text-block", "display": true, "description": "", "x": 262.9, "y": 279, "width": 206.1, "height": 41, "style": { "font-family": [ "Helvetica" ], "font-size": 26, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": 30, "line-height-ratio": 1, "letter-spacing": "", "font-style": [], "overflow": "expand", "word-wrap": "break-word" }, "reference-id": "", "value": "", "multiple-line": true, "format": { "base": "", "type": "" } } ], "state": { "layout-guides": [] }, "title": "Tblock Overflow", "report": { "paper-type": "A4", "orientation": "portrait", "margin": [ 20, 20, 20, 20 ] } } ================================================ FILE: test/basic_report/features/text_block_overflow/test_feature.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::TestTextBlockOverflowFeature < Thinreports::FeatureTest[__dir__] feature do report = Thinreports::BasicReport::Report.new layout: template_path report.start_new_page do |page| page.values(truncate: 'The string overflowing from the area will be truncated', truncate_ja: '領域から溢れたテキストは切り捨てられます。', truncate_m: 'The string overflowing from the area will be truncated') page.values(fit: 'The string overflowing from the area will be reduced', fit_ja: '領域から溢れたテキストは縮小されます。', fit_m: '複数行モードでも、領域から溢れたテキストは自動的に縮小され折り返して表示されます。') page.values(expand: 'If a string was overflowed, a box will be expanded automatically', expand_ja: 'テキストが溢れた場合、ボックスが自動的に拡張されます。', expand_m: 'If a string was overflowed, a box will be expanded automatically') end assert_pdf report.generate end end ================================================ FILE: test/basic_report/features/text_block_singleline/template.tlf ================================================ { "version": "0.9.0", "items": [ { "id": "fallback_to_ipafont", "type": "text-block", "display": true, "description": "", "x": 37, "y": 102, "width": 291.1, "height": 26, "style": { "font-family": [ "Helvetica" ], "font-size": 24, "color": "#ff0000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "break-word" }, "reference-id": "", "value": "", "multiple-line": false, "format": { "base": "", "type": "" } }, { "id": "", "type": "text", "display": true, "description": "", "x": 38, "y": 52, "width": 488, "height": 36, "style": { "font-family": [ "IPAMincho" ], "font-size": 16, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "Helveticaフォントが設定されているTblockに日本語をセットする。", "溢れたテキストは表示されない。" ] }, { "id": "set_multiline_text", "type": "text-block", "display": true, "description": "", "x": 37, "y": 178, "width": 291.1, "height": 24, "style": { "font-family": [ "IPAMincho" ], "font-size": 24, "color": "#ff0000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "break-word" }, "reference-id": "", "value": "", "multiple-line": false, "format": { "base": "", "type": "" } }, { "id": "", "type": "text", "display": true, "description": "", "x": 38, "y": 147, "width": 208, "height": 16, "style": { "font-family": [ "IPAMincho" ], "font-size": 16, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "複数行テキストをセットする" ] }, { "id": "", "type": "rect", "display": true, "description": "", "x": 37, "y": 102, "width": 291.1, "height": 26, "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid", "fill-color": "none" }, "border-radius": 0 }, { "id": "", "type": "rect", "display": true, "description": "", "x": 37, "y": 178, "width": 291.1, "height": 24, "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid", "fill-color": "none" }, "border-radius": 0 } ], "state": { "layout-guides": [] }, "title": "Single Line Tblock", "report": { "paper-type": "A4", "orientation": "portrait", "margin": [ 20, 20, 20, 20 ] } } ================================================ FILE: test/basic_report/features/text_block_singleline/test_feature.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::TestTextBlockSinglelineFeature < Thinreports::FeatureTest[__dir__] feature do report = Thinreports::BasicReport::Report.new layout: template_path report.start_new_page report.page.item(:fallback_to_ipafont).value('IPA明朝に自動的にフォールバックされる') report.page.item(:set_multiline_text).value("1行目\n2行目\n3行目\n4行目\n5行目") assert_pdf report.generate end end ================================================ FILE: test/basic_report/features/text_block_style/templates/basic_styles.tlf ================================================ { "version": "0.9.0", "items": [ { "id": "", "type": "text", "display": true, "description": "", "x": 20, "y": 21, "width": 555.2, "height": 20.5, "style": { "font-family": [ "Helvetica" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": 5, "font-style": [] }, "texts": [ "Char Space in Single(Helvetica)" ] }, { "id": "space_single_helvetica", "type": "text-block", "display": true, "description": "", "x": 20, "y": 50, "width": 555.2, "height": 18, "style": { "font-family": [ "Helvetica" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": 5, "font-style": [], "overflow": "truncate", "word-wrap": "break-word" }, "reference-id": "", "value": "", "multiple-line": false, "format": { "base": "", "type": "" } }, { "id": "", "type": "line", "display": true, "description": "", "style": { "border-color": "#ff0000", "border-width": 1, "border-style": "solid" }, "x1": 426, "y1": 20, "x2": 426, "y2": 78 }, { "id": "", "type": "text", "display": true, "description": "", "x": 20, "y": 83, "width": 323, "height": 18, "style": { "font-family": [ "IPAMincho" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": 5, "font-style": [] }, "texts": [ "文字間隔 in 単行(IPA明朝)" ] }, { "id": "space_single_ipamincho", "type": "text-block", "display": true, "description": "", "x": 20, "y": 110, "width": 555.2, "height": 18, "style": { "font-family": [ "IPAMincho" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": 5, "font-style": [], "overflow": "truncate", "word-wrap": "break-word" }, "reference-id": "", "value": "", "multiple-line": false, "format": { "base": "", "type": "" } }, { "id": "", "type": "line", "display": true, "description": "", "style": { "border-color": "#ff0000", "border-width": 1, "border-style": "solid" }, "x1": 333, "y1": 80, "x2": 333, "y2": 138 }, { "id": "", "type": "text", "display": true, "description": "", "x": 20, "y": 148, "width": 555.2, "height": 38, "style": { "font-family": [ "Times New Roman" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": 5, "font-style": [] }, "texts": [ "Char Space", "in Multiple(Times New Roman)" ] }, { "id": "space_multi_times", "type": "text-block", "display": true, "description": "", "x": 20, "y": 199.9, "width": 555.2, "height": 51.2, "style": { "font-family": [ "Times New Roman" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": 5, "font-style": [], "overflow": "truncate", "word-wrap": "break-word" }, "reference-id": "", "value": "", "multiple-line": true, "format": { "base": "", "type": "" } }, { "id": "", "type": "line", "display": true, "description": "", "style": { "border-color": "#ff0000", "border-width": 1, "border-style": "solid" }, "x1": 386, "y1": 165, "x2": 386, "y2": 251.1 }, { "id": "", "type": "line", "display": true, "description": "", "style": { "border-color": "#ff0000", "border-width": 1, "border-style": "solid" }, "x1": 149, "y1": 148, "x2": 149, "y2": 234.1 }, { "id": "", "type": "text", "display": true, "description": "", "x": 20, "y": 275, "width": 323, "height": 36, "style": { "font-family": [ "IPAMincho" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": 5, "font-style": [] }, "texts": [ "文字間隔", "in 複数行(IPA明朝)" ] }, { "id": "space_multi_ipamincho", "type": "text-block", "display": true, "description": "", "x": 20, "y": 324, "width": 555.2, "height": 52.1, "style": { "font-family": [ "IPAMincho" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": 5, "font-style": [], "overflow": "truncate", "word-wrap": "break-word" }, "reference-id": "", "value": "", "multiple-line": true, "format": { "base": "", "type": "" } }, { "id": "", "type": "line", "display": true, "description": "", "style": { "border-color": "#ff0000", "border-width": 1, "border-style": "solid" }, "x1": 249, "y1": 292, "x2": 249, "y2": 378.1 }, { "id": "", "type": "line", "display": true, "description": "", "style": { "border-color": "#ff0000", "border-width": 1, "border-style": "solid" }, "x1": 107, "y1": 273, "x2": 107, "y2": 359.1 }, { "id": "left_top", "type": "text-block", "display": true, "description": "", "x": 20, "y": 406.1, "width": 165.1, "height": 69.1, "style": { "font-family": [ "IPAGothic" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "break-word" }, "reference-id": "", "value": "", "multiple-line": true, "format": { "base": "", "type": "" } }, { "id": "left_center", "type": "text-block", "display": true, "description": "", "x": 215.1, "y": 406.1, "width": 165.1, "height": 69.1, "style": { "font-family": [ "IPAGothic" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "middle", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "break-word" }, "reference-id": "", "value": "", "multiple-line": true, "format": { "base": "", "type": "" } }, { "id": "left_bottom", "type": "text-block", "display": true, "description": "", "x": 410, "y": 406.1, "width": 165.1, "height": 69.1, "style": { "font-family": [ "IPAGothic" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "bottom", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "break-word" }, "reference-id": "", "value": "", "multiple-line": true, "format": { "base": "", "type": "" } }, { "id": "", "type": "rect", "display": true, "description": "", "x": 20, "y": 406.1, "width": 165.1, "height": 69.1, "style": { "border-color": "#ff0000", "border-width": 1, "border-style": "solid", "fill-color": "none" }, "border-radius": 0 }, { "id": "", "type": "rect", "display": true, "description": "", "x": 215.1, "y": 406.1, "width": 165.1, "height": 69.1, "style": { "border-color": "#ff0000", "border-width": 1, "border-style": "solid", "fill-color": "none" }, "border-radius": 0 }, { "id": "", "type": "rect", "display": true, "description": "", "x": 410, "y": 406.1, "width": 165.1, "height": 69.1, "style": { "border-color": "#ff0000", "border-width": 1, "border-style": "solid", "fill-color": "none" }, "border-radius": 0 }, { "id": "line_height", "type": "text-block", "display": true, "description": "", "x": 311.1, "y": 678, "width": 179.1, "height": 90, "style": { "font-family": [ "IPAPGothic" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": 41, "line-height-ratio": 2, "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "break-word" }, "reference-id": "", "value": "", "multiple-line": true, "format": { "base": "", "type": "" } }, { "id": "", "type": "text", "display": true, "description": "", "x": 99, "y": 678, "width": 179.1, "height": 142.8, "style": { "font-family": [ "IPAPGothic" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": 41, "line-height-ratio": 2, "letter-spacing": "", "font-style": [] }, "texts": [ "行間隔2.0", "日本語", "Thinreports" ] }, { "id": "", "type": "line", "display": true, "description": "", "style": { "border-color": "#ff0000", "border-width": 1, "border-style": "solid" }, "x1": 93, "y1": 692.8, "x2": 511.2, "y2": 692.8 }, { "id": "", "type": "line", "display": true, "description": "", "style": { "border-color": "#ff0000", "border-width": 1, "border-style": "solid" }, "x1": 93, "y1": 727.8, "x2": 511.2, "y2": 727.8 }, { "id": "", "type": "line", "display": true, "description": "", "style": { "border-color": "#ff0000", "border-width": 1, "border-style": "solid" }, "x1": 93, "y1": 766.8, "x2": 511.2, "y2": 766.8 }, { "id": "center_top", "type": "text-block", "display": true, "description": "", "x": 20, "y": 481, "width": 165.1, "height": 69.1, "style": { "font-family": [ "IPAGothic" ], "font-size": 18, "color": "#000000", "text-align": "center", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "break-word" }, "reference-id": "", "value": "", "multiple-line": true, "format": { "base": "", "type": "" } }, { "id": "center_center", "type": "text-block", "display": true, "description": "", "x": 215.1, "y": 481, "width": 165.1, "height": 69.1, "style": { "font-family": [ "IPAGothic" ], "font-size": 18, "color": "#000000", "text-align": "center", "vertical-align": "middle", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "break-word" }, "reference-id": "", "value": "", "multiple-line": true, "format": { "base": "", "type": "" } }, { "id": "center_bottom", "type": "text-block", "display": true, "description": "", "x": 410, "y": 481, "width": 165.1, "height": 69.1, "style": { "font-family": [ "IPAGothic" ], "font-size": 18, "color": "#000000", "text-align": "center", "vertical-align": "bottom", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "break-word" }, "reference-id": "", "value": "", "multiple-line": true, "format": { "base": "", "type": "" } }, { "id": "", "type": "rect", "display": true, "description": "", "x": 20, "y": 481, "width": 165.1, "height": 69.1, "style": { "border-color": "#ff0000", "border-width": 1, "border-style": "solid", "fill-color": "none" }, "border-radius": 0 }, { "id": "", "type": "rect", "display": true, "description": "", "x": 215.1, "y": 481, "width": 165.1, "height": 69.1, "style": { "border-color": "#ff0000", "border-width": 1, "border-style": "solid", "fill-color": "none" }, "border-radius": 0 }, { "id": "", "type": "rect", "display": true, "description": "", "x": 410, "y": 481, "width": 165.1, "height": 69.1, "style": { "border-color": "#ff0000", "border-width": 1, "border-style": "solid", "fill-color": "none" }, "border-radius": 0 }, { "id": "right_top", "type": "text-block", "display": true, "description": "", "x": 20, "y": 556, "width": 165.1, "height": 69.1, "style": { "font-family": [ "IPAGothic" ], "font-size": 18, "color": "#000000", "text-align": "right", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "break-word" }, "reference-id": "", "value": "", "multiple-line": true, "format": { "base": "", "type": "" } }, { "id": "right_center", "type": "text-block", "display": true, "description": "", "x": 215.1, "y": 556, "width": 165.1, "height": 69.1, "style": { "font-family": [ "IPAGothic" ], "font-size": 18, "color": "#000000", "text-align": "right", "vertical-align": "middle", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "break-word" }, "reference-id": "", "value": "", "multiple-line": true, "format": { "base": "", "type": "" } }, { "id": "right_bottom", "type": "text-block", "display": true, "description": "", "x": 410, "y": 556, "width": 165.1, "height": 69.1, "style": { "font-family": [ "IPAGothic" ], "font-size": 18, "color": "#000000", "text-align": "right", "vertical-align": "bottom", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "break-word" }, "reference-id": "", "value": "", "multiple-line": true, "format": { "base": "", "type": "" } }, { "id": "", "type": "rect", "display": true, "description": "", "x": 20, "y": 556, "width": 165.1, "height": 69.1, "style": { "border-color": "#ff0000", "border-width": 1, "border-style": "solid", "fill-color": "none" }, "border-radius": 0 }, { "id": "", "type": "rect", "display": true, "description": "", "x": 215.1, "y": 556, "width": 165.1, "height": 69.1, "style": { "border-color": "#ff0000", "border-width": 1, "border-style": "solid", "fill-color": "none" }, "border-radius": 0 }, { "id": "", "type": "rect", "display": true, "description": "", "x": 410, "y": 556, "width": 165.1, "height": 69.1, "style": { "border-color": "#ff0000", "border-width": 1, "border-style": "solid", "fill-color": "none" }, "border-radius": 0 } ], "state": { "layout-guides": [] }, "title": "Tblock Styles", "report": { "paper-type": "A4", "orientation": "portrait", "margin": [ 20, 20, 20, 20 ] } } ================================================ FILE: test/basic_report/features/text_block_style/templates/font_size.tlf ================================================ { "version": "0.9.0", "items": [ { "id": "", "type": "text", "display": true, "description": "", "x": 40, "y": 60, "width": 151.8, "height": 46, "style": { "font-family": [ "Helvetica" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "サイズ18" ] }, { "id": "text_single24", "type": "text", "display": true, "description": "", "x": 165, "y": 60, "width": 151.8, "height": 46, "style": { "font-family": [ "Helvetica" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "サイズ24" ] }, { "id": "text_single32", "type": "text", "display": true, "description": "", "x": 345, "y": 60, "width": 151.8, "height": 46, "style": { "font-family": [ "Helvetica" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "サイズ32" ] }, { "id": "", "type": "text", "display": true, "description": "", "x": 35, "y": 179.8, "width": 200, "height": 100, "style": { "font-family": [ "Helvetica" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "サイズ18", "サイズ18" ] }, { "id": "text_multiple24", "type": "text", "display": true, "description": "", "x": 165, "y": 179.8, "width": 200, "height": 100, "style": { "font-family": [ "Helvetica" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "サイズ24", "サイズ24" ] }, { "id": "text_multiple32", "type": "text", "display": true, "description": "", "x": 345, "y": 179.8, "width": 200, "height": 100, "style": { "font-family": [ "Helvetica" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "サイズ32", "サイズ32" ] }, { "id": "block_single18", "type": "text-block", "display": true, "description": "", "x": 40, "y": 320.6, "width": 103.4, "height": 17, "style": { "font-family": [ "Helvetica" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "break-word" }, "reference-id": "", "value": "", "multiple-line": false, "format": { "base": "", "type": "" } }, { "id": "block_single24", "type": "text-block", "display": true, "description": "", "x": 165, "y": 320.6, "width": 157.2, "height": 17, "style": { "font-family": [ "Helvetica" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "none" }, "reference-id": "", "value": "", "multiple-line": false, "format": { "base": "", "type": "" } }, { "id": "block_single32", "type": "text-block", "display": true, "description": "", "x": 345, "y": 320.6, "width": 230.2, "height": 17, "style": { "font-family": [ "Helvetica" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "none" }, "reference-id": "", "value": "", "multiple-line": false, "format": { "base": "", "type": "" } }, { "id": "block_multiple18", "type": "text-block", "display": true, "description": "", "x": 40, "y": 390, "width": 103.4, "height": 113.3, "style": { "font-family": [ "Helvetica" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "none" }, "reference-id": "", "value": "", "multiple-line": true, "format": { "base": "", "type": "" } }, { "id": "block_multiple24", "type": "text-block", "display": true, "description": "", "x": 165, "y": 390, "width": 157.2, "height": 113.3, "style": { "font-family": [ "Helvetica" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "none" }, "reference-id": "", "value": "", "multiple-line": true, "format": { "base": "", "type": "" } }, { "id": "block_multiple32", "type": "text-block", "display": true, "description": "", "x": 345, "y": 390, "width": 230.2, "height": 113.3, "style": { "font-family": [ "Helvetica" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "none" }, "reference-id": "", "value": "", "multiple-line": true, "format": { "base": "", "type": "" } } ], "state": { "layout-guides": [ { "type": "x", "position": 164.4 }, { "type": "x", "position": 344.1 }, { "type": "y", "position": 503.3 } ] }, "title": "FontSize", "report": { "paper-type": "A4", "orientation": "portrait", "margin": [ 20, 20, 20, 20 ] } } ================================================ FILE: test/basic_report/features/text_block_style/test_feature.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::TestTextBlockStyleFeature < Thinreports::FeatureTest[__dir__] feature do report = Thinreports::BasicReport::Report.new report.start_new_page layout: template_path('templates/basic_styles.tlf') do |page| page.item(:space_single_helvetica).value('Char Space in Single(Helvetica)') page.item(:space_single_ipamincho).value = '文字間隔 in 単行(IPA明朝)' page[:space_multi_times] = "Char Space\nin Multiple(Times New Roman)" page.item(:space_multi_ipamincho).value("文字間隔\nin 複数行(IPA明朝)") page.values(left_top: '左上揃え', left_center: '左中央揃え', left_bottom: '左下揃え', center_top: '中央上揃え', center_center: '中央揃え', center_bottom: '中央下揃え', right_top: '右上揃え', right_center: '右中央揃え', right_bottom: '右下揃え') page.item(:line_height).value("行間隔2.0\n日本語\nThinreports") end report.start_new_page layout: template_path('templates/font_size.tlf') do |page| page[:text_single24].style(:font_size, 24) page[:text_single32].style(:font_size, 32) page.item(:text_multiple24).style(:font_size, 24) page.item(:text_multiple32).style(:font_size, 32) page.item(:block_single18).value('サイズ18') page.item(:block_single24).style(:font_size, 24).value('サイズ24') page.item(:block_single32).style(:font_size, 32).value('サイズ32') page.item(:block_multiple18).value("サイズ18\nサイズ18") page.item(:block_multiple24).style(:font_size, 24).value("サイズ24\nサイズ24") page.item(:block_multiple32).style(:font_size, 32).value("サイズ32\nサイズ32") end assert_pdf report.generate end end ================================================ FILE: test/basic_report/features/text_character_spacing/template.tlf ================================================ { "version": "0.9.0", "items": [ { "id": "", "type": "text", "display": true, "description": "", "x": 20, "y": 21, "width": 555.2, "height": 40, "style": { "font-family": [ "Times New Roman" ], "font-size": 36, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": 10, "font-style": [] }, "texts": [ "Times Roman Left" ] }, { "id": "", "type": "line", "display": true, "description": "", "style": { "border-color": "#ff0000", "border-width": 1, "border-style": "solid" }, "x1": 436, "y1": 20, "x2": 436, "y2": 57 }, { "id": "", "type": "text", "display": true, "description": "", "x": 20, "y": 137, "width": 555.2, "height": 36, "style": { "font-family": [ "IPAMincho" ], "font-size": 36, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": 10, "font-style": [] }, "texts": [ "IPA明朝 左寄せ" ] }, { "id": "", "type": "line", "display": true, "description": "", "style": { "border-color": "#ff0000", "border-width": 1, "border-style": "solid" }, "x1": 351, "y1": 136, "x2": 351, "y2": 173 }, { "id": "", "type": "text", "display": true, "description": "", "x": 20, "y": 260, "width": 555.2, "height": 38.5, "style": { "font-family": [ "IPAPGothic" ], "font-size": 36, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": 10, "font-style": [] }, "texts": [ "IPA Pゴシック 左寄せ" ] }, { "id": "", "type": "line", "display": true, "description": "", "style": { "border-color": "#ff0000", "border-width": 1, "border-style": "solid" }, "x1": 487, "y1": 262, "x2": 487, "y2": 299 }, { "id": "", "type": "text", "display": true, "description": "", "x": 20, "y": 70, "width": 555.2, "height": 40, "style": { "font-family": [ "Times New Roman" ], "font-size": 36, "color": "#000000", "text-align": "right", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": 10, "font-style": [] }, "texts": [ "Times Roman Right" ] }, { "id": "", "type": "line", "display": true, "description": "", "style": { "border-color": "#ff0000", "border-width": 1, "border-style": "solid" }, "x1": 575, "y1": 21, "x2": 575.2, "y2": 821.8 }, { "id": "", "type": "line", "display": true, "description": "", "style": { "border-color": "#ff0000", "border-width": 1, "border-style": "solid" }, "x1": 131, "y1": 66, "x2": 131, "y2": 103 }, { "id": "", "type": "text", "display": true, "description": "", "x": 20, "y": 186.9, "width": 555.2, "height": 36, "style": { "font-family": [ "IPAMincho" ], "font-size": 36, "color": "#000000", "text-align": "right", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": 10, "font-style": [] }, "texts": [ "IPA明朝 右寄せ" ] }, { "id": "", "type": "line", "display": true, "description": "", "style": { "border-color": "#ff0000", "border-width": 1, "border-style": "solid" }, "x1": 247, "y1": 185.9, "x2": 247, "y2": 222.9 }, { "id": "", "type": "text", "display": true, "description": "", "x": 20, "y": 316, "width": 555.2, "height": 38.5, "style": { "font-family": [ "IPAPGothic" ], "font-size": 36, "color": "#000000", "text-align": "right", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": 10, "font-style": [] }, "texts": [ "IPA Pゴシック 右寄せ" ] }, { "id": "", "type": "line", "display": true, "description": "", "style": { "border-color": "#ff0000", "border-width": 1, "border-style": "solid" }, "x1": 108, "y1": 318, "x2": 108, "y2": 355 } ], "state": { "layout-guides": [ { "type": "x", "position": 59.5 }, { "type": "y", "position": 434.1 }, { "type": "y", "position": 374.1 } ] }, "title": "Character Spacing", "report": { "paper-type": "A4", "orientation": "portrait", "margin": [ 20, 20, 20, 20 ] } } ================================================ FILE: test/basic_report/features/text_character_spacing/test_feature.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::TestTextCharacterSpacingFeature < Thinreports::FeatureTest[__dir__] feature do report = Thinreports::BasicReport::Report.new layout: template_path report.start_new_page assert_pdf report.generate end end ================================================ FILE: test/basic_report/features/text_overflow_wrap/template.tlf ================================================ { "version": "0.12.0", "items": [ { "id": "", "type": "text", "display": true, "description": "", "x": 20, "y": 20, "width": 191, "height": 20, "style": { "font-family": [ "Helvetica" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "overflow-wrap: normal" ] }, { "id": "", "type": "text", "display": true, "description": "", "x": 20, "y": 230, "width": 199.9, "height": 20, "style": { "font-family": [ "Helvetica" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "overflow-wrap: anywhere" ] }, { "id": "", "type": "text", "display": true, "description": "", "x": 20, "y": 445, "width": 353.8, "height": 20, "style": { "font-family": [ "Helvetica" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "overflow-wrap: disable-break-word-by-space" ] }, { "id": "normal", "type": "text-block", "display": true, "description": "", "x": 20, "y": 49, "width": 243.6, "height": 162, "style": { "font-family": [ "IPAPGothic" ], "font-size": 16, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "none", "overflow-wrap": "normal" }, "reference-id": "", "value": "", "multiple-line": true, "format": { "base": "", "type": "" } }, { "id": "anywhere", "type": "text-block", "display": true, "description": "", "x": 20, "y": 261, "width": 243.6, "height": 162, "style": { "font-family": [ "IPAPGothic" ], "font-size": 16, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "none", "overflow-wrap": "anywhere" }, "reference-id": "", "value": "", "multiple-line": true, "format": { "base": "", "type": "" } }, { "id": "disable_break_word_by_space", "type": "text-block", "display": true, "description": "", "x": 20, "y": 481, "width": 243.6, "height": 162, "style": { "font-family": [ "IPAPGothic" ], "font-size": 16, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "none", "overflow-wrap": "disable-break-word-by-space" }, "reference-id": "", "value": "", "multiple-line": true, "format": { "base": "", "type": "" } }, { "id": "", "type": "line", "display": true, "description": "", "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid" }, "x1": 20, "y1": 41, "x2": 263.6, "y2": 41 }, { "id": "", "type": "line", "display": true, "description": "", "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid" }, "x1": 20, "y1": 253, "x2": 263.6, "y2": 253 }, { "id": "", "type": "line", "display": true, "description": "", "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid" }, "x1": 20, "y1": 470, "x2": 263.6, "y2": 470 } ], "state": { "layout-guides": [ { "type": "x", "position": 263.6 } ] }, "title": "", "report": { "paper-type": "A4", "orientation": "portrait", "margin": [ 20, 20, 20, 20 ] } } ================================================ FILE: test/basic_report/features/text_overflow_wrap/test_feature.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::TestTextOverflowWrapFeature < Thinreports::FeatureTest[__dir__] feature do report = Thinreports::BasicReport::Report.new layout: template_path report.start_new_page text = 'Thinreports is a PDF generation tool ' + 'that provides thinreports-editor and thinreports-generator. ' + 'Thinreports は thinreports-editor と thinreports-generator を提供するPDF 生成ツールです。' report.page.values( normal: text, anywhere: text, disable_break_word_by_space: text ) assert_pdf report.generate end end ================================================ FILE: test/basic_report/features/text_word_wrap/template.tlf ================================================ { "version": "0.9.0", "items": [ { "id": "single_none", "type": "text-block", "display": true, "description": "", "x": 33.1, "y": 101, "width": 210.1, "height": 17, "style": { "font-family": [ "Helvetica" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "expand", "word-wrap": "break-word" }, "reference-id": "", "value": "", "multiple-line": false, "format": { "base": "", "type": "" } }, { "id": "multiple_none", "type": "text-block", "display": true, "description": "", "x": 290.2, "y": 101.9, "width": 259.1, "height": 105.1, "style": { "font-family": [ "Helvetica" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "none" }, "reference-id": "", "value": "", "multiple-line": true, "format": { "base": "", "type": "" } }, { "id": "", "type": "rect", "display": true, "description": "", "x": 290.2, "y": 101.9, "width": 259.1, "height": 105.1, "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid", "fill-color": "none" }, "border-radius": 0 }, { "id": "multiple_break_word", "type": "text-block", "display": true, "description": "", "x": 290.2, "y": 232.9, "width": 259.1, "height": 105.1, "style": { "font-family": [ "Helvetica" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "break-word" }, "reference-id": "", "value": "", "multiple-line": true, "format": { "base": "", "type": "" } }, { "id": "", "type": "rect", "display": true, "description": "", "x": 290.2, "y": 232.9, "width": 259.1, "height": 105.1, "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid", "fill-color": "none" }, "border-radius": 0 }, { "id": "", "type": "line", "display": true, "description": "", "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid" }, "x1": 252, "y1": 100, "x2": 252, "y2": 379.1 }, { "id": "single_break_word", "type": "text-block", "display": true, "description": "", "x": 35, "y": 235, "width": 210.1, "height": 17, "style": { "font-family": [ "Helvetica" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "expand", "word-wrap": "break-word" }, "reference-id": "", "value": "", "multiple-line": false, "format": { "base": "", "type": "" } }, { "id": "", "type": "text", "display": true, "description": "", "x": 32.1, "y": 33, "width": 58, "height": 20.5, "style": { "font-family": [ "Helvetica" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [] }, "texts": [ "Locale:" ] }, { "id": "locale", "type": "text-block", "display": true, "description": "", "x": 100, "y": 36, "width": 145.1, "height": 17, "style": { "font-family": [ "Helvetica" ], "font-size": 18, "color": "#000000", "text-align": "left", "vertical-align": "top", "line-height": "", "line-height-ratio": "", "letter-spacing": "", "font-style": [], "overflow": "truncate", "word-wrap": "break-word" }, "reference-id": "", "value": "", "multiple-line": false, "format": { "base": "", "type": "" } }, { "id": "", "type": "rect", "display": true, "description": "", "x": 33.1, "y": 101, "width": 210.1, "height": 17, "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid", "fill-color": "none" }, "border-radius": 0 }, { "id": "", "type": "rect", "display": true, "description": "", "x": 34.1, "y": 235, "width": 210.1, "height": 17, "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid", "fill-color": "none" }, "border-radius": 0 } ], "state": { "layout-guides": [ { "type": "x", "position": 59.5 }, { "type": "y", "position": 61 } ] }, "title": "", "report": { "paper-type": "A4", "orientation": "portrait", "margin": [ 20, 20, 20, 20 ] } } ================================================ FILE: test/basic_report/features/text_word_wrap/test_feature.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::TestTextWordWrapFeature < Thinreports::FeatureTest[__dir__] feature do report = Thinreports::BasicReport::Report.new layout: template_path report.start_new_page report.page.item(:locale).value('ja') text = 'Thinreports は Ruby 向けのオープンソース帳票・PDF ツールです。' report.page.values single_none: text, single_break_word: text, multiple_none: text, multiple_break_word: text report.start_new_page report.page.item(:locale).value('en') text = 'Thinreports is the OSS reporting tool for Ruby-lang.' report.page.values single_none: text, single_break_word: text, multiple_none: text, multiple_break_word: text assert_pdf report.generate end end ================================================ FILE: test/basic_report/schema_helper.rb ================================================ # frozen_string_literal: true module Thinreports::BasicReport::SchemaHelper BASIC_SCHEMA_JSON = <<-EOF { "version": "%{version}", "title": "Report Title", "report": { "paper-type": "A4", "width": 100.0, "height": 200.0, "orientation": "landscape", "margin": [100.0, 200.0, 300.0, 999.9] }, "state": { "layout-guides": [ { "type": "x", "position": 0.1 } ] }, "items": [ { "type": "rect", "id": "rect_with_id", "display": true, "description": "", "x": 20, "y": 59, "width": 120.1, "height": 62, "style": { "border-color": "#000000", "border-width": 1, "border-style": "solid", "fill-color": "#FFFFFF" }, "border-radius": 0 }, { "type": "text-block", "id": "text_block", "x": 100.0, "y": 100.0, "value": "", "display": true }, { "type": "text-block", "id": "text_block_referenced", "x": 100.0, "y": 100.0, "value": "", "display": true, "reference-id": "text_block" }, { "type": "text-block", "id": "text_block_hidden", "x": 100.0, "y": 100.0, "value": "default value", "display": false }, { "type": "image-block", "id": "image_block", "x": 200.0, "y": 200.0, "width": 150.0, "height": 150.0, "value": "", "display": true }, { "type": "list", "id": "default", "x": 1.0, "y": 2.0, "display": true, "width": 200.0, "content-height": 300, "auto-page-break": true, "header": { "enabled": true, "height": 10.0, "items": [] }, "detail": { "height": 70.0, "items": [ { "type": "text-block", "id": "name", "value": "" } ] }, "page-footer": { "enabled": true, "items": [], "height": 30.0 }, "footer": { "enabled": true, "items": [], "height": 40.0 } } ] } EOF LIST_NO_HEADER_SCHEMA_JSON = <<-EOF { "version": "%{version}", "title": "Report Title", "report": { "paper-type": "A4", "width": 100.0, "height": 200.0, "orientation": "landscape", "margin": [100.0, 200.0, 300.0, 999.9] }, "state": { "layout-guides": [] }, "items": [ { "type": "list", "id": "default", "x": 1.0, "y": 2.0, "display": true, "header": { "enabled": false, "height": 10.0, "items": [] }, "width": 200.0, "content-height": 300, "auto-page-break": true, "detail": { "height": 20.0, "items": [ { "type": "text-block", "id": "name", "value": "" } ] }, "page-footer": { "enabled": true, "items": [], "height": 30.0 }, "footer": { "enabled": true, "items": [], "height": 40.0 } } ] } EOF def layout_file(options = {}) schema_version = options[:version] || Thinreports::VERSION schema_json = options[:schema] || BASIC_SCHEMA_JSON Tempfile.open %w( test-thinreports-layout .tlf ) do |file| file.puts (schema_json % { version: schema_version }) file end end end ================================================ FILE: test/basic_report/test_helper.rb ================================================ # frozen_string_literal: true require 'minitest/autorun' require 'minitest/spec' require 'minitest/unit' require 'mocha/minitest' require 'pdf/inspector' require 'digest/sha1' require 'pathname' require 'thinreports' require 'schema_helper' require 'feature_test' Mocha.configure do |c| c.strict_keyword_argument_matching = true end module Thinreports module BasicReport module TestHelper ROOT = Pathname.new(File.expand_path('..', __FILE__)) include SchemaHelper def assert_deprecated(&block) _out, err = capture_io { block.call } assert err.to_s.include?('[DEPRECATION]') end def data_file(*paths) ROOT.join('data', *paths).to_s end def read_data_file(*paths) File.read(data_file(*paths)) end def analyze_pdf_images(pdf_data) analyzer = PDF::Inspector::XObject.analyze(pdf_data) analyzer.page_xobjects .reduce(:merge).values .select { |o| o.hash[:Subtype] == :Image } end def assert_pdf_data(data) assert_equal '%PDF-', data[0..4] end end end end ================================================ FILE: test/basic_report/units/core/format/test_base.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::Core::Format::BaseTest < Minitest::Test TEST_FORMAT_CONFIG = { 'c1' => 'c1', 'c2' => { 'c2_1' => 'c2_1' }, 'c3' => { 'c3_1' => { 'c3_1_1' => 'c3_1_1', 'c3_1_2' => 'c3_1_2' } }, 'c4' => 'c4', 'c5' => 'c5', 'c6' => 9999, 'c7' => true, 'c8' => false, 'c9' => { 'c9_1' => true, 'c9_2' => false }, 'c10' => 'c10', 'c11' => { 'c11_1' => 'c11_1' }, 'c20' => 'c20', 'c21' => 'c21' } class TestFormat < Thinreports::BasicReport::Core::Format::Base # For testing attr_reader :config # Basic methods config_reader :c1 config_reader c2_1: %w( c2 c2_1 ), c3_1_1: %w( c3 c3_1 c3_1_1 ) config_reader :c4, :c5 config_reader 'c6' # Alias methods config_reader c1_alias: %w( c1 ), c2_1_alias: %w( c2 c2_1 ) # Invalid methods config_reader c3_invalid: %w( c3 c3_1 not_exist_key ) # Customized methods config_reader c2_1_customized: %w( c2 c2_1 ) do |val| "Customized #{val}" end # Checker methods config_checker true, :c7, :c8 config_checker true, c9_1: %w( c9 c9_1 ), c9_2: %w( c9 c9_2 ) # Writer methods config_writer :c10 config_writer c10_alias: %w( c10 ) config_writer c11_1: %w( c11 c11_1 ) # Writer for not exist config on default config_writer c11_2: %w( c11 c11_2 ) # Accessor methods config_accessor :c20 config_accessor :c21 do |val| "Customized #{val}" end end def setup @format = TestFormat.new(TEST_FORMAT_CONFIG) end def test_definition_of_all_config_methods methods = [:c1, :c2_1, :c3_1_1, :c4, :c5, :c6, :c1_alias, :c2_1_alias, :c3_invalid, :c2_1_customized, :c7?, :c8?, :c9_1?, :c9_2?, :c10=, :c10_alias=, :c11_1=, :c11_2=, :c20, :c20=, :c21, :c21=] assert methods.all? {|m| TestFormat.method_defined?(m) } end def test_readers # Basic assert_equal @format.c1, 'c1' assert_equal @format.c2_1, 'c2_1' assert_equal @format.c3_1_1, 'c3_1_1' assert_equal @format.c4, 'c4' assert_equal @format.c5, 'c5' assert_equal @format.c6, 9999 # Alias assert_equal @format.c1_alias, @format.c1 assert_equal @format.c2_1_alias, @format.c2_1 # Invalid location assert_nil @format.c3_invalid # Customized assert_equal @format.c2_1_customized, 'Customized c2_1' end def test_checkers assert_equal @format.c7?, true assert_equal @format.c8?, false assert_equal @format.c9_1?, true assert_equal @format.c9_2?, false end def test_reader_cannot_write_value assert_raises NoMethodError do @format.c1 = 'write' end # Alias method assert_raises NoMethodError do @format.c1_alias = 'write' end end def test_writers config = @format.config @format.c10 = 'c1_changed' assert_equal config['c10'], 'c1_changed' @format.c10_alias = 'c10_changed_by_alias' assert_equal config['c10'], 'c10_changed_by_alias' @format.c11_1 = 'c11_1_changed' assert_equal config['c11']['c11_1'], 'c11_1_changed' @format.c11_2 = 'c11_2_created' assert_equal config['c11']['c11_2'], 'c11_2_created' end def test_accessors @format.c20 = 'c20_changed' assert_equal @format.c20, 'c20_changed' @format.c21 = 'c21_changed' assert_equal @format.c21, 'Customized c21_changed' end def test_attributes assert_same @format.config, @format.attributes end end ================================================ FILE: test/basic_report/units/core/shape/base/test_internal.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::Core::Shape::Base::TestInternal < Minitest::Test include Thinreports::BasicReport::TestHelper def setup @report = Thinreports::BasicReport::Report.new layout: layout_file.path @report.start_new_page end def create_internal(format_config = {}, &block) format = unless format_config.empty? format_klass = Class.new(Thinreports::BasicReport::Core::Shape::Basic::Format) { config_reader(*format_config.keys.map {|k| k.to_sym }) } format_klass.new(format_config) else Thinreports::BasicReport::Core::Shape::Basic::Format.new({}) end klass = Class.new(Thinreports::BasicReport::Core::Shape::Base::Internal, &block) klass.new(@report.page, format) end def test_self_format_delegators_should_defines_method_delegated_from_format internal = create_internal('m1' => 'm1_value') { format_delegators :m1 } assert_respond_to internal, :m1 end def test_method_delegated_from_format_should_return_same_value_as_an_format internal = create_internal('m1' => 'm1 value') { format_delegators :m1 } assert_same internal.m1, internal.format.m1 end def test_copied_internal_should_have_the_new_value_specified_as_a_parent_property new_page = @report.start_new_page internal = create_internal { def style @style ||= Thinreports::BasicReport::Core::Shape::Style::Base.new(format) end } assert_same internal.copy(new_page).parent, new_page end def test_copied_internal_should_have_style_copied_from_original internal = create_internal { def style @style ||= Thinreports::BasicReport::Core::Shape::Style::Base.new(format) end } internal.style.write_internal_style(:foo, 'bar') new_internal = internal.copy(@report.start_new_page) assert_equal new_internal.style.read_internal_style(:foo), 'bar' end def test_copied_internal_should_have_states_property_copied_from_original internal = create_internal { def style @style ||= Thinreports::BasicReport::Core::Shape::Style::Base.new(format) end } internal.states[:foo] = 'bar' new_internal = internal.copy(@report.start_new_page) assert_equal new_internal.states[:foo], 'bar' end def test_copy_should_execute_block_with_new_internal_as_argument internal = create_internal { def style @style ||= Thinreports::BasicReport::Core::Shape::Style::Base.new(format) end } internal.copy(@report.start_new_page) do |new_internal| assert_equal new_internal.is_a?(Thinreports::BasicReport::Core::Shape::Base::Internal), true end end end ================================================ FILE: test/basic_report/units/core/shape/basic/test_block_format.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::Core::Shape::Basic::TestBlockFormat < Minitest::Test include Thinreports::BasicReport::TestHelper BLOCK_FORMAT = { 'value' => 'default value', 'x' => 100.0, 'y' => 200.0, 'width' => 300.0, 'height' => 400.0 } Basic = Thinreports::BasicReport::Core::Shape::Basic def test_attribute_readers format = Basic::BlockFormat.new(BLOCK_FORMAT) assert_equal 'default value', format.value end end ================================================ FILE: test/basic_report/units/core/shape/basic/test_block_interface.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::Core::Shape::Basic::TestBlockInterface < Minitest::Test include Thinreports::BasicReport::TestHelper Basic = Thinreports::BasicReport::Core::Shape::Basic def setup report = Thinreports::BasicReport::Report.new layout: layout_file.path parent = report.start_new_page format = Basic::BlockFormat.new({}) internal = Basic::BlockInternal.new parent, format @interface = Basic::BlockInterface.new parent, format, internal end def test_value_should_work_as_reader_with_no_arguments @interface.internal.write_value('new value') assert_equal @interface.value, 'new value' end def test_value_should_work_as_writer_with_arguments @interface.value('new value') assert_equal @interface.internal.read_value, 'new value' end end ================================================ FILE: test/basic_report/units/core/shape/basic/test_block_internal.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::Core::Shape::Basic::TestBlockInternal < Minitest::Test include Thinreports::BasicReport::TestHelper Basic = Thinreports::BasicReport::Core::Shape::Basic def test_read_value_should_return_value_from_format internal = init_internal('value' => 'default value') assert_equal internal.read_value, 'default value' end def test_read_value_should_return_value_from_state_with_value_key internal = init_internal internal.states[:value] = 'new value' assert_equal internal.read_value, 'new value' end def test_real_value_should_return_the_same_value_as_a_read_value_method internal = init_internal internal.states[:value] = 'foo' assert_same internal.real_value, internal.read_value end def test_write_value_should_save_value_to_states_store_as_value internal = init_internal internal.write_value('new value') assert_equal internal.states[:value], 'new value' end def test_type_of_asker_should_return_true_when_given_the_block_value assert_equal init_internal.type_of?(:block), true end def test_value_should_works_the_same_as_read_value_method internal = init_internal internal.write_value('new value') assert_same internal.read_value, internal.value end def test_type_of_asker_should_return_false_otherwise assert_equal %w( image-block text-block text list ).all? {|t| !init_internal.type_of?(t)}, true end private def init_internal(format = {}) report = Thinreports::BasicReport::Report.new layout: layout_file.path parent = report.start_new_page Basic::BlockInternal.new(parent, Basic::BlockFormat.new(format)) end end ================================================ FILE: test/basic_report/units/core/shape/basic/test_format.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::Core::Shape::Basic::TestFormat < Minitest::Test include Thinreports::BasicReport::TestHelper RECT_FORMAT = { "id" => "", "type" => "rect", "x" => 100.0, "y" => 200.0, "width" => 300.0, "height" => 400.0, "description" => "Description for rect", "display" => true, "rx" => 1.0, "ry" => 1.0, "style" => { "border-width" => 1, "border-color" => "#000000", "border-style" => "dashed", "fill-color" => "#ff0000" } } Basic = Thinreports::BasicReport::Core::Shape::Basic def test_attribute_readers format = Basic::Format.new(RECT_FORMAT) assert_equal '', format.id assert_equal 'rect', format.type assert_equal RECT_FORMAT['style'], format.style assert_equal true, format.display? end end ================================================ FILE: test/basic_report/units/core/shape/basic/test_interface.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::Core::Shape::Basic::TestInterface < Minitest::Test include Thinreports::BasicReport::TestHelper # Alias Basic = Thinreports::BasicReport::Core::Shape::Basic def create_interface(format_config = {}) report = Thinreports::BasicReport::Report.new layout: layout_file.path parent = report.start_new_page Basic::Interface.new parent, Basic::Format.new(format_config) end def test_id_should_return_id_with_reference_to_internal basic = create_interface('id' => 'foo') assert_equal basic.id, basic.internal.id end def test_id_should_return_cloned_id basic = create_interface('id' => 'basic-id') refute_same basic.id, basic.internal.id end def test_type_should_operate_as_delegator_of_internal basic = create_interface('type' => 'rect') assert_same basic.type, basic.internal.type end def test_visible_asker_should_return_result_with_reference_to_style_of_internal basic = create_interface('display' => false) assert_equal basic.visible?, basic.internal.style.visible end def test_visible_should_properly_set_visibility_to_style_of_internal basic = create_interface('display' => false) basic.visible(true) assert_equal basic.internal.style.visible, true end def test_style_should_operate_as_reader_when_one_argument_is_given basic = create_interface('style' => { 'fill-color' => '#ff0000' }) assert_equal basic.style(:fill_color), '#ff0000' end def test_style_should_operate_as_writer_when_two_arguments_are_given basic = create_interface basic.style(:border_color, '#000000') assert_equal basic.style(:border_color), '#000000' end def test_style_should_operate_as_writer_for_border_style_when_three_arguments_are_given basic = create_interface basic.style(:border, 1, '#ffffff') assert_equal basic.style(:border), [1, '#ffffff'] end def test_style_should_return_self_when_two_arguments_are_given assert_instance_of Basic::Interface, create_interface.style(:border_width, 1) end def test_style_should_return_self_when_three_arguments_are_given assert_instance_of Basic::Interface, create_interface.style(:border, 1, '#000000') end def test_styles_should_properly_set_the_specified_styles_as_Hash basic = create_interface basic.styles(fill_color: '#ff0000', border_color: '#000000', border_width: 5, visible: false) assert_equal [basic.style(:fill_color), basic.style(:border_color), basic.style(:border_width), basic.style(:visible)], ['#ff0000', '#000000', 5, false] end def test_hide_should_properly_set_false_to_visibility basic = create_interface basic.hide assert_equal basic.visible?, false end def test_hide_should_return_self assert_instance_of Basic::Interface, create_interface.hide end def test_show_should_properly_set_true_to_visibility basic = create_interface basic.show assert_equal basic.visible?, true end def test_show_should_return_self assert_instance_of Basic::Interface, create_interface.show end end ================================================ FILE: test/basic_report/units/core/shape/basic/test_internal.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::Core::Shape::Basic::TestInternal < Minitest::Test include Thinreports::BasicReport::TestHelper # Alias Basic = Thinreports::BasicReport::Core::Shape::Basic def create_internal(format_config = {}) report = Thinreports::BasicReport::Report.new layout: layout_file.path Basic::Internal.new report.page, Basic::Format.new(format_config) end def test_id_should_operate_as_delegator_of_format basic = create_internal('id' => 'basic-id') assert_same basic.id, basic.format.id end def test_type_should_operate_as_delegator_of_format basic = create_internal('type' => 'ellipse') assert_same basic.type, basic.format.type end def test_style_should_return_instance_of_StyleGraphic assert_instance_of Thinreports::BasicReport::Core::Shape::Style::Graphic, create_internal.style end def test_type_of_asker_should_already_return_true_when_the_specified_type_is_basic assert_equal create_internal.type_of?(:basic), true end def test_type_of_asker_should_return_true_when_the_specified_type_equal_self_type_name result = [] result << create_internal('type' => 'rect').type_of?('rect') result << create_internal('type' => 'ellipse').type_of?('ellipse') result << create_internal('type' => 'line').type_of?('line') result << create_internal('type' => 'image').type_of?('image') assert_equal result.all?, true end def test_identifier basic = create_internal 'id' => 'basic-id' assert_equal 'basic-id', basic.identifier basic.style.stubs(identifier: 'style-identifier') assert_equal basic.identifier, 'basic-idstyle-identifier' end end ================================================ FILE: test/basic_report/units/core/shape/image_block/test_interface.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::Core::Shape::ImageBlock::TestInterface < Minitest::Test include Thinreports::BasicReport::TestHelper ImageBlock = Thinreports::BasicReport::Core::Shape::ImageBlock def setup @report = Thinreports::BasicReport::Report.new layout: layout_file.path @page = @report.start_new_page end def test_src @page.item(:image_block).src('/path/to/image.png') assert_equal '/path/to/image.png', @page.item(:image_block).src end def test_src= @page.item(:image_block).src = '/path/to/image.png' assert_equal '/path/to/image.png', @page.item(:image_block).src end end ================================================ FILE: test/basic_report/units/core/shape/image_block/test_internal.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::Core::Shape::ImageBlock::TestInternal < Minitest::Test include Thinreports::BasicReport::TestHelper ImageBlock = Thinreports::BasicReport::Core::Shape::ImageBlock def test_src_should_return_the_same_value_as_value_method internal = create_internal internal.write_value('/path/to/image.png') assert_same internal.src, internal.read_value end def test_type_of_asker_should_return_true_when_iblock_value_is_given assert_equal create_internal.type_of?('image-block'), true end def test_type_of_asker_should_return_true_when_block_value_is_given assert_equal create_internal.type_of?(:block), true end def create_internal report = Thinreports::BasicReport::Report.new layout: layout_file.path parent = report.start_new_page ImageBlock::Internal.new parent, ImageBlock::Format.new({}) end end ================================================ FILE: test/basic_report/units/core/shape/list/test_format.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::Core::Shape::List::TestFormat < Minitest::Test include Thinreports::BasicReport::TestHelper LIST_FORMAT = { 'type' => 'list', 'id' => 'list', 'display' => true, 'x' => 10.0, 'y' => 20.0, 'width' => 30.0, 'height' => 40.0, 'content-height' => 255, 'auto-page-break' => true, 'header' => { 'enabled' => true, 'height' => 10.0, 'translate' => { 'x' => 210.0, 'y' => 310.0 }, 'items' => [] }, 'detail' => { 'height' => 20.0, 'translate' => { 'x' => 220.0, 'y' => 320.0 }, 'items' => [] }, 'page-footer' => { 'enabled' => true, 'height' => 30.0, 'translate' => { 'x' => 230.0, 'y' => 330.0 }, 'items' => [] }, 'footer' => { 'enabled' => false, 'height' => 40.0, 'translate' => { 'x' => 240.0, 'y' => 340.0 }, 'items' => [] } } List = Thinreports::BasicReport::Core::Shape::List def test_has_section? format = List::Format.new(LIST_FORMAT) assert_equal true, format.has_section?(:detail) assert_equal true, format.has_section?(:page_footer) assert_equal false, format.has_section?(:footer) end def test_section_height format = List::Format.new(LIST_FORMAT) assert_equal 10.0, format.section_height(:header) end def test_attribute_readers format = List::Format.new(LIST_FORMAT) assert_equal 255, format.height assert_equal true, format.auto_page_break? assert_equal true, format.has_header? assert_equal true, format.has_page_footer? assert_equal false, format.has_footer? assert_equal 20.0, format.detail_height assert_equal 30.0, format.page_footer_height assert_equal 40.0, format.footer_height assert_equal 10.0, format.header_height end def test_section_base_position_top page_footer_disabled = LIST_FORMAT format = List::Format.new(page_footer_disabled) assert_equal 310.0, format.section_base_position_top(:header) assert_equal 320.0, format.section_base_position_top(:detail) assert_equal 310.0, format.section_base_position_top(:page_footer) assert_equal 0, format.section_base_position_top(:footer) format_footer_enabled = format_section_enabled(true, 'footer', LIST_FORMAT) format = List::Format.new(format_footer_enabled) assert_equal 290.0, format.section_base_position_top(:footer) format_footer_enabld_and_page_footer_disabled = format_section_enabled(false, 'page-footer', format_footer_enabled) format = List::Format.new(format_footer_enabld_and_page_footer_disabled) assert_equal 320.0, format.section_base_position_top(:footer) end def test_initialize_sections format = List::Format.new(LIST_FORMAT) assert_instance_of List::SectionFormat, format.sections[:header] assert_instance_of List::SectionFormat, format.sections[:detail] assert_instance_of List::SectionFormat, format.sections[:page_footer] assert_nil format.sections[:footer] end private def format_section_enabled(enable, section, list_format) section_format = list_format[section].dup section_format['enabled'] = enable list_format.merge(section => section_format) end end ================================================ FILE: test/basic_report/units/core/shape/list/test_manager.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::Core::Shape::List::TestManager < Minitest::Test include Thinreports::BasicReport::TestHelper # Alias List = Thinreports::BasicReport::Core::Shape::List def create_report(&block) report = Thinreports::BasicReport::Report.new layout: layout_file.path block.call(report) if block_given? report end def list_manager report = create_report {|r| r.start_new_page } report.page.list.manager end def test_current_page_should_return_the_instance_of_ListPage assert_instance_of List::Page, list_manager.current_page end def test_current_page_state_should_return_the_instance_of_ListPageState assert_instance_of List::PageState, list_manager.current_page_state end def test_switch_current_should_replace_own_current_page_property_by_the_given_page report = create_report {|r| r.start_new_page } list = report.page.list new_page = List::Page.new(report.page, list.internal.format) list.manager.switch_current!(new_page) assert_same list.manager.current_page, new_page end def test_switch_current_should_replace_own_current_page_state_property_by_internal_property_of_the_given_page report = create_report {|r| r.start_new_page } list = report.page.list new_page = List::Page.new(report.page, list.internal.format) list.manager.switch_current!(new_page) assert_same list.manager.current_page_state, new_page.internal end def test_switch_current_should_return_the_self report = create_report {|r| r.start_new_page } list = report.page.list new_page = List::Page.new(report.page, list.internal.format) assert_same list.manager.switch_current!(new_page), list.manager end def test_page_count report = create_report assert_equal report.page_count, 0 report.list.page_break report.list.page_break assert_equal report.list.manager.page_count, 2 end end ================================================ FILE: test/basic_report/units/core/shape/list/test_page.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::Core::Shape::List::TestPage < Minitest::Test include Thinreports::BasicReport::TestHelper # Alias List = Thinreports::BasicReport::Core::Shape::List LIST_SCHEMA = { 'type' => 'list', 'id' => 'list', 'display' => true, 'x' => 10.0, 'y' => 20.0, 'width' => 30.0, 'height' => 40.0, 'content-height' => 255, 'auto-page-break' => true, 'header' => { 'enabled' => true, 'height' => 10.0, 'translate' => { 'x' => 210.0, 'y' => 310.0 }, 'items' => [] }, 'detail' => { 'height' => 20.0, 'translate' => { 'x' => 220.0, 'y' => 320.0 }, 'items' => [] }, 'page-footer' => { 'enabled' => false, 'height' => 30.0, 'translate' => { 'x' => 230.0, 'y' => 330.0 }, 'items' => [] }, 'footer' => { 'enabled' => false, 'height' => 40.0, 'translate' => { 'x' => 240.0, 'y' => 340.0 }, 'items' => [] } } def create_report(&block) report = Thinreports::BasicReport::Report.new layout: layout_file.path block.call(report) if block_given? report end def test_on_page_finalize_callback report = create_report list = report.list counter = 0 callback = -> { counter += 1 } list.on_page_finalize(&callback) 5.times { list.add_row } assert_equal 1, counter report.finalize assert_equal 2, counter end def test_on_page_footer_insert_callback report = create_report list = report.list tester = 0 callback = -> footer { assert_instance_of List::SectionInterface, footer assert_equal footer.internal.section_name, :page_footer tester += 1 } list.on_page_footer_insert(&callback) 5.times { list.add_row } assert_equal 1, tester report.finalize assert_equal 2, tester end def test_on_footer_insert_callback report = create_report list = report.list tester = 0 callback = -> footer { assert_instance_of List::SectionInterface, footer assert_equal footer.internal.section_name, :footer tester += 1 } list.on_footer_insert(&callback) 5.times { list.add_row } assert_equal 0, tester report.finalize assert_equal 1, tester end def test_copy_should_properly_work_when_list_has_not_header report = Thinreports::BasicReport::Report.new layout: layout_file(schema: LIST_NO_HEADER_SCHEMA_JSON).path 10.times { report.list.add_row } rescue => e flunk exception_details(e, 'Not worked when list has not header') end def test_copy_when_auto_page_break_disabled list_schema = LIST_SCHEMA.merge('auto-page-break' => false) report = Thinreports::BasicReport::Report::Base.new layout = Thinreports::BasicReport::Layout::Base.new(layout_file.path) list_format = Thinreports::BasicReport::Core::Shape::List::Format.new(list_schema) list_page = List::Page.new(report.page, list_format) 2.times { list_page.add_row } copied_list_page = list_page.copy(Thinreports::BasicReport::Report::Page.new(report, layout)) assert list_page.manager.finalized? assert copied_list_page.manager.finalized? assert_equal 2, copied_list_page.internal.rows.count end def test_copy_when_auto_page_break_enabled list_schema = LIST_SCHEMA.merge('auto-page-break' => true) report = Thinreports::BasicReport::Report::Base.new layout = Thinreports::BasicReport::Layout::Base.new(layout_file.path) list_format = Thinreports::BasicReport::Core::Shape::List::Format.new(list_schema) list_page = List::Page.new(report.page, list_format) 2.times { list_page.add_row } list_page.manager.finalize_page copied_list_page = list_page.copy(Thinreports::BasicReport::Report::Page.new(report, layout)) refute list_page.manager.finalized? refute copied_list_page.manager.finalized? assert_empty copied_list_page.internal.rows end end ================================================ FILE: test/basic_report/units/core/shape/list/test_page_state.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::Core::Shape::List::TestPageState < Minitest::Test include Thinreports::BasicReport::TestHelper # Alias List = Thinreports::BasicReport::Core::Shape::List def setup parent = mock('parent') format = mock('format') @state = List::PageState.new(parent, format) end def test_alias_class assert_same List::PageState, List::Internal end def test_type_of? assert_equal @state.type_of?('list'), true end def test_finalized! assert_equal @state.finalized?, false @state.finalized! assert_equal @state.finalized?, true end end ================================================ FILE: test/basic_report/units/core/shape/list/test_section_format.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::Core::Shape::List::TestSectionFormat < Minitest::Test include Thinreports::BasicReport::TestHelper LIST_SECTION_FORMAT = { 'enabled' => true, 'height' => 47.7, 'translate' => { 'x' => 0, 'y' => -64.2 }, 'items' => [ { 'type' => 'rect', 'id' => '' }, { 'type' => 'text-block', 'id' => 'text_block' } ] } Shape = Thinreports::BasicReport::Core::Shape List = Thinreports::BasicReport::Core::Shape::List def test_attribute_readers format = List::SectionFormat.new(LIST_SECTION_FORMAT) assert_equal 47.7, format.height assert_equal 0, format.relative_left assert_equal(-64.2, format.relative_top) assert_equal true, format.display? end def test_initialize_items format = List::SectionFormat.new(LIST_SECTION_FORMAT) assert_equal 1, format.shapes.count assert_instance_of Shape::TextBlock::Format, format.shapes[:text_block] end end ================================================ FILE: test/basic_report/units/core/shape/list/test_section_interface.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::Core::Shape::List::TestSectionInterface < Minitest::Test include Thinreports::BasicReport::TestHelper BASIC_SECTION_SCHEMA = { 'height' => 1.0, 'enabled' => true, 'items' => [] } # Alias List = Thinreports::BasicReport::Core::Shape::List def setup @report = Thinreports::BasicReport::Report.new layout: layout_file.path end def create_interface(extra_section_schema = {}) parent = @report.start_new_page List::SectionInterface.new( parent, List::SectionFormat.new(BASIC_SECTION_SCHEMA.merge(extra_section_schema)), :section ) end def test_internal_should_return_instance_of_SectionInternal assert_instance_of List::SectionInternal, create_interface.internal end def test_initialize_should_properly_set_the_specified_section_name_to_internal assert_equal create_interface.internal.section_name, :section end def test_initialize_should_properly_initialize_manager assert_instance_of Thinreports::BasicReport::Core::Shape::Manager::Internal, create_interface.manager end def test_height_should_operate_as_delegator_of_internal list = create_interface('height' => 100) assert_same list.height, list.internal.height end def test_copied_interface_should_succeed_an_section_name_of_original list = create_interface new_parent = @report.start_new_page assert_same list.copy(new_parent).internal.section_name, list.internal.section_name end def test_copied_interface_should_have_all_the_copies_of_Shape_which_original_holds list = create_interface copied_list(list) do |new_list| assert_equal new_list.manager.shapes.size, 3 end end def copied_list(list, &block) tblock = Thinreports::BasicReport::Core::Shape::TextBlock new_parent = @report.start_new_page %w( foo bar hoge ).each do |id| list.manager.format.shapes[id.to_sym] = tblock::Format.new('type' => 'text-block', 'id' => id) list.item(id).value(10) end block.call(list.copy(new_parent)) end end ================================================ FILE: test/basic_report/units/core/shape/list/test_section_internal.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::Core::Shape::List::TestSectionInternal < Minitest::Test include Thinreports::BasicReport::TestHelper BASIC_SECTION_SCHEMA = { 'height' => 1.0, 'enabled' => true, 'items' => [] } # Alias List = Thinreports::BasicReport::Core::Shape::List def create_internal(extra_section_schema = {}) report = Thinreports::BasicReport::Report.new layout: layout_file.path List::SectionInternal.new( report, List::SectionFormat.new(BASIC_SECTION_SCHEMA.merge(extra_section_schema)) ) end def test_height_should_operate_as_delegator_of_format list = create_internal('height' => 100) assert_same list.height, list.format.height end def test_relative_left_should_operate_as_delegator_of_format list = create_internal('translate' => {'x' => 10}) assert_same list.relative_left, list.format.relative_left end def test_move_top_to_should_properly_set_value_to_states_as_relative_top list = create_internal list.move_top_to(200) assert_equal list.states[:relative_top], 200 end def test_relative_top list = create_internal('translate' => { 'y' => 100 }) assert_equal 0, list.relative_top list.move_top_to 50 assert_equal 50, list.relative_top end end ================================================ FILE: test/basic_report/units/core/shape/manager/test_format.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::Core::Shape::Manager::TestFormat < Minitest::Test include Thinreports::BasicReport::TestHelper class TestFormat < Thinreports::BasicReport::Core::Shape::Manager::Format; end def test_identifier_should_return_the_same_as_object_id_when_id_is_not_given format = TestFormat.new({}) assert_equal format.identifier, format.object_id end def test_identifier_should_return_the_specified_id_when_id_is_given assert_equal TestFormat.new({}, :any_id).identifier, :any_id end def test_has_shape format = TestFormat.new({}) do |f| f.shapes[:foo] = 1 end assert format.has_shape?(:foo) refute format.has_shape?(:unknown) end def test_find_shape_should_return_format_of_shape_when_shape_is_found format = TestFormat.new({}) do |f| f.shapes[:foo] = Thinreports::BasicReport::Core::Shape::TextBlock::Format.new('id' => 'foo', 'type' => 'text-block') end assert_equal format.find_shape(:foo).id, 'foo' end def test_find_shape_should_return_nil_when_shape_is_not_found assert_nil TestFormat.new({}).find_shape(:unknown) end end ================================================ FILE: test/basic_report/units/core/shape/manager/test_internal.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::Core::Shape::Manager::TestInternal < Minitest::Test include Thinreports::BasicReport::TestHelper # Alias Shape = Thinreports::BasicReport::Core::Shape def create_shape_format(type, id, other_config = {}) Shape::Format(type).new({'id' => id, 'type' => type, 'display' => true}.merge(other_config)) end def create_internal(&block) report = Thinreports::BasicReport::Report.new layout: layout_file.path format = report.layout.format block.call(format) if block_given? report.start_new_page.manager end def test_find_format_should_return_format_with_the_specified_Symbol_id assert_equal create_internal.find_format(:text_block).id, 'text_block' end def test_find_format_should_return_format_with_the_specified_String_id assert_equal create_internal.find_format('text_block').id, 'text_block' end def test_find_format_should_return_nil_when_format_with_specified_id_is_not_found assert_nil create_internal.find_format(:unknown) end def test_find_item_should_return_shape_with_the_specified_id assert_instance_of Shape::TextBlock::Interface, create_internal.find_item(:text_block) end def test_find_item_should_return_shape_in_shapes_registry_when_the_specified_shape_exists_in_registry internal = create_internal internal.find_item(:text_block) assert_same internal.find_item(:text_block), internal.shapes[:text_block] end def test_find_item_should_return_shape_when_passing_in_the_specified_only_filter internal = create_internal assert_equal internal.find_item(:text_block, only: 'text-block').id, 'text_block' end def test_find_item_should_return_nil_when_not_passing_in_the_specified_only_filter internal = create_internal assert_nil internal.find_item(:text_block, only: 'list') end def test_find_item_should_return_shape_when_passing_in_the_specified_except_filter internal = create_internal assert_equal internal.find_item(:default, except: 'text-block').id, 'default' end def test_find_item_should_return_shape_when_not_passing_in_the_specified_except_filter internal = create_internal assert_nil internal.find_item(:default, except: 'list') end def test_final_shape_should_return_nil_when_shape_is_not_found internal = create_internal assert_nil internal.final_shape(:unknown) end def test_final_shape_should_return_nil_when_shape_is_stored_in_shapes_and_hidden internal = create_internal internal.find_item(:text_block).hide assert_nil internal.final_shape(:text_block) end def test_final_shape_should_return_shape_when_shape_is_stored_in_shapes_and_TextBlock_with_value internal = create_internal internal.find_item(:text_block).value('value') assert_equal internal.final_shape(:text_block).id, 'text_block' end def test_final_shape_should_return_nil_when_shape_is_stored_in_shapes_and_TextBlock_with_no_value internal = create_internal internal.find_item(:text_block) assert_nil internal.final_shape(:text_block) end def test_final_shape_should_return_shape_when_shape_is_stored_in_shapes_and_ImageBlock_with_src internal = create_internal internal.find_item(:image_block).src('/path/to/image.png') assert_equal internal.final_shape(:image_block).id, 'image_block' end def test_final_shape_should_return_nil_when_shape_is_stored_in_shapes_and_ImageBlock_with_no_src internal = create_internal assert_nil internal.final_shape(:image_block) end def test_final_shape_should_return_nil_when_shape_isnot_stored_in_shapes_and_hidden assert_nil create_internal.final_shape(:text_block_hidden) end def test_final_shape_should_return_shape_when_shape_isnot_stored_in_shapes_and_not_Block assert_equal create_internal.final_shape(:rect_with_id).id, 'rect_with_id' end def test_final_shape_should_return_nil_when_shape_isnot_stored_in_shapes_and_ImageBlock internal = create_internal do |f| f.shapes[:iblock] = create_shape_format('image-block', 'iblock') end assert_nil internal.final_shape(:iblock) end def test_final_shape_should_return_shape_when_shape_isnot_stored_in_shapes_and_TextBlock_with_reference assert_equal create_internal.final_shape(:text_block_referenced).id, 'text_block_referenced' end def test_final_shape_should_return_nil_when_shape_isnot_stored_in_shapes_and_TextBlock_with_no_value_no_reference internal = create_internal assert_nil internal.final_shape(:t1) end end ================================================ FILE: test/basic_report/units/core/shape/manager/test_target.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::Core::Shape::Manager::TestTarget < Minitest::Test include Thinreports::BasicReport::TestHelper # Alias Shape = Thinreports::BasicReport::Core::Shape class TestManager include Shape::Manager::Target attr_reader :layout, :report def initialize(report, layout) @report = report @layout = layout initialize_manager(layout.format) do |f| Shape::Interface(self, f) end end end def create_shape_format(type, id) Shape::Format(type).new('id' => id, 'type' => type) end def create_manager report = Thinreports::BasicReport::Report.new layout: layout_file.path layout = report.layout TestManager.new(report, layout) end def test_shorthand_for_finding_item manager = create_manager text_block1 = manager[:text_block] text_block1.value = 'block1' assert_instance_of Shape::TextBlock::Interface, manager[:text_block] assert_instance_of Shape::ImageBlock::Interface, manager[:image_block] assert_instance_of Shape::Basic::Interface, manager[:rect_with_id] assert_raises Thinreports::BasicReport::Errors::UnknownItemId do manager[:unknown_id] end assert_raises Thinreports::BasicReport::Errors::UnknownItemId do manager[:default] end end def test_shorthand_for_setting_value_to_block manager = create_manager manager[:text_block] = 'value of text block1' assert_equal 'value of text block1', manager.item(:text_block).value manager[:image_block] = '/path/to/image.png' assert_equal '/path/to/image.png', manager.item(:image_block).src assert_raises(NoMethodError) { manager[:rect_with_id] = 'value' } assert_raises(Thinreports::BasicReport::Errors::UnknownItemId) { manager[:default] = 'value' } end def test_manager_should_return_instance_of_ManagerInternal assert_instance_of Shape::Manager::Internal, create_manager.manager end def test_item_should_properly_return_shape_with_the_specified_Symbol_id assert_equal create_manager.item(:text_block).id, 'text_block' end def test_item_should_properly_return_shape_with_the_specified_String_id assert_equal create_manager.item('text_block').id, 'text_block' end def test_item_should_raise_when_the_shape_with_the_specified_id_is_not_found assert_raises Thinreports::BasicReport::Errors::UnknownItemId do create_manager.item(:unknown) end end def test_item_should_set_an_shape_as_argument_when_a_block_is_given id = nil create_manager.item(:text_block) {|s| id = s.id } assert_equal id, 'text_block' end def test_item_should_raise_when_type_of_shape_with_the_specified_id_is_list assert_raises Thinreports::BasicReport::Errors::UnknownItemId do create_manager.item(:default) end end def test_list_should_properly_return_list_with_the_specified_Symbol_id assert_equal create_manager.list(:default).id, 'default' end def test_list_should_properly_return_list_with_the_specified_String_id assert_equal create_manager.list('default').id, 'default' end def test_list_should_raise_when_type_of_shape_with_the_specified_id_is_not_list assert_raises Thinreports::BasicReport::Errors::UnknownItemId do create_manager.list(:text_block) end end def test_list_should_use_default_as_id_when_id_is_omitted assert_equal create_manager.list.id, 'default' end def test_values_should_properly_set_values_to_shapes_with_specified_id manager = create_manager manager.values(text_block: 1000) assert_equal manager.item(:text_block).value, 1000 end def test_item_exists_asker_should_return_true_when_shape_with_specified_Symbol_id_is_found assert_equal create_manager.item_exists?(:text_block), true end def test_item_exists_asker_should_return_true_when_shape_with_specified_String_id_is_found assert_equal create_manager.item_exists?('text_block'), true end def test_item_exists_asker_should_return_false_when_shape_with_specified_id_not_found assert_equal create_manager.item_exists?('unknown'), false end def test_exists_asker_should_operate_like_as_item_exists_asker manager = create_manager assert_equal manager.exists?(:unknown), manager.item_exists?(:unknown) end end ================================================ FILE: test/basic_report/units/core/shape/page_number/test_format.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::Core::Shape::PageNumber::TestFormat < Minitest::Test include Thinreports::BasicReport::TestHelper PAGE_NUMBER_FORMAT = { 'id' => '', 'type' => 'page-number', 'display' => true, 'x' => 100.0, 'y' => 200.0, 'width' => 300.0, 'height' => 400.0, 'format' => '{page} / {total}', 'target' => '', 'style' => { 'overflow' => 'truncate', 'letter-spacing' => 'normal', 'color' => '#000000', 'font-size' => 18, 'font-family' => ['Helvetica'], 'line-height' => 60, 'line-height-ratio' => 1.5, 'text-align' => 'left' } } PageNumber = Thinreports::BasicReport::Core::Shape::PageNumber def test_attribute_readers format = PageNumber::Format.new(PAGE_NUMBER_FORMAT) assert_equal 'truncate', format.overflow assert_equal '', format.target assert_equal '{page} / {total}', format.default_format end def test_id format = PageNumber::Format.new(PAGE_NUMBER_FORMAT) assert_match(/^__pageno\d+$/, format.id) format = PageNumber::Format.new(PAGE_NUMBER_FORMAT.merge('id' => 'foo')) assert_equal 'foo', format.id end def test_for_report? format_for_report = PageNumber::Format.new(PAGE_NUMBER_FORMAT) assert_equal true, format_for_report.for_report? format_for_list = PageNumber::Format.new(PAGE_NUMBER_FORMAT.merge('target' => 'target_list_id')) assert_equal false, format_for_list.for_report? end end ================================================ FILE: test/basic_report/units/core/shape/page_number/test_interface.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::Core::Shape::PageNumber::TestInterface < Minitest::Test include Thinreports::BasicReport::TestHelper PageNumber = Thinreports::BasicReport::Core::Shape::PageNumber def create_pageno(format = {}) report = Thinreports::BasicReport::Report.new layout: layout_file.path parent = report.start_new_page PageNumber::Interface.new parent, PageNumber::Format.new(format) end def test_format pageno = create_pageno 'format' => '{page}' assert_equal pageno.format, '{page}' pageno.format('{page} / {total}') assert_equal pageno.format, '{page} / {total}' end def test_reset_format pageno = create_pageno 'format' => '{page}' pageno.format('-- {page} --') pageno.reset_format assert_equal pageno.format, '{page}' end end ================================================ FILE: test/basic_report/units/core/shape/page_number/test_internal.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::Core::Shape::PageNumber::TestInternal < Minitest::Test include Thinreports::BasicReport::TestHelper PageNumber = Thinreports::BasicReport::Core::Shape::PageNumber def setup @report = Thinreports::BasicReport::Report.new layout: layout_file.path @report.start_new_page end def init_pageno(format = {}) PageNumber::Internal.new(@report.page, PageNumber::Format.new(format)) end def test_read_format pageno = init_pageno('format' => 'Page {page}') assert_equal pageno.read_format, 'Page {page}' end def test_write_format pageno = init_pageno('format' => 'Page {page}') pageno.write_format('{page}') assert_equal pageno.read_format, '{page}' end def test_reset_format pageno = init_pageno('format' => '{page}') pageno.write_format('Page {page}') pageno.reset_format assert_equal pageno.read_format, '{page}' end def test_build_format pageno = init_pageno('format' => '{page} / {total}') assert_equal pageno.build_format(1, 100), '1 / 100' pageno.write_format('{page}') assert_equal pageno.build_format(1, 100), '1' @report.start_page_number_from 5 pageno = init_pageno('format' => '{page} / {total}') assert_equal pageno.build_format(1, 100), '5 / 104' # if counted target is a List shape pageno = init_pageno('format' => '{page} / {total}', 'target' => 'list-id') assert_equal pageno.build_format(1, 100), '1 / 100' end def test_type_of pageno = init_pageno assert pageno.type_of?('page-number') end def test_style pageno = init_pageno style = pageno.style assert_instance_of PageNumber::Style, style assert_same pageno.style, style end def test_for_report pageno = init_pageno('target' => '') assert_equal pageno.for_report?, true pageno = init_pageno('target' => 'list-id') assert_equal pageno.for_report?, false end def test_Style_class refute_includes PageNumber::Style.accessible_styles, :valign end end ================================================ FILE: test/basic_report/units/core/shape/styles/test_base.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::Core::Shape::Style::TestBase < Minitest::Test include Thinreports::BasicReport::TestHelper # Alias Style = Thinreports::BasicReport::Core::Shape::Style::Base def create_basic_format(base_style = {}) Thinreports::BasicReport::Core::Shape::Basic::Format.new('style' => base_style) end def create_style(base_style = {}) Style.new(create_basic_format(base_style)) end def create_new_style(base_style = {}, &block) klass = ::Class.new(Style, &block) klass.new(create_basic_format(base_style)) end def test_self_style_reader_should_properly_define_a_reading_method style = create_new_style { style_reader :hoge, 'foo' } assert_respond_to style, :hoge end def test_self_style_writer_should_properly_define_a_writing_method style = create_new_style { style_writer :hoge, 'foo' } assert_respond_to style, :hoge= end def test_self_style_accessor_should_properly_defines_reading_and_writing_methods style = create_new_style { style_accessor :hoge, 'foo' } assert_respond_to style, :hoge assert_respond_to style, :hoge= end def test_self_style_accessible_should_add_specified_styles_to_the_accessible_styles_variable style = create_new_style { style_accessible :hoge style_accessible :foo, :bar } assert_equal style.class.accessible_styles, [:hoge, :foo, :bar] end def test_self_accessible_styles_variable_should_not_be_shared_each_SubClasses style1 = create_new_style { style_accessible :hoge, :foo } assert_equal style1.class.accessible_styles, [:hoge, :foo] end def test_self_accessible_styles_variable_should_be_inherited_to_SubClass super_klass = ::Class.new(Style) { style_accessible :hoge, :foo } sub_klass = ::Class.new(super_klass) assert_equal sub_klass.accessible_styles, super_klass.accessible_styles end def test_self_accessible_styles_variables_of_SubClass_should_not_interfere_mutually super_klass = ::Class.new(Style) { style_accessible :foo, :bar } sub_klass1 = ::Class.new(super_klass) sub_klass2 = ::Class.new(super_klass) refute_same sub_klass1.accessible_styles, sub_klass2.accessible_styles end def test_read_internal_style_should_return_style_of_styles_when_style_is_found_in_styles style = create_style('hoge' => 'base_style') style.write_internal_style('hoge', 'new_style') assert_equal style.read_internal_style('hoge'), 'new_style' end def test_read_internal_style_should_return_style_of_base_when_style_is_not_found_in_styles style = create_style('hoge' => 'base_style') assert_equal style.read_internal_style('hoge'), 'base_style' end def test_write_internal_style_should_properly_set_style_to_the_styles style = create_style({}) style.write_internal_style('hoge', 'hoge_style') assert_equal style.styles['hoge'], 'hoge_style' end def test_has_style_asker_should_return_true_when_specified_style_method_is_accessible style = create_new_style { style_accessible :hoge, :foo } assert_equal style.has_style?(:foo), true end def test_has_style_asker_should_return_false_when_specified_style_method_is_not_accessible style = create_new_style { style_accessible :foo } assert_equal style.has_style?(:hoge), false end def test_verify_style_value_should_raise_when_value_is_not_included_in_list assert_raises ArgumentError do create_style.send(:verify_style_value, :invalid, [:hoge, :foo, :bar]) end end def test_verify_style_value_should_not_raise_when_value_is_found_in_list create_style.send(:verify_style_value, :valid, [:hoge, :valid, :foo]) rescue ArgumentError flunk end def test_reader_method_caller_should_properly_delegate_to_real_method style = create_new_style('hoge_style' => 'hoge_style_value') { style_accessible :hoge style_reader :hoge, 'hoge_style' } assert_equal style[:hoge], 'hoge_style_value' end def test_writer_method_caller_should_properly_delegate_to_real_method style = create_new_style { style_accessible :hoge style_accessor :hoge, 'hoge_style' } style[:hoge] = 'hoge_style_value' assert_equal style.hoge, 'hoge_style_value' end def test_reader_method_caller_should_raise_when_style_is_not_accessible assert_raises Thinreports::BasicReport::Errors::UnknownShapeStyleName do create_new_style[:unknown] end end def test_writer_method_caller_should_raise_when_style_is_not_accessible assert_raises Thinreports::BasicReport::Errors::UnknownShapeStyleName do create_new_style[:unknown] = 'value' end end def test_copy_should_return_the_instance_of_the_same_class_as_itself style = create_style assert_instance_of Style, style.copy end def test_styles_of_copied_style_should_not_same_the_styles_of_original style = create_style refute_same style.styles, style.copy.styles end def test_styles_of_copied_style_should_equal_the_style_of_original style = create_style style.write_internal_style('foo', 'foo_value') assert_equal style.styles['foo'], style.copy.styles['foo'] end def test_identifier_should_return_empty_string_when_the_style_is_not_set style = create_style assert_equal style.identifier, '' end def test_identifier_should_return_the_same_as_hash_value_of_styles_when_style_is_set_something style = create_style style.write_internal_style('foo', 'foo_value') assert_equal style.identifier, style.styles.hash.to_s end def test_finalized_styles base_styles = { 'foo' => 'default foo', 'bar' => 'default bar' } style = create_style(base_styles) style.write_internal_style('foo', 'new foo') assert_equal({ 'foo' => 'new foo', 'bar' => 'default bar' }, style.finalized_styles) assert_same style.finalized_styles, style.finalized_styles end end ================================================ FILE: test/basic_report/units/core/shape/styles/test_basic.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::Core::Shape::Style::TestBasic < Minitest::Test include Thinreports::BasicReport::TestHelper def create_basic_style(format_config = {}) format = Thinreports::BasicReport::Core::Shape::Basic::Format.new(format_config) Thinreports::BasicReport::Core::Shape::Style::Basic.new(format) end def test_visible_should_return_visibility_of_format_as_default style = create_basic_style('display' => false) assert_equal style.visible, false end def test_visible_should_properly_set_visibility style = create_basic_style('display' => false) style.visible = true assert_equal style.visible, true end end ================================================ FILE: test/basic_report/units/core/shape/styles/test_graphic.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::Core::Shape::Style::TestGraphic < Minitest::Test include Thinreports::BasicReport::TestHelper def test_border_color style = create_graphic_style('border-color' => 'red') assert_equal 'red', style.border_color style.border_color = '#ff0000' assert_equal '#ff0000', style.styles['border-color'] assert_equal '#ff0000', style.border_color end def test_border_width style = create_graphic_style('border-width' => 2.0) assert_equal 2.0, style.border_width style.border_width = 10.9 assert_equal 10.9, style.styles['border-width'] assert_equal 10.9, style.border_width end def test_fill_color style = create_graphic_style('fill-color' => '#0000ff') assert_equal '#0000ff', style.fill_color style.fill_color = 'blue' assert_equal 'blue', style.styles['fill-color'] assert_equal 'blue', style.fill_color end def test_border style = create_graphic_style('border-color' => 'red', 'border-width' => 1) assert_equal [1, 'red'], style.border style.border = [2.0, '#ff0000'] assert_equal 2.0, style.styles['border-width'] assert_equal '#ff0000', style.styles['border-color'] end private def create_graphic_style(default_style = {}) format = Thinreports::BasicReport::Core::Shape::Basic::Format.new('style' => default_style) Thinreports::BasicReport::Core::Shape::Style::Graphic.new(format) end end ================================================ FILE: test/basic_report/units/core/shape/styles/test_text.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::Core::Shape::Style::TestText < Minitest::Test include Thinreports::BasicReport::TestHelper def test_color style = create_text_style('color' => 'red') assert_equal 'red', style.color style.color = 'blue' assert_equal 'blue', style.styles['color'] assert_equal 'blue', style.color end def test_font_size style = create_text_style('font-size' => 18.0) assert_equal 18.0, style.font_size style.font_size = 19 assert_equal 19, style.styles['font-size'] assert_equal 19, style.font_size end def test_initialize_font_style default_font_style = ['bold', 'italic'] style = create_text_style('font-style' => default_font_style) refute_same default_font_style, style.styles['font-style'] assert_equal default_font_style, style.styles['font-style'] end def test_bold style = create_text_style('font-style' => ['bold']) assert_equal true, style.bold style.bold = false assert_equal [], style.styles['font-style'] assert_equal false, style.bold end def test_italic style = create_text_style('font-style' => ['italic']) assert_equal true, style.italic style.italic = false assert_equal [], style.styles['font-style'] assert_equal false, style.italic end def test_underline style = create_text_style('font-style' => ['underline']) assert_equal true, style.underline style.underline = false assert_equal [], style.styles['font-style'] assert_equal false, style.underline end def test_linethrough style = create_text_style('font-style' => ['linethrough']) assert_equal true, style.linethrough style.linethrough = false assert_equal [], style.styles['font-style'] assert_equal false, style.linethrough end def test_align style = create_text_style('text-align' => 'center') assert_equal :center, style.align style.align = :right assert_equal 'right', style.styles['text-align'] assert_equal :right, style.align end def test_valign style = create_text_style('vertical-align' => '') assert_equal :top, style.valign style.valign = :middle assert_equal 'middle', style.styles['vertical-align'] assert_equal :middle, style.valign assert_deprecated { style.valign = :center } assert_equal :middle, style.valign end private def create_text_style(default_style = {}) format = Thinreports::BasicReport::Core::Shape::Text::Format.new('style' => default_style) Thinreports::BasicReport::Core::Shape::Style::Text.new(format) end end ================================================ FILE: test/basic_report/units/core/shape/text/test_format.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::Core::Shape::Text::TestFormat < Minitest::Test include Thinreports::BasicReport::TestHelper TEXT_FORMAT = { 'id' => 'text_1', 'type' => 'text', 'x' => 100.0, 'y' => 200.0, 'width' => 300.0, 'height' => 400.0, 'description' => 'Description for item', 'display' => true, 'texts' => [ '1st text line', '2nd text line' ], 'valign' => 'top', 'style' => { 'font-family' => ['Arial'], 'font-size' => 12, 'color' => '#000000', 'font-style' => ['bold', 'italic', 'linethrough', 'underline'], 'text-align' => 'left', 'vertical-align' => 'top', 'line-height' => 60, 'line-height-ratio' => 1.5, 'letter-spacing' => 'normal' } } Text = Thinreports::BasicReport::Core::Shape::Text def test_attribute_readers format = Text::Format.new(TEXT_FORMAT) assert_equal TEXT_FORMAT['texts'], format.texts assert_equal 'top', format.valign assert_equal 60, format.line_height end end ================================================ FILE: test/basic_report/units/core/shape/text/test_internal.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::Core::Shape::Text::TestInternal < Minitest::Test include Thinreports::BasicReport::TestHelper Text = Thinreports::BasicReport::Core::Shape::Text def create_internal(format_config = {}) report = Thinreports::BasicReport::Report.new layout: layout_file.path Text::Internal.new(report.start_new_page, Text::Format.new(format_config)) end def test_type_of? assert create_internal.type_of?('text'), true refute create_internal.type_of?(:basic), false end end ================================================ FILE: test/basic_report/units/core/shape/text_block/formatter/test_basic.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::Core::Shape::TextBlock::Formatter::TestBasic < Minitest::Test include Thinreports::BasicReport::TestHelper # Alias Formatter = Thinreports::BasicReport::Core::Shape::TextBlock::Formatter::Basic def test_apply_simple_format format = stub(format_base: 'Hello {value}!') assert_equal Formatter.new(format).apply('Thinreports'), 'Hello Thinreports!' end def test_apply_multiple_format format = stub(format_base: 'Hello {value}! And good bye {value}.') assert_equal Formatter.new(format).apply('Thinreports'), 'Hello Thinreports! And good bye Thinreports.' end end ================================================ FILE: test/basic_report/units/core/shape/text_block/formatter/test_datetime.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::Core::Shape::TextBlock::Formatter::TestDatetime < Minitest::Test include Thinreports::BasicReport::TestHelper # Aliases TextBlock = Thinreports::BasicReport::Core::Shape::TextBlock Formatter = TextBlock::Formatter::Datetime def setup @datetime_format = '%Y/%m/%d %H:%M:%S' @time = Time.now end def text_block_format(format = {}) default = {'base' => '', 'datetime' => {'format' => '%Y/%m/%d %H:%M:%S'}} TextBlock::Format.new('format' => default.merge(format)) end def test_apply_datetime_format_without_basic_format formatter = Formatter.new(text_block_format) assert_equal @time.strftime(@datetime_format), formatter.apply(@time) end def test_apply_datetime_format_with_basic_format formatter = Formatter.new(text_block_format('base' => 'Now: {value}')) assert_equal "Now: #{@time.strftime(@datetime_format)}", formatter.apply(@time) end def test_not_apply_datetime_format_and_return_raw_value # When value is invalid formatter = Formatter.new(text_block_format) assert_same formatter.apply(val = 'invalid value'), val assert_same formatter.apply(val = 123456), val # When format is empty formatter = Formatter.new(text_block_format('datetime' => {'format' => ''})) assert_same formatter.apply(@time), @time end end ================================================ FILE: test/basic_report/units/core/shape/text_block/formatter/test_number.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::Core::Shape::TextBlock::Formatter::TestNumber < Minitest::Test include Thinreports::BasicReport::TestHelper # Alias Formatter = Thinreports::BasicReport::Core::Shape::TextBlock::Formatter::Number def init_formatter(expect_formats) format = stub({ format_base: '', format_number_precision: nil, format_number_delimiter: nil }.merge(expect_formats)) Formatter.new(format) end def test_apply_precision_formats # When precision is 2 formatter = init_formatter(format_number_precision: 2) assert_equal formatter.apply(1.005), '1.01' assert_equal formatter.apply(1.004), '1.00' assert_equal formatter.apply(1), '1.00' # With String value assert_equal formatter.apply('999.999'), '1000.00' assert_equal formatter.apply('-999.999'), '-1000.00' assert_equal formatter.apply('9'), '9.00' # Cannot apply, Return the raw value assert_equal formatter.apply('invalid value'), 'invalid value' # When precision is 0 formatter = init_formatter(format_number_precision: 0) assert_equal formatter.apply(1.5), '2' end def test_apply_precision_format_with_basic_format formatter = init_formatter(format_base: '$ {value}', format_number_precision: 0) assert_equal formatter.apply(199.5), '$ 200' end def test_apply_delimiter_formats # When delimiter is ',' formatter = init_formatter(format_number_delimiter: ',') assert_equal formatter.apply(1000000), '1,000,000' assert_equal formatter.apply(-1000000), '-1,000,000' assert_equal formatter.apply('1000.0'), '1,000.0' assert_equal formatter.apply('-1000.0'), '-1,000.0' assert_equal '1,000.1111', formatter.apply(1000.1111) # Cannot apply, Return the raw value assert_equal formatter.apply('invalid value'), 'invalid value' # When delimiter is whitespace formatter = init_formatter(format_number_delimiter: ' ') assert_equal formatter.apply(99999), '99 999' end def test_apply_delimiter_format_with_basic_format formatter = init_formatter(format_base: '¥{value}', format_number_delimiter: ',') assert_equal formatter.apply(199800), '¥199,800' end def test_apply_all_format formatter = init_formatter(format_base: '-- {value} --', format_number_delimiter: ',', format_number_precision: 2) assert_equal formatter.apply(95618.88567), '-- 95,618.89 --' end end ================================================ FILE: test/basic_report/units/core/shape/text_block/formatter/test_padding.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::Core::Shape::TextBlock::Formatter::TestPadding < Minitest::Test include Thinreports::BasicReport::TestHelper # Alias Formatter = Thinreports::BasicReport::Core::Shape::TextBlock::Formatter::Padding def init_formatter(expect_formats) format = stub({ format_base: nil, format_padding_length: 0, format_padding_char: nil, format_padding_rdir?: false }.merge(expect_formats)) Formatter.new(format) end def test_apply_padding_formats_with_left_direction formatter = init_formatter(format_padding_length: 5, format_padding_char: '0') assert_equal formatter.apply(1), '00001' assert_equal formatter.apply('日本語'), '00日本語' end def test_apply_padding_formats_should_not_apply_when_character_length_is_short formatter = init_formatter(format_padding_length: 5, format_padding_char: '0') assert_equal formatter.apply('1234567'), '1234567' end def test_apply_padding_formats_with_right_direction formatter = init_formatter(format_padding_length: 5, format_padding_char: '0', :format_padding_rdir? => true) assert_equal formatter.apply(123), '12300' end def test_apply_padding_format_with_basic_format formatter = init_formatter(format_base: '[{value}]', format_padding_length: 10, format_padding_char: ' ') assert_equal formatter.apply('ABC'), '[ ABC]' end def test_return_raw_value_when_length_is_0 formatter = init_formatter(format_padding_length: 0, format_padding_char: '0') assert_same formatter.apply(v = 123), v # But apply only basic format if have basic-format. formatter = init_formatter(format_base: '<{value}>', format_padding_length: 0, format_padding_char: '0') assert_equal formatter.apply(123), '<123>' end def test_return_raw_value_when_char_is_empty formatter = init_formatter(format_padding_length: 10, format_padding_char: '') assert_same formatter.apply(v = '1'), v # But apply only basic format if have basic-format. formatter = init_formatter(format_base: '<{value}>', format_padding_length: 0, format_padding_char: '0') assert_equal formatter.apply('1'), '<1>' end end ================================================ FILE: test/basic_report/units/core/shape/text_block/test_format.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::Core::Shape::TextBlock::TestFormat < Minitest::Test include Thinreports::BasicReport::TestHelper TEXT_BLOCK_FORMAT = { 'id' => 'text_block', 'reference-id' => 'referenced_text_block', 'type' => 'text-block', 'display' => true, 'multiple-line' => false, 'x' => 100.0, 'y' => 200.0, 'width' => 300.0, 'height' => 400.0, 'value' => 'default value', 'format' => { 'base' => 'Price: {value}', 'type' => 'number', 'number' => { 'delimiter' => ',', 'precision' => 1 } }, 'description' => 'Description for item', 'style' => { 'word-wrap' => 'break-word', 'overflow' => 'truncate', 'text-align' => 'right', 'vertical-align' => 'middle', 'color' => '#000000', 'font-size' => 12, 'font-family' => ['Helvetica'], 'font-style' => ['bold', 'italic', 'linethrough', 'underline'], 'letter-spacing' => 'normal', 'line-height' => 30, 'line-height-ratio' => 1.5 } } TextBlock = Thinreports::BasicReport::Core::Shape::TextBlock def test_attribute_readers format = TextBlock::Format.new(TEXT_BLOCK_FORMAT) assert_equal 'referenced_text_block', format.ref_id assert_equal 'middle', format.valign assert_equal 'truncate', format.overflow assert_equal 30, format.line_height assert_equal false, format.multiple? assert_equal 'Price: {value}', format.format_base assert_equal 'number', format.format_type end def test_has_reference? format_has_reference = TextBlock::Format.new(TEXT_BLOCK_FORMAT.merge('reference-id' => 'other')) assert_equal true, format_has_reference.has_reference? format_has_no_reference = TextBlock::Format.new(TEXT_BLOCK_FORMAT.merge('reference-id' => '')) assert_equal false, format_has_no_reference.has_reference? end def test_has_format? format_has_text_format = TextBlock::Format.new(TEXT_BLOCK_FORMAT.merge( 'format' => { 'type' => 'datetime' } )) assert_equal true, format_has_text_format.has_format? format_has_no_text_format = TextBlock::Format.new(TEXT_BLOCK_FORMAT.merge( 'format' => { 'type' => '' } )) assert_equal false, format_has_no_text_format.has_format? end def test_number_format_attribute_readers format = TextBlock::Format.new(TEXT_BLOCK_FORMAT.merge( 'format' => { 'type' => 'number', 'number' => { 'delimiter' => ',', 'precision' => 1 } } )) assert_equal ',', format.format_number_delimiter assert_equal 1, format.format_number_precision end def test_datetime_format_attribute_readers format = TextBlock::Format.new(TEXT_BLOCK_FORMAT.merge( 'format' => { 'type' => 'datetime', 'datetime' => { 'format' => '%Y-%m-%d' } } )) assert_equal '%Y-%m-%d', format.format_datetime_format end def test_padding_format_attribute_readers format = TextBlock::Format.new(TEXT_BLOCK_FORMAT.merge( 'format' => { 'type' => 'padding', 'padding' => { 'char' => '0', 'length' => 10, 'direction' => 'L' } } )) assert_equal '0', format.format_padding_char assert_equal 'L', format.format_padding_dir end def test_format_padding_length format = TextBlock::Format.new(TEXT_BLOCK_FORMAT.merge( 'format' => { 'type' => 'padding', 'padding' => { 'char' => '0', 'length' => 10, 'direction' => 'L' } } )) assert_equal 10, format.format_padding_length format = TextBlock::Format.new(TEXT_BLOCK_FORMAT.merge( 'format' => { 'type' => 'padding', 'padding' => { 'char' => '0', 'length' => '', 'direction' => 'L' } } )) assert_equal 0, format.format_padding_length end def test_format_padding_rdir? format_direction_left = TextBlock::Format.new(TEXT_BLOCK_FORMAT.merge( 'format' => { 'type' => 'padding', 'padding' => { 'char' => '0', 'length' => 10, 'direction' => 'L' } } )) assert_equal false, format_direction_left.format_padding_rdir? format_direction_right = TextBlock::Format.new(TEXT_BLOCK_FORMAT.merge( 'format' => { 'type' => 'padding', 'padding' => { 'char' => '0', 'length' => 10, 'direction' => 'R' } } )) assert_equal true, format_direction_right.format_padding_rdir? end end ================================================ FILE: test/basic_report/units/core/shape/text_block/test_formatter.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::Core::Shape::TextBlock::TestFormatter < Minitest::Test include Thinreports::BasicReport::TestHelper # Alias Formatter = Thinreports::BasicReport::Core::Shape::TextBlock::Formatter def test_initialize_formatter_by_type assert_instance_of Formatter::Basic, Formatter.setup( stub(format_type: '') ) assert_instance_of Formatter::Number, Formatter.setup( stub(format_type: 'number') ) assert_instance_of Formatter::Datetime, Formatter.setup( stub(format_type: 'datetime') ) assert_instance_of Formatter::Padding, Formatter.setup( stub(format_type: 'padding') ) assert_raises Thinreports::BasicReport::Errors::UnknownFormatterType do Formatter.setup( stub(format_type: 'unknown') ) end end end ================================================ FILE: test/basic_report/units/core/shape/text_block/test_interface.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::Core::Shape::TextBlock::TestInterface < Minitest::Test include Thinreports::BasicReport::TestHelper # Alias TextBlock = Thinreports::BasicReport::Core::Shape::TextBlock def create_interface(format_config = {}) report = Thinreports::BasicReport::Report.new layout: layout_file.path parent = report.start_new_page TextBlock::Interface.new parent, TextBlock::Format.new(format_config) end def test_format_enabled_asker_should_operate_as_delegator_of_internal tblock = create_interface('format' => {'type' => 'datetime'}) assert_equal tblock.format_enabled?, tblock.internal.format_enabled? end def test_format_enabled_should_properly_set_value_to_internal tblock = create_interface('format' => {'type' => 'number'}) tblock.format_enabled(false) assert_equal tblock.internal.format_enabled?, false end def test_set_should_properly_set_a_value tblock = create_interface tblock.set(1000, visible: false) assert_equal tblock.value, 1000 end def test_set_should_properly_set_styles tblock = create_interface tblock.set(1000, color: '#ff0000', bold: true, italic: true) assert_equal [tblock.style(:color), tblock.style(:bold), tblock.style(:italic)], ['#ff0000', true, true] end def test_value= report = Thinreports::BasicReport::Report.new layout: layout_file.path page = report.start_new_page page.item(:text_block).value = 'foo' assert_equal 'foo', page.item(:text_block).value page.item(:text_block).value += 'bar' assert_equal 'foobar', page.item(:text_block).value page.item(:text_block).value = 1000 page.item(:text_block).value += 999 assert_equal 1999, page.item(:text_block).value end end ================================================ FILE: test/basic_report/units/core/shape/text_block/test_internal.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::Core::Shape::TextBlock::TestInternal < Minitest::Test include Thinreports::BasicReport::TestHelper # Alias TextBlock = Thinreports::BasicReport::Core::Shape::TextBlock def create_parent report = Thinreports::BasicReport::Report.new layout: layout_file.path report.start_new_page do |page| # Add to force TextBlock shape. page.manager.format.shapes[:foo] = TextBlock::Format.new('type' => 'text-block', 'id' => 'foo') # Set value to TextBlock. page.item(:foo).value('foo value') end end def create_internal(format_config = {}) TextBlock::Internal.new(create_parent, TextBlock::Format.new(format_config)) end def test_multiple_asker_should_operate_as_delegator_of_format tblock = create_internal('multiple-line' => true) assert_equal tblock.multiple?, tblock.format.multiple? end def test_style_should_return_the_instance_of_Style_Text assert_instance_of Thinreports::BasicReport::Core::Shape::Style::Text, create_internal.style end def test_read_value_should_return_the_format_value_as_default tblock = create_internal('value' => 'default value') assert_equal tblock.read_value, 'default value' end def test_read_value_should_return_the_value_of_referenced_shape tblock = create_internal('reference-id' => 'foo', 'value' => '') assert_equal tblock.read_value, 'foo value' end def test_read_value_should_return_the_value_stored_in_states tblock = create_internal tblock.states[:value] = 'value in states' assert_equal tblock.read_value, 'value in states' end def test_write_value_should_properly_set_the_specified_value_to_states tblock = create_internal tblock.write_value(1000) assert_equal tblock.states[:value], 1000 end def test_write_value_should_show_warnings_when_tblock_has_reference tblock = create_internal('id' => 'bar', 'reference-id' => 'foo') _out, err = capture_io do tblock.write_value('value') end assert_equal err.chomp, 'The set value was not saved, ' + "Because 'bar' has reference to 'foo'." end def test_real_value_should_return_the_formatted_value_when_tblock_has_any_format tblock = create_internal('format' => {'type' => 'datetime', 'datetime' => {'format' => '%Y/%m/%d'}}) tblock.write_value(value = Time.now) assert_equal tblock.real_value, value.strftime('%Y/%m/%d') end def test_real_value_should_return_the_raw_value_when_tblock_has_no_format tblock = create_internal tblock.write_value('any value') assert_equal tblock.real_value, 'any value' end def test_format_enabled_should_properly_set_value_to_states_as_format_enabled tblock = create_internal tblock.format_enabled(false) assert_equal tblock.states[:format_enabled], false end def test_format_enabled_asker_should_return_true_when_format_has_any_type tblock = create_internal('format' => {'type' => 'datetime'}) assert_equal tblock.format_enabled?, true end def test_format_enabled_asker_should_return_true_when_base_of_format_has_any_value tblock = create_internal('format' => {'base' => '{value}'}) assert_equal tblock.format_enabled?, true end def test_format_enabled_asker_should_return_false_when_format_has_no_type_and_base_has_not_value assert_equal create_internal.format_enabled?, false end def test_format_enabled_asker_should_return_true_constantly_when_tblock_is_multiple_mode tblock = create_internal('multiple-line' => true, 'format' => {'base' => '{value}', 'type' => 'number'}) tblock.format_enabled(true) assert tblock.format_enabled? end def test_type_of_asker_should_return_true_when_value_is_tblock assert_equal create_internal.type_of?('text-block'), true end def test_type_of_asker_should_return_true_when_value_is_block assert_equal create_internal.type_of?(:block), true end def test_formatter_should_return_instance_of_FormatterBasic_when_format_is_enable tblock = create_internal('format' => {'type' => 'datetime'}) assert_kind_of TextBlock::Formatter::Basic, tblock.send(:formatter) end end ================================================ FILE: test/basic_report/units/core/test_shape.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::Core::TestShape < Minitest::Test include Thinreports::BasicReport::TestHelper # Alias ShapeModule = Thinreports::BasicReport::Core::Shape def test_find_by_type_should_return_PageNumber assert_same ShapeModule.find_by_type('page-number'), ShapeModule::PageNumber end def test_find_by_type_should_return_ImageBlock assert_same ShapeModule.find_by_type('image-block'), ShapeModule::ImageBlock end def test_find_by_type_should_return_TextBlock assert_same ShapeModule.find_by_type('text-block'), ShapeModule::TextBlock end def test_find_by_type_should_return_List assert_same ShapeModule.find_by_type('list'), ShapeModule::List end def test_find_by_type_should_return_Text assert_same ShapeModule.find_by_type('text'), ShapeModule::Text end def test_find_by_type_should_return_Basic_as_Image assert_same ShapeModule.find_by_type('image'), ShapeModule::Basic end def test_find_by_type_should_return_Basic_as_Line assert_same ShapeModule.find_by_type('line'), ShapeModule::Basic end def test_find_by_type_should_return_Basic_as_Rect assert_same ShapeModule.find_by_type('rect'), ShapeModule::Basic end def test_find_by_type_should_return_Basic_as_Ellipse assert_same ShapeModule.find_by_type('ellipse'), ShapeModule::Basic end def test_find_by_type_should_raise assert_raises Thinreports::BasicReport::Errors::UnknownShapeType do ShapeModule.find_by_type('unknown') end end end ================================================ FILE: test/basic_report/units/core/test_utils.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::Core::TestShape < Minitest::Test include Thinreports::BasicReport::TestHelper include Thinreports::BasicReport::Utils def test_call_block_in expected = '123' assert_same expected, call_block_in(expected) assert_equal [1, 2, 3], call_block_in([2, 1, 3], &proc { sort! }) assert_equal [1, 2, 3], call_block_in([2, 1, 3], &proc { |a| a.sort! }) end def test_deep_copy_in_unsupported_object [123, nil, Struct.new(:foo).new].each do |unsupported_value| assert_raises ArgumentError do deep_copy(unsupported_value) end end end def test_deep_copy_in_Array src = ['string', Time.now] dup = deep_copy(src) refute_same dup, src src.each_with_index do |e, i| assert_equal dup[i], e refute_same dup[i], e end end def test_deep_copy_in_Hash src = { a: 'string', b: Time.now } dup = deep_copy(src) refute_same dup, src src.each do |k, v| assert_equal dup[k], v refute_same dup[k], v end end def test_blank_value_in_String ["", ''].each do |val| assert_equal true, blank_value?(val) end [' ', ' ', 'abc', 'あいう'].each do |val| assert_equal false, blank_value?(val) end end def test_blank_value_in_NilClass assert_equal true, blank_value?(nil) end def test_blank_value_in_other_classes [0, 1, -1, 9.99, true, false, Time.now].each do |val| assert_equal false, blank_value?(val) end end end ================================================ FILE: test/basic_report/units/generator/pdf/document/graphics/test_attributes.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::Generator::PDF::Graphics::TestAttributes < Minitest::Test include Thinreports::BasicReport::TestHelper def setup @pdf = Thinreports::BasicReport::Generator::PDF::Document.new end def test_build_graphic_attributes graphic_styles = { 'border-color' => '#ff0000', 'border-width' => 2.0, 'border-style' => 'solid', 'fill-color' => '#000000' } assert_equal( { stroke: '#ff0000', stroke_width: 2.0, stroke_type: 'solid', fill: '#000000' }, @pdf.build_graphic_attributes(graphic_styles) ) customized_attributes = @pdf.build_graphic_attributes(graphic_styles) { |attrs| attrs[:stroke] = 'blue' } assert_equal 'blue', customized_attributes[:stroke] end def test_build_text_attributes text_styles_without_overflow_wrap = { 'font-family' => %w( Helvetica IPAMincho ), 'font-size' => 18.0, 'color' => 'red', 'text-align' => 'right', 'vertical-align' => 'bottom', 'font-style' => %w( bold italic ), 'letter-spacing' => 2.0, 'line-height' => 20.0, 'overflow' => 'expand', 'word-wrap' => 'break-word' } assert_equal( { font: 'Helvetica', size: 18.0, color: 'red', align: :right, valign: :bottom, styles: [:bold, :italic], letter_spacing: 2.0, line_height: 20.0, overflow: :expand, word_wrap: :break_word, overflow_wrap: :normal }, @pdf.build_text_attributes(text_styles_without_overflow_wrap) ) text_styles_with_overflow_wrap = text_styles_without_overflow_wrap.merge( 'overflow-wrap' => 'anywhere' ) assert_equal( { font: 'Helvetica', size: 18.0, color: 'red', align: :right, valign: :bottom, styles: [:bold, :italic], letter_spacing: 2.0, line_height: 20.0, overflow: :expand, word_wrap: :break_word, overflow_wrap: :anywhere }, @pdf.build_text_attributes(text_styles_with_overflow_wrap) ) customized_attributes = @pdf.build_text_attributes(text_styles_without_overflow_wrap) { |attrs| attrs[:color] = 'blue' } assert_equal 'blue', customized_attributes[:color] end def test_font_family assert_equal 'IPAGothic', @pdf.font_family(%w( IPAGothic Helvetica )) assert_equal 'Helvetica', @pdf.font_family(%w( Unknown IPAMincho )) end def test_font_styles assert_equal [:bold, :italic, :underline, :strikethrough], @pdf.font_styles(%w( bold italic underline linethrough )) end def test_letter_spacing assert_equal 2.0, @pdf.letter_spacing(2.0) assert_nil @pdf.letter_spacing('') assert_nil @pdf.letter_spacing(nil) end def test_text_align assert_equal :left, @pdf.text_align('left') assert_equal :center, @pdf.text_align('center') assert_equal :right, @pdf.text_align('right') assert_equal :left, @pdf.text_align('') assert_equal :left, @pdf.text_align(nil) end def test_text_valign assert_equal :top, @pdf.text_valign('top') assert_equal :center, @pdf.text_valign('middle') assert_equal :bottom, @pdf.text_valign('bottom') assert_equal :top, @pdf.text_valign('') assert_equal :top, @pdf.text_valign(nil) end def test_text_overflow assert_equal :truncate, @pdf.text_overflow('truncate') assert_equal :shrink_to_fit, @pdf.text_overflow('fit') assert_equal :expand, @pdf.text_overflow('expand') assert_equal :truncate, @pdf.text_overflow('') assert_equal :truncate, @pdf.text_overflow(nil) end def test_word_wrap assert_equal :break_word, @pdf.word_wrap('break-word') assert_equal :none, @pdf.word_wrap('none') assert_equal :none, @pdf.word_wrap(nil) end def test_line_height assert_equal 20.9, @pdf.line_height(20.9) assert_nil @pdf.line_height('') assert_nil @pdf.line_height(nil) end def test_image_position_x assert_equal :left, @pdf.image_position_x('left') assert_equal :center, @pdf.image_position_x('center') assert_equal :right, @pdf.image_position_x('right') assert_equal :left, @pdf.image_position_x('') assert_equal :left, @pdf.image_position_x(nil) end def test_image_position_y assert_equal :top, @pdf.image_position_y('top') assert_equal :center, @pdf.image_position_y('middle') assert_equal :bottom, @pdf.image_position_y('bottom') assert_equal :top, @pdf.image_position_y('') assert_equal :top, @pdf.image_position_y(nil) end def test_overflow_wrap assert_equal :anywhere, @pdf.overflow_wrap('anywhere', :none) assert_equal :disable_break_word_by_space, @pdf.overflow_wrap('disable-break-word-by-space', :break_word) assert_equal :normal, @pdf.overflow_wrap('normal', :none) # When value is unexpected value assert_equal :normal, @pdf.overflow_wrap('', :none) end def text_overflow_wrap_fallback_to_word_wrap # word_wrap: none fallbacks to :disable_break_word_by_space assert_equal :disable_break_word_by_space, @pdf.overflow_wrap(nil, :none) # word_wrap: break_word fallbacks to :normal assert_equal :normal, @pdf.overflow_wrap(nil, :break_word) end def test_migrate_overflow_wrap_from_word_wrap assert_equal 'normal', @pdf.migrate_overflow_wrap_from_word_wrap(:break_word) assert_equal 'disable-break-word-by-space', @pdf.migrate_overflow_wrap_from_word_wrap(:none) end end ================================================ FILE: test/basic_report/units/generator/pdf/document/graphics/test_basic.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::Generator::PDF::Graphics::TestBasic < Minitest::Test include Thinreports::BasicReport::TestHelper def setup @pdf = Thinreports::BasicReport::Generator::PDF::Document.new end def test_build_stroke_styles style = { stroke: 'red', stroke_width: 2.0, stroke_type: 'solid' } assert_equal( { color: 'ff0000', width: 2.0, dash: nil }, @pdf.build_stroke_styles(style) ) style_stroke_dashed = style.merge(stroke_type: 'dashed') assert_equal [2, 2], @pdf.build_stroke_styles(style_stroke_dashed)[:dash] style_stroke_dotted = style.merge(stroke_type: 'dotted') assert_equal [1, 2], @pdf.build_stroke_styles(style_stroke_dotted)[:dash] assert_nil @pdf.build_stroke_styles(stroke: nil, stroke_width: 1) assert_nil @pdf.build_stroke_styles(stroke: 'none', stroke_width: 1) assert_nil @pdf.build_stroke_styles(stroke_width: nil, stroke: 'red') assert_nil @pdf.build_stroke_styles(stroke_width: 0, stroke: 'red') end def test_build_fill_styles assert_equal({ color: 'ff0000' }, @pdf.build_fill_styles(fill: 'red')) assert_nil @pdf.build_fill_styles(fill: nil) assert_nil @pdf.build_fill_styles(fill: 'none') end end ================================================ FILE: test/basic_report/units/generator/pdf/document/graphics/test_image.rb ================================================ # frozen_string_literal: true require 'test_helper' require 'base64' class Thinreports::BasicReport::Generator::PDF::Graphics::TestImage < Minitest::Test include Thinreports::BasicReport::TestHelper def setup format = Thinreports::BasicReport::Layout::Format.build(self.layout_file.path) @document = Thinreports::BasicReport::Generator::PDF::Document.new.tap { |doc| doc.start_new_page(format) } end def test_image each_image do |image_filename| @document.image(data_file(image_filename), 0, 0, 100, 100) @document.image(StringIO.new(read_data_file(image_filename)), 0, 100, 100, 100) end assert_equal 6, analyze_pdf_images(@document.render).count end def test_base64image each_image do |image_filename| @document.base64image(Base64.encode64(read_data_file(image_filename)), 0, 0, 100, 100) end assert_equal 3, analyze_pdf_images(@document.render).count end def test_image_box each_image do |image_filename| @document.image_box(data_file(image_filename), 0, 0, 100, 100) @document.image(StringIO.new(read_data_file(image_filename)), 0, 100, 100, 100) end assert_equal 6, analyze_pdf_images(@document.render).count end def test_clean_temp_images @document.base64image(Base64.encode64(read_data_file('image_normal.png')), 0, 0, 100, 100) @document.base64image(Base64.encode64(read_data_file('image_normal.jpg')), 0, 0, 100, 100) assert_equal 2, @document.temp_image_registry.size image_file_and_paths = @document.temp_image_registry.values.map { |image| [image, image.path] } @document.clean_temp_images assert_empty @document.temp_image_registry image_file_and_paths.each do |file, path| assert_nil file.path refute File.exist?(path) end end def each_image(&block) %w( image_normal.png image_normal.jpg image_pallete_based.png ).each { |image_filename| block.call(image_filename) } end end ================================================ FILE: test/basic_report/units/generator/pdf/document/graphics/test_text.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::Generator::PDF::Graphics::TestText < Minitest::Test include Thinreports::BasicReport::TestHelper def setup @pdf = Thinreports::BasicReport::Generator::PDF::Document.new @pdf.internal.start_new_page end def exec_with_font_styles(attrs = nil, font = nil, &block) attrs ||= {styles: [:bold]} font ||= {name: 'IPAMincho', size: 18, color: 'ff0000'} @pdf.send(:with_font_styles, attrs, font, &block) end def exec_with_text_styles(attrs = {}, &block) default_attrs = {font: 'Helvetica', color: 'ff0000', size: 18} @pdf.send(:with_text_styles, default_attrs.merge(attrs), &block) end def test_with_text_styles_should_not_operate_when_color_is_none exec_with_text_styles(color: 'none') do |_attrs, _styles| flunk end end def test_with_text_styles_should_set_leading_via_line_height_attribute exec_with_text_styles(line_height: 30) do |attrs, _styles| expected = @pdf.send(:text_line_leading, @pdf.send(:s2f, 30), name: 'Helvetica', size: 18) assert_equal attrs[:leading], expected end end def test_with_text_styles_should_not_set_leading_when_line_height_is_not_specified exec_with_text_styles do |attrs, _styles| refute_includes attrs.keys, :leading end end def test_with_text_styles_should_set_character_spacing_via_letter_spacing_attribute exec_with_text_styles(letter_spacing: 5) do |attrs, _styles| assert_equal attrs[:character_spacing], @pdf.send(:s2f, 5) end end def test_with_text_styles_should_not_set_character_spacing_when_letter_spacing_is_not_specified exec_with_text_styles do |attrs, _styles| refute_includes attrs.keys, :character_spacing end end def test_with_text_styles_should_parse_color exec_with_text_styles(color: '#ff0000') do |_attrs, _styles| assert_equal @pdf.internal.fill_color, 'ff0000' end end def test_with_font_styles_should_set_fill_color_using_color_of_font exec_with_font_styles do |_attrs, _styles| assert_equal @pdf.internal.fill_color, 'ff0000' end end def test_with_font_styles_should_perform_manual_style_when_bold_style_cannot_be_applied exec_with_font_styles do |_attrs, styles| assert_empty styles end end def test_with_font_styles_should_perform_manual_style_when_italic_style_cannot_be_applied exec_with_font_styles do |_attrs, styles| assert_empty styles end end def test_with_font_styles_should_set_stroke_color_using_color_of_font_when_bold_style_cannot_be_applied exec_with_font_styles do |_attrs, _styles| assert_equal @pdf.internal.stroke_color, 'ff0000' end end def test_with_font_styles_should_set_line_width_calculated_from_font_size_when_bold_style_cannot_be_applied exec_with_font_styles do |_attrs, _styles| assert_equal @pdf.internal.line_width, 18 * 0.025 end end def test_with_font_styles_should_set_mode_to_fill_stroke_when_bold_style_cannot_be_applied exec_with_font_styles do |attrs, _styles| assert_equal attrs[:mode], :fill_stroke end end def test_with_font_styles_should_not_perform_a_manual_style_when_bold_style_can_be_applied exec_with_font_styles(nil, name: 'Helvetica', size: 12, color: '0000ff') do |_attrs, styles| assert_includes styles, :bold end end def test_with_font_styles_should_not_perform_a_manual_style_when_italic_style_can_be_applied exec_with_font_styles({styles: [:italic]}, name: 'Helvetica', size: 12, color: 'ff0000') do |_attrs, styles| assert_includes styles, :italic end end def test_text_line_leading_should_return_a_specified_leading_value_minus_the_font_height font = {name: 'IPAMincho', size: 36} font_height = @pdf.internal.font(font[:name], size: font[:size]).height assert_equal @pdf.send(:text_line_leading, 100, font), 100 - font_height end def test_replace_space_to_nbsp_should_replace_the_spaces_NBSP assert_equal @pdf.send(:replace_space_to_nbsp, ' ' * 2), Prawn::Text::NBSP * 2 end def test_text_box_should_not_raise_PrawnCannotFitError @pdf.text_box('foo', 0, 0, 1, 1, font: 'IPAMincho', size: 100, color: '000000') rescue Prawn::Errors::CannotFit flunk('Raise Prawn::Errors::CannotFit.') end def test_text_box_attrs_should_return_a_Hash_containing_a_at_and_width_options attrs = @pdf.send(:text_box_attrs, 0, 0, 50, 100) assert_equal attrs.values_at(:at, :width), [@pdf.send(:pos, 0, 0), @pdf.send(:s2f, 50)] end def test_text_box_attrs_should_return_a_Hash_which_doesnt_contain_the_single_line_option_when_single_is_true_but_overflow_is_expand attrs = @pdf.send(:text_box_attrs, 0, 0, 100, 100, single: true, overflow: :expand) refute attrs.key?(:single_line) end def test_text_box_attrs_should_return_a_Hash_containing_a_single_line_option_when_single_is_true_and_overflow_isnot_expand attrs = @pdf.send(:text_box_attrs, 0, 0, 100, 100, single: true, overflow: :truncate) assert_equal attrs[:single_line], true end def test_text_box_attrs_should_return_a_Hash_which_does_not_contain_a_height_option_when_single_is_true attrs = @pdf.send(:text_box_attrs, 0, 0, 100, 100, single: true) refute attrs.key?(:height) end def test_text_box_attrs_should_return_a_Hash_which_does_not_contain_a_single_line_option_when_single_is_not_specified attrs = @pdf.send(:text_box_attrs, 0, 0, 100, 100) refute attrs.key?(:single_line) end def test_text_box_attrs_should_return_a_Hash_containing_a_height_optin_when_single_is_not_specified attrs = @pdf.send(:text_box_attrs, 0, 0, 100, 200) assert_equal attrs[:height], 200 end def test_text @pdf.expects(:text_box). with('contents', 100, 200, 150, 250, { overflow: :shirink_to_fit }).once @pdf.text('contents', 100, 200, 150, 250) end end ================================================ FILE: test/basic_report/units/generator/pdf/document/test_font.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::Generator::PDF::TestFont < Minitest::Test include Thinreports::BasicReport::TestHelper Font = Thinreports::BasicReport::Generator::PDF::Font def teardown # Reset font settings Thinreports.configure do |c| c.fallback_fonts = [] end end def test_setup_fonts pdf = document.pdf Font::BUILTIN_FONTS.each do |name, path| expected_font = { normal: path, bold: path, italic: path, bold_italic: path } assert_equal expected_font, pdf.font_families[name] end Font::PRAWN_BUINTIN_FONT_ALIASES.each do |alias_font, original_font| assert_equal pdf.font_families[alias_font], pdf.font_families[original_font] end assert_equal Font::DEFAULT_FALLBACK_FONTS, %w[IPAMincho] end def test_setup_fonts_with_custom_fallback_fonts Thinreports.configure do |c| c.fallback_fonts = [] end assert_equal Font::DEFAULT_FALLBACK_FONTS, document.pdf.fallback_fonts Thinreports.configure do |c| c.fallback_fonts = 'IPAGothic' end assert_equal ['IPAGothic'] + Font::DEFAULT_FALLBACK_FONTS, document.pdf.fallback_fonts Thinreports.configure do |c| c.fallback_fonts = ['IPAMincho'] end assert_equal ['IPAMincho'] + Font::DEFAULT_FALLBACK_FONTS, document.pdf.fallback_fonts Thinreports.configure do |c| c.fallback_fonts = ['IPAMincho', data_file('font.ttf')] end assert_equal ['IPAMincho', 'Custom-fallback-font1'] + Font::DEFAULT_FALLBACK_FONTS, document.pdf.fallback_fonts end def test_setup_fonts_with_unknown_custom_fallback_fonts Thinreports.configure do |c| c.fallback_fonts = ['/path/to/unknown.ttf'] end assert_raises Thinreports::BasicReport::Errors::FontFileNotFound do create_document end end def test_default_family assert_equal 'Helvetica', document.default_family end def test_default_family_if_mmissing assert_equal 'Helvetica', document.default_family_if_missing('unknown') assert_equal 'IPAMincho', document.default_family_if_missing('IPAMincho') end def test_font_has_style? doc = create_document assert_equal false, doc.font_has_style?('unknown', :bold) doc.pdf.font_families['font_foo'] = { normal: '/path/to/foo.ttf' } assert_equal false, doc.font_has_style?('font_foo', :italic) doc.pdf.font_families['font_foo'] = { normal: '/path/to/foo.ttf', bold: '/path/to/foo.ttf' } assert_equal false, doc.font_has_style?('font_foo', :bold) doc.pdf.font_families['font_foo'] = { normal: '/path/to/foo.ttf', bold: '/path/to/foo_bold.ttf' } assert_equal true, doc.font_has_style?('font_foo', :bold) end def document Thinreports::BasicReport::Generator::PDF::Document.new end alias_method :create_document, :document end ================================================ FILE: test/basic_report/units/generator/pdf/document/test_graphics.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::Generator::PDF::TestGraphics < Minitest::Test include Thinreports::BasicReport::TestHelper class TestGraphics attr_accessor :pdf include Thinreports::BasicReport::Generator::PDF::Graphics end def setup @g = TestGraphics.new @g.pdf = mock('pdf') end def test_setup_custom_graphic_states @g.pdf.expects(:line_width). with(TestGraphics::BASE_LINE_WIDTH).once @g.send(:setup_custom_graphic_states) end def test_line_width @g.pdf. expects(:line_width). with(10 * TestGraphics::BASE_LINE_WIDTH).once @g.send(:line_width, 10) end def test_save_graphics_state @g.pdf.expects(:save_graphics_state).once @g.send(:save_graphics_state) end def test_restore_graphics_state @g.pdf.expects(:restore_graphics_state).once @g.send(:restore_graphics_state) end end ================================================ FILE: test/basic_report/units/generator/pdf/document/test_page.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::Generator::PDF::Document::TestPage < Minitest::Test include Thinreports::BasicReport::TestHelper def create_pdf @pdf = Thinreports::BasicReport::Generator::PDF::Document.new end def test_JIS_page_size sizes = Thinreports::BasicReport::Generator::PDF::Document::JIS_SIZES assert_equal sizes['B4'], [728.5, 1031.8] assert_equal sizes['B5'], [515.9, 728.5] end def test_B4_paper_size_should_returns_size_as_B4_JIS create_pdf format = Thinreports::BasicReport::Layout::Format.build(layout_file.path) format.stubs(page_paper_type: 'B4') @pdf.start_new_page(format) assert_equal @pdf.internal.page.size, [728.5, 1031.8] end def test_B4_ISO_paper_size_should_be_converted_to_B4 create_pdf format = Thinreports::BasicReport::Layout::Format.build(layout_file.path) format.stubs(page_paper_type: 'B4_ISO') @pdf.start_new_page(format) assert_equal @pdf.internal.page.size, 'B4' end def test_change_page_format_should_return_true_at_first_time create_pdf format = Thinreports::BasicReport::Layout::Format.build(layout_file.path) assert_equal @pdf.send(:change_page_format?, format), true end def test_change_page_format_should_return_false_when_given_the_same_format create_pdf format = Thinreports::BasicReport::Layout::Format.build(layout_file.path) @pdf.instance_variable_set(:@current_page_format, format) assert_equal @pdf.send(:change_page_format?, format), false end def test_change_page_format_should_return_true_when_given_the_other_format create_pdf format1 = Thinreports::BasicReport::Layout::Format.build(layout_file.path) format2 = Thinreports::BasicReport::Layout::Format.build(layout_file.path) @pdf.instance_variable_set(:@current_page_format, format1) assert_equal @pdf.send(:change_page_format?, format2), true end def test_new_basic_page_options format = Thinreports::BasicReport::Layout::Format.build(layout_file.path) options = create_pdf.send(:new_basic_page_options, format) assert_equal options[:layout], format.page_orientation.to_sym assert_equal options[:size], format.page_paper_type end def test_new_basic_page_options_when_the_layout_has_customize_size format = stub user_paper_type?: true, page_width: 100, page_height: 100, page_orientation: 'portrait' options = create_pdf.send(:new_basic_page_options, format) assert_equal options[:size], [100, 100] end def test_start_new_page_should_create_stamp create_pdf format = Thinreports::BasicReport::Layout::Format.build(layout_file.path) @pdf.start_new_page(format) assert_includes @pdf.send(:format_stamp_registry), format.identifier end def test_start_new_page_should_not_create_stamp create_pdf format = Thinreports::BasicReport::Layout::Format.build(layout_file.path) @pdf.start_new_page(format) @pdf.start_new_page(format) assert_equal @pdf.send(:format_stamp_registry).size, 1 end def test_start_new_page_should_stamp_constantly create_pdf format = Thinreports::BasicReport::Layout::Format.build(layout_file.path) @pdf.expects(:stamp).with(format.identifier.to_s).times(2) @pdf.start_new_page(format) @pdf.start_new_page(format) end def test_add_blank_page_should_create_an_A4_size_page_in_first_page create_pdf @pdf.internal.expects(:start_new_page).with({size: 'A4'}).once @pdf.add_blank_page end def test_add_blank_page_should_call_with_no_arguments_since_second_page create_pdf format = Thinreports::BasicReport::Layout::Format.build(layout_file.path) @pdf.start_new_page(format) @pdf.internal.expects(:start_new_page).with({}).once @pdf.add_blank_page end end ================================================ FILE: test/basic_report/units/generator/pdf/document/test_parse_color.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::Generator::PDF::TestParseColor < Minitest::Test include Thinreports::BasicReport::TestHelper class TestColorParser include Thinreports::BasicReport::Generator::PDF::ParseColor end def setup @parser = TestColorParser.new end def test_parse_color_with_hexcolor assert_equal @parser.parse_color('#ff0000'), 'ff0000' assert_equal @parser.parse_color('000000'), '000000' end def test_parse_color_with_colorname assert_equal @parser.parse_color('red'), 'ff0000' end def test_parse_color_with_colorname_raise_when_unknown_name_given assert_raises Thinreports::BasicReport::Errors::UnsupportedColorName do @parser.parse_color('whitesmoke') end end end ================================================ FILE: test/basic_report/units/generator/pdf/prawn_ext/test_calc_image_dimensions.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::Generator::PrawnExt::TestCalcImageDimensions < Minitest::Test class Klass prepend Thinreports::BasicReport::Generator::PrawnExt::CalcImageDimensions def calc_image_dimensions(options) options end end def setup @klass = Klass.new end def test_calc_image_dimensions res_options = @klass.calc_image_dimensions( auto_fit: [100, 200], width: 101, height: 199 ) assert_equal [100, 200], res_options[:fit] refute_includes res_options.keys, :auto_fit res_options = @klass.calc_image_dimensions( auto_fit: [100, 200], width: 99, height: 201 ) assert_equal [100, 200], res_options[:fit] res_options = @klass.calc_image_dimensions( auto_fit: [100, 200], width: 99, height: 199 ) refute_includes res_options.keys, :fit end end ================================================ FILE: test/basic_report/units/generator/pdf/prawn_ext/test_width_of.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::Generator::PrawnExt::TestWidthOf < Minitest::Test def setup @pdf = Prawn::Document.new end def test_width_of text_width = @pdf.width_of('abcd') @pdf.character_spacing(1) do expected_character_space_width = 1 * 3 assert_equal text_width + expected_character_space_width, @pdf.width_of('abcd') end @pdf.character_spacing(1) do assert_equal 0, @pdf.width_of('') end end end ================================================ FILE: test/basic_report/units/generator/pdf/test_document.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::Generator::PDF::TestDocument < Minitest::Test include Thinreports::BasicReport::TestHelper # Alias Document = Thinreports::BasicReport::Generator::PDF::Document def test_new pdf = Document.new assert_equal pdf.internal.page_count, 0 assert_equal pdf.internal.page.margins.values, [0, 0, 0, 0] pdf = Document.new(security: { owner_password: 'abc' }) assert_equal true, pdf.internal.state.encrypt pdf = Document.new(title: 'Title') assert_equal 'Title', pdf.internal.state.store.info.data[:Title] end end ================================================ FILE: test/basic_report/units/generator/test_pdf.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::Generator::TestPDF < Minitest::Test include Thinreports::BasicReport::TestHelper PDF = Thinreports::BasicReport::Generator::PDF def test_default_layout layout_filename = layout_file.path report = Thinreports::BasicReport::Report.new layout: layout_filename generator = PDF.new report assert_equal layout_filename, generator.default_layout.filename end def test_initialize report = Thinreports::BasicReport::Report.new layout: layout_file.path report.start_new_page PDF.new(report) assert_equal true, report.finalized? end end ================================================ FILE: test/basic_report/units/layout/test_base.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::Layout::TestBase < Minitest::Test include Thinreports::BasicReport::TestHelper Layout = Thinreports::BasicReport::Layout def test_load_format assert_instance_of Layout::Format, Layout::Base.load_format(layout_file.path) assert_raises Thinreports::BasicReport::Errors::LayoutFileNotFound do Layout::Base.load_format 'unknown.tlf' end end def test_new layout_filename = layout_file.path layout = Layout::Base.new(layout_filename) assert_nil layout.id assert_equal layout_filename, layout.filename assert_instance_of Layout::Format, layout.format end def test_id layout_without_id = Layout::Base.new(layout_file.path) assert_nil layout_without_id.id layout_with_id = Layout::Base.new(layout_file.path, id: 'foo') assert_equal 'foo', layout_with_id.id end def test_default? layout_without_id = Layout::Base.new(layout_file.path) assert_equal true, layout_without_id.default? layout_with_id = Layout::Base.new(layout_file.path, id: 'bar') assert_equal false, layout_with_id.default? end end ================================================ FILE: test/basic_report/units/layout/test_format.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::Layout::TestFormat < Minitest::Test include Thinreports::BasicReport::TestHelper LAYOUT_SCHEMA = { 'version' => '0.9.0', 'title' => 'Report Title', 'report' => { 'paper-type' => 'A4', 'width' => 100.0, 'height' => 200.0, 'orientation' => 'landscape', 'margin' => [100.0, 200.0, 300.0, 999.9] }, 'state' => { 'layout-guides' => [ { 'type' => 'x', 'position' => 0.1 } ] }, 'items' => [ { 'type'=> 'rect', 'id'=> '', 'x'=> 100.0, 'y'=> 100.0, 'width'=> 100.0, 'height'=> 100.0, 'style'=> {'stroke-width'=> 1}}, { 'type'=> 'text-block', 'id'=> 'text_block', 'x'=> 100.0, 'y'=> 100.0 }, { 'type'=> 'page-number', 'id'=> '', 'x'=> 100.0, 'y'=> 100.0 } ] } Shape = Thinreports::BasicReport::Core::Shape Layout = Thinreports::BasicReport::Layout def test_attribute_readers format = Layout::Format.new(layout_schema) assert_equal 'Report Title', format.report_title assert_equal Thinreports::VERSION, format.last_version assert_equal 'A4', format.page_paper_type assert_equal 100.0, format.page_width assert_equal 200.0, format.page_height assert_equal 'landscape', format.page_orientation end def test_user_paper_type? format_paper_type_is_not_user = Layout::Format.new(layout_schema) assert_equal false, format_paper_type_is_not_user.user_paper_type? format_paper_type_is_user = Layout::Format.new(layout_schema.merge( { 'report' => { 'paper-type' => 'user' } } )) assert_equal true, format_paper_type_is_user.user_paper_type? end def test_build compatible_layout_file = layout_file assert_instance_of Layout::Format, Layout::Format.build(compatible_layout_file.path) incompatible_layout_file = layout_file(version: '0.0.1') assert_raises Thinreports::BasicReport::Errors::IncompatibleLayoutFormat do Layout::Format.build(incompatible_layout_file.path) end end def test_build_legacy_layout format = nil assert_deprecated { format = Layout::Format.build(data_file('legacy_layout', 'all-items.tlf')) } assert_equal 'Report Title', format.report_title assert_equal '0.8.2', format.last_version assert_equal 'A4', format.page_paper_type assert_equal 'portrait', format.page_orientation item_types = format.attributes['items'].map { |items| items['type'] } assert_equal 9, item_types.count assert_equal %w( rect ellipse line image image-block text-block list page-number text ).sort, item_types.sort end def test_initialize_items format = Layout::Format.new(layout_schema) assert_equal 2, format.shapes.count shape_classes = format.shapes.values.map(&:class) assert_includes shape_classes, Shape::TextBlock::Format assert_includes shape_classes, Shape::PageNumber::Format end private def layout_schema(version = Thinreports::VERSION) LAYOUT_SCHEMA.merge('version' => version) end end ================================================ FILE: test/basic_report/units/layout/test_legacy_schema.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::Layout::TestLegacySchema < Minitest::Test include Thinreports::BasicReport::TestHelper Layout = Thinreports::BasicReport::Layout def test_upgrade legacy_schema = { 'version' => '0.8.2', 'finger-print' => 'abcd', 'config' => { 'title' => 'Report Title', 'page' => { 'paper-type' => 'A4', 'width' => '100.0', 'height' => '200.0', 'orientation' => 'landscape', 'margin-top' => '0.1', 'margin-right' => '0.2', 'margin-bottom' => '0.3', 'margin-left' => '0.4' } }, 'svg' => '', 'state' => { 'layout-guide' => [] } } assert_equal( { 'version' => '0.8.2', 'title' => 'Report Title', 'report' => { 'paper-type' => 'A4', 'width' => 100.0, 'height' => 200.0, 'orientation' => 'landscape', 'margin' => [0.1, 0.2, 0.3, 0.4] }, 'items' => [] }, Layout::LegacySchema.new(legacy_schema).upgrade ) end def test_text_item_schema legacy_attributes = { 'x-id' => 'text_id', 'x-left' => '100.1', 'x-top' => '200.1', 'x-width' => '300.1', 'x-height' => '400.1', 'x-display' => 'true', 'font-family' => 'Helvetica', 'font-size' => '18', 'font-weight' => 'normal', 'font-style' => 'normal', 'text-decoration' => 'underline line-through', 'fill' => 'red', 'text-anchor' => 'start', 'x-valign' => 'top', 'x-line-height' => '20.1', 'kerning' => '2.1' } legacy_texts = %w( line1 line2 ) assert_equal( { 'id' => 'text_id', 'type' => 'text', 'x' => 100.1, 'y' => 200.1, 'width' => 300.1, 'height' => 400.1, 'display' => true, 'texts' => %w( line1 line2 ), 'style' => { 'font-family' => %w( Helvetica ), 'font-size' => 18.0, 'color' => 'red', 'font-style' => %w( underline linethrough ), 'text-align' => 'left', 'vertical-align' => 'top', 'line-height' => 20.1, 'letter-spacing' => 2.1 } }, layout_legacy_schema.text_item_schema(legacy_attributes, legacy_texts) ) end def test_rect_item_schema legacy_attributes = { 'x-id' => 'rect_id', 'x' => '100.1', 'y' => '200.1', 'width' => '300.1', 'height' => '400.1', 'x-display' => 'false', 'stroke-width' => '2.5', 'stroke' => '#ff0000', 'x-stroke-type' => 'dotted', 'fill' => 'red', 'rx' => '2' } assert_equal( { 'id' => 'rect_id', 'type' => 'rect', 'x' => 100.1, 'y' => 200.1, 'width' => 300.1, 'height' => 400.1, 'display' => false, 'border-radius' => 2, 'style' => { 'border-width' => 2.5, 'border-color' => '#ff0000', 'border-style' => 'dotted', 'fill-color' => 'red' } }, layout_legacy_schema.rect_item_schema(legacy_attributes) ) end def test_line_item_schema legacy_attributes = { 'x-id' => 'line_id', 'x1' => '100.1', 'y1' => '200.1', 'x2' => '300.1', 'y2' => '400.1', 'x-display' => 'true', 'stroke-width' => '1', 'stroke' => 'red', 'x-stroke-type' => 'solid' } assert_equal( { 'id' => 'line_id', 'type' => 'line', 'x1' => 100.1, 'y1' => 200.1, 'x2' => 300.1, 'y2' => 400.1, 'display' => true, 'style' => { 'border-width' => 1.0, 'border-color' => 'red', 'border-style' => 'solid' } }, layout_legacy_schema.line_item_schema(legacy_attributes) ) end def test_ellipse_item_schema legacy_attributes = { 'x-id' => 'ellipse_id', 'cx' => '100.1', 'cy' => '200.1', 'rx' => '300.1', 'ry' => '400.1', 'x-display' => 'true', 'stroke-width' => '1', 'stroke' => 'red', 'x-stroke-type' => 'solid', 'fill' => 'blue' } assert_equal( { 'id' => 'ellipse_id', 'type' => 'ellipse', 'cx' => 100.1, 'cy' => 200.1, 'rx' => 300.1, 'ry' => 400.1, 'display' => true, 'style' => { 'border-width' => 1.0, 'border-color' => 'red', 'border-style' => 'solid', 'fill-color' => 'blue' } }, layout_legacy_schema.ellipse_item_schema(legacy_attributes) ) end def test_image_item_schema legacy_attributes = { 'x-id' => 'image_id', 'x' => '100.1', 'y' => '200.1', 'width' => '300.1', 'height' => '400.1', 'x-display' => 'true', 'xlink:href' => 'data:image/png;base64,xxxxxxxxxxxxx' } assert_equal( { 'id' => 'image_id', 'type' => 'image', 'x' => 100.1, 'y' => 200.1, 'width' => 300.1, 'height' => 400.1, 'display' => true, 'data' => { 'mime-type' => 'image/png', 'base64' => 'xxxxxxxxxxxxx' } }, layout_legacy_schema.image_item_schema(legacy_attributes) ) end def test_page_number_item_schema legacy_attributes = { 'x-id' => 'page_number_id', 'x-left' => '100.1', 'x-top' => '200.1', 'x-width' => '300.1', 'x-height' => '400.1', 'x-format' => '{page}', 'x-target' => 'report', 'x-display' => 'true', 'font-family' => 'IPAMincho', 'font-size' => '18.5', 'fill' => 'red', 'font-weight' => 'bold', 'font-style' => 'italic', 'text-decoration' => '', 'text-anchor' => 'end', 'x-overflow' => 'fit' } assert_equal( { 'id' => 'page_number_id', 'type' => 'page-number', 'x' => 100.1, 'y' => 200.1, 'width' => 300.1, 'height' => 400.1, 'format' => '{page}', 'target' => 'report', 'display' => true, 'style' => { 'font-family' => %w( IPAMincho ), 'font-size' => 18.5, 'color' => 'red', 'font-style' => %w( bold italic ), 'text-align' => 'right', 'overflow' => 'fit' } }, layout_legacy_schema.page_number_item_schema(legacy_attributes) ) end def test_image_block_item_schema legacy_attributes = { 'x-id' => 'image_block_id', 'x-left' => '100.1', 'x-top' => '200.1', 'x-width' => '300.1', 'x-height' => '400.1', 'x-display' => 'false', 'x-position-x' => 'right', 'x-position-y' => 'bottom' } assert_equal( { 'id' => 'image_block_id', 'type' => 'image-block', 'x' => 100.1, 'y' => 200.1, 'width' => 300.1, 'height' => 400.1, 'display' => false, 'style' => { 'position-x' => 'right', 'position-y' => 'bottom' } }, layout_legacy_schema.image_block_item_schema(legacy_attributes) ) end def test_text_block_schema base_legacy_attributes = { 'x-id' => 'text_block_id', 'x-left' => '100.1', 'x-top' => '200.1', 'x-width' => '300.1', 'x-height' => '400.1', 'x-display' => 'true', 'x-value' => 'default value', 'x-multiple' => 'true', 'font-family' => 'Helvetica', 'font-size' => '18', 'font-weight' => 'bold', 'font-style' => 'normal', 'text-decoration' => 'line-through', 'fill' => 'red', 'text-anchor' => 'start', 'x-valign' => 'top', 'x-line-height' => '20.1', 'kerning' => '2.1', 'x-overflow' => 'expand', 'x-word-wrap' => 'break-word', 'x-format-base' => '$ {value}', 'x-format-type' => '', 'x-ref-id' => 'other_text_block_id' } assert_equal( { 'id' => 'text_block_id', 'type' => 'text-block', 'x' => 100.1, 'y' => 200.1, 'width' => 300.1, 'height' => 400.1, 'display' => true, 'value' => 'default value', 'multiple-line' => true, 'reference-id' => 'other_text_block_id', 'format' => { 'base' => '$ {value}', 'type' => '' }, 'style' => { 'font-family' => %w( Helvetica ), 'font-size' => 18.0, 'color' => 'red', 'font-style' => %w( bold linethrough ), 'text-align' => 'left', 'vertical-align' => 'top', 'line-height' => 20.1, 'letter-spacing' => 2.1, 'overflow' => 'expand', 'word-wrap' => 'break-word' } }, layout_legacy_schema.text_block_item_schema(base_legacy_attributes) ) schema_with_datetime_format = layout_legacy_schema.text_block_item_schema(base_legacy_attributes.merge( 'x-format-type' => 'datetime', 'x-format-datetime-format' => '%Y' )) assert_equal( { 'base' => '$ {value}', 'type' => 'datetime', 'datetime' => { 'format' => '%Y' } }, schema_with_datetime_format['format'] ) schema_with_number_format = layout_legacy_schema.text_block_item_schema(base_legacy_attributes.merge( 'x-format-type' => 'number', 'x-format-number-precision' => '1', 'x-format-number-delimiter' => ',' )) assert_equal( { 'base' => '$ {value}', 'type' => 'number', 'number' => { 'delimiter' => ',', 'precision' => 1 } }, schema_with_number_format['format'] ) schema_with_padding_format = layout_legacy_schema.text_block_item_schema(base_legacy_attributes.merge( 'x-format-type' => 'padding', 'x-format-padding-char' => '0', 'x-format-padding-length' => '10', 'x-format-padding-direction' => 'L' )) assert_equal( { 'base' => '$ {value}', 'type' => 'padding', 'padding' => { 'length' => 10, 'char' => '0', 'direction' => 'L' } }, schema_with_padding_format['format'] ) end def test_list_item_schema legacy_schema = { 'id' => 'default', 'type' => 's-list', 'content-height' => '300', 'page-break' => 'true', 'display' => 'false', 'header-enabled' => 'false', 'page-footer-enabled' => 'true', 'footer-enabled' => 'true', 'header' => { 'height' => '100.1', 'translate' => { 'x' => '100', 'y' => '200' }, 'svg' => { 'content' => '' } }, 'detail' => { 'height' => '200.1', 'translate' => { 'x' => '300', 'y' => '400' }, 'svg' => { 'content' => '' } }, 'page-footer' => { 'height' => '300.1', 'translate' => { 'x' => '500', 'y' => '600' }, 'svg' => { 'content' => '' } }, 'footer' => { 'height' => '400.1', 'translate' => { 'x' => '700', 'y' => '800' }, 'svg' => { 'content' => '' } } } layout_legacy_schema.stubs(:legacy_item_schemas).returns({ 'default' => legacy_schema }) legacy_element = mock() legacy_element.stubs(:attributes).returns({ 'x-id' => 'default' }) legacy_element.stubs(:elements).returns({}) assert_equal( { 'id' => 'default', 'type' => 'list', 'content-height' => 300.0, 'auto-page-break' => true, 'display' => false, 'header' => { 'height' => 100.1, 'translate' => { 'x' => 100.0, 'y' => 200.0 }, 'items' => [], 'enabled' => false }, 'detail' => { 'height' => 200.1, 'translate' => { 'x' => 300.0, 'y' => 400.0 }, 'items' => [] }, 'page-footer' => { 'height' => 300.1, 'translate' => { 'x' => 500.0, 'y' => 800.1 }, 'items' => [], 'enabled' => true }, 'footer' => { 'height' => 400.1, 'translate' => { 'x' => 700.0, 'y' => 1300.2 }, 'items' => [], 'enabled' => true } }, layout_legacy_schema.list_item_schema(legacy_element) ) end def test_image_position_y assert_equal 'top', layout_legacy_schema.image_position_y('top') assert_equal 'middle', layout_legacy_schema.image_position_y('center') assert_equal 'bottom', layout_legacy_schema.image_position_y('bottom') end def test_display assert_equal true, layout_legacy_schema.display('true') assert_equal false, layout_legacy_schema.display('false') end def test_font_style no_style = { 'font-weight' => 'normal', 'font-style' => 'normal', 'text-decoration' => 'none' } assert_equal [], layout_legacy_schema.font_style(no_style) assert_equal %w( bold ), layout_legacy_schema.font_style(no_style.merge('font-weight' => 'bold')) assert_equal %w( italic ), layout_legacy_schema.font_style(no_style.merge('font-style' => 'italic')) assert_equal %w( underline ), layout_legacy_schema.font_style(no_style.merge('text-decoration' => 'underline')) assert_equal %w( linethrough ), layout_legacy_schema.font_style(no_style.merge('text-decoration' => 'line-through')) assert_equal %w( underline linethrough ), layout_legacy_schema.font_style(no_style.merge('text-decoration' => 'line-through underline')) end def test_text_align assert_equal 'left', layout_legacy_schema.text_align('start') assert_equal 'center', layout_legacy_schema.text_align('middle') assert_equal 'right', layout_legacy_schema.text_align('end') end def test_vertical_align assert_equal '', layout_legacy_schema.vertical_align(nil) assert_equal 'top', layout_legacy_schema.vertical_align('top') assert_equal 'middle', layout_legacy_schema.vertical_align('center') assert_equal 'bottom', layout_legacy_schema.vertical_align('bottom') assert_equal 'top', layout_legacy_schema.vertical_align('') end def test_line_height assert_equal '', layout_legacy_schema.line_height('') assert_equal '', layout_legacy_schema.line_height(nil) assert_equal 20.1, layout_legacy_schema.line_height('20.1') end def test_letter_spacing assert_equal '', layout_legacy_schema.letter_spacing('') assert_equal '', layout_legacy_schema.letter_spacing('auto') assert_equal 2.5, layout_legacy_schema.letter_spacing('2.5') end def test_extract_item_schemas svg = <<-SVG SVG assert_equal( { 'item1' => { 'id' => 'item1' }, 'item3' => { 'id' => 'item3' } }, layout_legacy_schema.extract_legacy_item_schemas(svg) ) end def test_cleanup_svg svg = '' assert_equal '', layout_legacy_schema.cleanup_svg(svg) end private def layout_legacy_schema @layout_legacy_schema ||= Layout::LegacySchema.new({ 'svg' => '' }) end end ================================================ FILE: test/basic_report/units/layout/test_version.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::Layout::TestVersion < Minitest::Test include Thinreports::BasicReport::TestHelper # Alias Version = Thinreports::BasicReport::Layout::Version def test_compatible? Version.stubs(:compatible_rules).returns(['>= 0.8.0', '< 1.0.0']) assert Version.new('0.8.0').compatible? assert Version.new('0.9.9').compatible? assert Version.new('0.10.0').compatible? refute Version.new('0.7.9').compatible? refute Version.new('1.0.0').compatible? end def test_legacy? assert Version.new('0.8.9').legacy? refute Version.new('0.9.0').legacy? end end ================================================ FILE: test/basic_report/units/report/test_base.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::Report::TestBase < Minitest::Test include Thinreports::BasicReport::TestHelper Report = Thinreports::BasicReport::Report def setup @report = Report::Base.new @layout_file = layout_file end def test_on_page_create_callback report = Report::Base.new layout: @layout_file.path counter = 0 callback = -> page { assert_instance_of Report::Page, page counter += 1 } report.on_page_create(&callback) 2.times { report.start_new_page } assert_equal counter, 2 end def test_initialize_should_register_layout_as_default_when_layout_is_specified_as_the_option report = Report::Base.new layout: @layout_file.path assert_equal report.default_layout.filename, @layout_file.path end def test_initialize_should_initialize_new_Report_without_default_layout assert_nil @report.default_layout end def test_use_layout_should_register_default_layout_when_default_property_is_omitted @report.use_layout(@layout_file.path) assert_equal @report.default_layout.filename, @layout_file.path end def test_use_layout_should_register_default_layout_when_default_property_is_true @report.use_layout(@layout_file.path, default: true) assert_equal @report.default_layout.filename, @layout_file.path end def test_start_new_page_should_properly_create_a_new_Page_and_return @report.use_layout(@layout_file.path) assert_instance_of Thinreports::BasicReport::Report::Page, @report.start_new_page end def test_start_new_page_should_raise_when_the_layout_has_not_been_registered_yet assert_raises Thinreports::BasicReport::Errors::NoRegisteredLayoutFound do @report.start_new_page(layout: :unknown) end end def test_start_new_page_should_create_a_new_page_using_a_default_layout @report.use_layout(@layout_file.path, default: true) assert_equal @report.start_new_page.layout.filename, @layout_file.path end def test_start_new_page_should_create_a_new_page_using_a_layout_with_specified_id @report.use_layout(@layout_file.path, id: :foo) assert_equal @report.start_new_page(layout: :foo).layout.filename, @layout_file.path end def test_start_new_page_should_create_a_new_page_using_a_specified_layoutfile new_page = @report.start_new_page(layout: @layout_file.path) assert_equal new_page.layout.filename, @layout_file.path end def test_start_new_page_with_count_option @report.use_layout @layout_file.path, default: true new_page = @report.start_new_page count: false assert_nil new_page.no assert_equal @report.page_count, 0 new_page = @report.start_new_page count: true assert_equal new_page.no, 1 assert_equal @report.page_count, 1 new_page = @report.start_new_page assert_equal new_page.no, 2 assert_equal @report.page_count, 2 end def test_add_blank_page_should_properly_create_a_new_blank_page @report.use_layout(@layout_file.path) assert_instance_of Thinreports::BasicReport::Report::BlankPage, @report.add_blank_page end def test_layout_should_return_the_default_layout_with_no_arguments @report.use_layout(@layout_file.path, default: true) assert_equal @report.layout.filename, @layout_file.path end def test_layout_should_raise_when_the_specified_layout_is_not_found assert_raises Thinreports::BasicReport::Errors::UnknownLayoutId do @report.layout(:unknown_layout_id) end end def test_layout_should_return_the_layout_with_specified_id @report.use_layout(@layout_file.path, id: :foo) assert_equal @report.layout(:foo).filename, @layout_file.path end def test_generate report = Report::Base.new(layout: @layout_file.path) generator = mock('generator') generator.expects(:generate).with('result.pdf') Thinreports::BasicReport::Generator::PDF.expects(:new) .with(report, security: { owner_password: 'pass' }, title: nil) .returns(generator) report.generate( filename: 'result.pdf', security: { owner_password: 'pass' } ) end def test_generate_when_title_argument_is_specified report = Report::Base.new layout: @layout_file.path pdf = report.generate(title: 'Custom title') assert_equal 'Custom title', PDF::Reader.new(StringIO.new(pdf)).info[:Title] end def test_page_should_return_the_current_page @report.use_layout(@layout_file.path) @report.start_new_page assert_instance_of Thinreports::BasicReport::Report::Page, @report.page end def test_page_count_should_return_total_page_count @report.use_layout(@layout_file.path) 2.times { @report.start_new_page } assert_equal @report.page_count, 2 end def test_finalize_should_finalize_report @report.finalize assert_equal @report.finalized?, true end def test_finalized_asker_should_return_false_when_report_has_not_been_finalized_yet assert_equal @report.finalized?, false end def test_finalized_asker_should_return_true_when_report_is_already_finalized @report.finalize assert_equal @report.finalized?, true end def test_list_should_create_new_page_when_page_is_not_created @report.use_layout(@layout_file.path) @report.list refute_nil @report.page end def test_list_should_create_new_page_when_page_is_finalized @report.tap do |r| r.use_layout(@layout_file.path) r.start_new_page r.page.finalize end @report.list assert_equal @report.page.finalized?, false end def test_list_should_properly_return_shape_with_the_specified_id @report.use_layout(@layout_file.path) assert_equal @report.list.id, 'default' end def test_start_page_number assert_equal @report.start_page_number, 1 @report.start_page_number_from 10 assert_equal @report.start_page_number, 10 end def test_Base_create_should_finalize_report report = Report::Base.create do |r| assert_instance_of Report::Base, r end assert_equal report.finalized?, true end def test_Base_create_should_raise_when_no_block_given assert_raises ArgumentError do Report::Base.create end end def test_Base_generate_should_raise_when_no_block_given assert_raises ArgumentError do Report::Base.generate(:pdf) end end def test_Base_generate_with_deprecated_arguments Report::Base.expects(:create).with(layout: '/path/to/layout.tlf').returns(@report) @report.expects(:generate).with( filename: 'result.pdf', security: { owner_password: 'pass' }, title: nil ) Report::Base.generate( report: { layout: '/path/to/layout.tlf' }, generator: { filename: 'result.pdf', security: { owner_password: 'pass' } } ) { |_| } end def test_Base_generate_argument_priority Report::Base.expects(:create).with(layout: '/path/to/layout.tlf').returns(@report) @report.expects(:generate).with( filename: 'result.pdf', security: { owner_password: 'pass' }, title: nil ) Report::Base.generate( layout: '/path/to/layout.tlf', filename: 'result.pdf', security: { owner_password: 'pass' }, report: { layout: '/path/to/deprecated.tlf' }, generator: { filename: 'deprecated.pdf', security: { owner_password: 'deprecated' } } ) { |_| } end def test_Base_generate_when_title_argument_is_specified pdf = Report::Base.generate(layout: @layout_file.path, title: 'Custom title') { start_new_page } assert_equal 'Custom title', PDF::Reader.new(StringIO.new(pdf)).info[:Title] end end ================================================ FILE: test/basic_report/units/report/test_internal.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::Report::TestInternal < Minitest::Test include Thinreports::BasicReport::TestHelper Report = Thinreports::BasicReport::Report def setup @layout_file = layout_file end def report Report::Base.new end def test_layout_specified_in_new_method_should_be_defined_as_default_layout internal = Report::Internal.new(report, layout: @layout_file.path) assert_equal internal.default_layout.filename, @layout_file.path end def test_pathname_layout_specified_in_new_method_should_be_defined_as_default_layout internal = Report::Internal.new(report, layout: Pathname(@layout_file.path)) assert_equal internal.default_layout.filename, @layout_file.path end def test_register_layout_should_be_set_as_default_layout_when_options_are_omitted internal = Report::Internal.new(report, {}) internal.register_layout(@layout_file.path) assert_equal internal.default_layout.filename, @layout_file.path end def test_register_layout_should_be_set_as_default_layout_when_default_option_is_true internal = Report::Internal.new(report, {}) internal.register_layout(@layout_file.path, default: true) assert_equal internal.default_layout.filename, @layout_file.path end def test_register_layout_should_be_able_to_change_the_default_layout internal = Report::Internal.new(report, layout: @layout_file.path) internal.register_layout(@layout_file.path, default: true) assert_equal internal.default_layout.filename, @layout_file.path end def test_register_layout_should_be_set_as_with_id_when_id_option_is_set internal = Report::Internal.new(report, {}) internal.register_layout(@layout_file.path, id: :foo) assert_equal internal.layout_registry[:foo].filename, @layout_file.path end def test_register_layout_should_raise_an_error_when_id_is_already_registered internal = Report::Internal.new(report, {}) internal.register_layout(@layout_file.path, id: :foo) assert_raises ArgumentError do internal.register_layout(@layout_file.path, id: :foo) end end def test_add_page_should_finalize_the_current_page layout = Thinreports::BasicReport::Layout.new(@layout_file.path) internal = Report::Internal.new(report, layout: @layout_file.path) page = internal.add_page(Thinreports::BasicReport::Report::Page.new(report, layout)) internal.add_page(Thinreports::BasicReport::Report::Page.new(report, layout)) assert_equal page.finalized?, true end def test_add_page_should_return_the_current_page layout = Thinreports::BasicReport::Layout.new(@layout_file.path) new_page = Thinreports::BasicReport::Report::Page.new(report, layout) internal = Report::Internal.new(report, layout: @layout_file.path) assert_same new_page, internal.add_page(new_page) end def test_add_page_should_add_the_initialized_page layout = Thinreports::BasicReport::Layout.new(@layout_file.path) new_page = Thinreports::BasicReport::Report::Page.new(report, layout) internal = Report::Internal.new(report, layout: @layout_file.path) internal.add_page(new_page) assert_same new_page, internal.pages.last end def test_add_page_should_count_up_the_total_page_count layout = Thinreports::BasicReport::Layout.new(@layout_file.path) internal = Report::Internal.new(report, layout: @layout_file.path) internal.add_page(Thinreports::BasicReport::Report::Page.new(report, layout)) assert_equal internal.page_count, 1 end def test_add_page_should_switch_to_a_reference_to_the_current_page layout = Thinreports::BasicReport::Layout.new(@layout_file.path) new_pages = (1..2).inject([]) do |pages| pages << Thinreports::BasicReport::Report::Page.new(report, layout) end internal = Report::Internal.new(report, layout: @layout_file.path) internal.add_page(new_pages[0]) assert_same internal.page, new_pages[0] internal.add_page(new_pages[1]) assert_same internal.page, new_pages[1] end def test_add_blank_page_should_not_count_up_the_total_page_count_when_count_is_disabled internal = Report::Internal.new(report, layout: @layout_file.path) internal.add_page(Thinreports::BasicReport::Report::BlankPage.new(false)) assert_equal internal.page_count, 0 end def test_add_blank_page_should_count_up_the_total_page_count_when_count_is_enabled internal = Report::Internal.new(report, layout: @layout_file.path) internal.add_page(Thinreports::BasicReport::Report::BlankPage.new) assert_equal internal.page_count, 1 end def test_finalize_should_finalize_the_report internal = Report::Internal.new(report, layout: @layout_file.path) internal.finalize assert internal.finalized? end def test_finalize_should_not_work_when_report_is_already_finalized internal = Report::Internal.new(report, layout: @layout_file.path) internal.finalize # #finalize_current_page must never be called internal.expects(:finalize_current_page).never internal.finalize end def test_finalized_should_return_true_when_report_is_already_finalized internal = Report::Internal.new(report, layout: @layout_file.path) internal.finalize assert internal.finalized? end def test_load_layout_with_String internal = Report::Internal.new(report, layout: @layout_file.path) assert_equal internal.load_layout(@layout_file.path).filename, @layout_file.path end def test_load_layout_with_id internal = Report::Internal.new(report, {}) internal.register_layout(@layout_file.path, id: :sample) assert_equal internal.load_layout(:sample).filename, @layout_file.path end def test_load_layout_with_unknown_id internal = Report::Internal.new(report, {}) assert_nil internal.load_layout(:unknown) end def test_load_layout_should_set_default_layout_when_default_layout_is_nil internal = Report::Internal.new(report, {}) internal.load_layout(@layout_file.path) assert_equal internal.default_layout.filename, @layout_file.path end def test_load_layout_should_raise_error_when_invalid_value_set internal = Report::Internal.new(report, {}) assert_raises Thinreports::BasicReport::Errors::LayoutFileNotFound do internal.load_layout('/path/to/unkown.tlf') end end def test_copy_page_should_finalize_current_page layout = Thinreports::BasicReport::Layout.new(@layout_file.path) internal = Report::Internal.new(report, layout: @layout_file.path) internal.add_page(page = Thinreports::BasicReport::Report::Page.new(report, layout)) internal.copy_page assert page.finalized? end def test_copy_page_should_add_the_copied_page layout = Thinreports::BasicReport::Layout.new(@layout_file.path) internal = Report::Internal.new(report, layout: @layout_file.path) internal.add_page(Thinreports::BasicReport::Report::Page.new(report, layout)) internal.copy_page assert_equal internal.page_count, 2 end end ================================================ FILE: test/basic_report/units/test_layout.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::TestLayout < Minitest::Test include Thinreports::BasicReport::TestHelper def test_new assert_instance_of Thinreports::BasicReport::Layout::Base, Thinreports::BasicReport::Layout.new(layout_file.path) end end ================================================ FILE: test/basic_report/units/test_report.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::BasicReport::TestReport < Minitest::Test include Thinreports::BasicReport::TestHelper # Alias Report = Thinreports::BasicReport::Report def test_new report = Report.new(layout: layout_file.path) assert_instance_of Report::Base, report end def test_create report = Report.create(layout: layout_file.path, &:start_new_page) assert_instance_of Report::Base, report end def test_generate result = Report.generate(layout: layout_file.path, &:start_new_page) assert_pdf_data result assert_raises Thinreports::BasicReport::Errors::LayoutFileNotFound do Report.generate(layout: '') { |_| } end end end ================================================ FILE: test/feature_test.rb ================================================ # frozen_string_literal: true require 'pdf_matcher/testing/minitest_adapter' PdfMatcher.config.diff_pdf_opts = %w( --mark-differences --channel-tolerance=40 ) module Thinreports module FeatureTest def self.[](base_dir) k = Class.new(Base) k.class_eval %Q{ def dir Pathname.new('#{base_dir}') end } k end class Base < Minitest::Test class << self def feature(&block) define_method(:test_feature, &block) end end def dir raise NotImplementedError end def path_of(filename) dir.join(filename).to_path end def assert_pdf(actual) actual_pdf.binwrite(actual) if expect_pdf.exist? assert_match_pdf expect_pdf, actual_pdf, output_diff: path_of('diff.pdf') else flunk 'expect.pdf does not exist.' end rescue PdfMatcher::DiffPdf::CommandNotAvailable skip 'The feature test was skipped because the diff-pdf command is not available; please install diff-pdf (https://github.com/vslavik/diff-pdf) and try again.' end def template_path(filename = 'template.tlf') dir.join(filename).to_path end private def actual_pdf dir.join('actual.pdf') end def expect_pdf dir.join('expect.pdf') end end end end ================================================ FILE: test/main/test_config.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::TestConfig < Minitest::Test def test_config_should_return_configuration_of_thinreports assert_instance_of Thinreports::Configuration, Thinreports.config end def test_configure_should_exec_an_given_block_with_config_which_instance_of_Configuration Thinreports.configure do |config| assert_instance_of Thinreports::Configuration, config end end def test_fallback_fonts config = Thinreports::Configuration.new # should be empty by default assert_empty config.fallback_fonts config.fallback_fonts = 'Helvetica' assert_equal config.fallback_fonts, ['Helvetica'] config.fallback_fonts = ['/path/to/font.ttf', 'Courier New'] assert_equal config.fallback_fonts, ['/path/to/font.ttf', 'Courier New'] config.fallback_fonts = [] config.fallback_fonts << 'Helvetica' config.fallback_fonts << 'IPAMincho' config.fallback_fonts.unshift 'Times New Roman' assert_equal config.fallback_fonts, ['Times New Roman', 'Helvetica', 'IPAMincho'] end end ================================================ FILE: test/main/test_helper.rb ================================================ # frozen_string_literal: true require 'minitest/autorun' require 'minitest/spec' require 'minitest/unit' require 'thinreports' ================================================ FILE: test/section_report/features/basic/README.md ================================================ # Basic Usage In the section-format, multiple sections of three types can be defined (header, footer, and repeatable section), and PDFs can be generated by combining them. - [Example code](test_feature.rb) - [Example template file](template.tlf) - [Example PDF](expect.pdf) ## Section types Header `headers` - Multiple definitions possible - Draw in the order defined in the template - Can be hidden by a parameter - If "show every page" is enabled, it will be drawn on every page, otherwise it will be drawn on the first page of each parameter group Footer `footers` - Muliple definitions possible - Draw in the order defined in the template - Can be hidden by a parameter Detail `details` - Multiple definitions possible - Draw in the order added by the parameter - Multiple definitions can be combined and drawn ## Page break Sections overflowing the page are drawn on the next page. ## Generating a PDF Generate a PDF by passing the following parameters to `Thinreports.generate`. ```ruby params = { type: :section, layout_file: '/path/to/template.tlf', params: { groups: [ { headers: { any_header_id: { items: { logo_image: '/path/to/logo.jpg' } } }, details: [ { id: 'detail_id', items: { name_field_id: 'field value' } } ], footers: { any_footer_id: { display: false } } } ] } } Thinreports.generate(params, filename: '/path/to/output.pdf') ``` When getting PDF data, omit the `filename` argument. ```ruby Thinreports.generate(params) #=> PDF data ``` ================================================ FILE: test/section_report/features/basic/template.tlf ================================================ { "schema-version": "1.0", "last-modified-by": "1.0.0-sectionreport.1", "title": "", "report": { "orientation": "portrait", "paper-type": "A4", "width": 0, "height": 0, "margin": [ 20, 20, 20, 20 ] }, "sections": [ { "id": "title", "type": "header", "height": 100, "display": true, "auto-stretch": true, "every-page": false, "items": [ { "type": "text", "id": "", "x": 25, "y": 37.5, "width": 124.2, "height": 31.67, "description": "", "display": true, "follow-stretch": "none", "texts": [ "Item List" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [ "bold" ], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 30, "line-height-ratio": "", "line-height": 33 }, "affect-bottom-margin": true } ] }, { "id": "", "type": "header", "height": 50, "display": true, "auto-stretch": true, "every-page": true, "items": [ { "type": "text", "id": "", "x": 29.3, "y": 18.33, "width": 49.84, "height": 18, "description": "", "display": true, "follow-stretch": "none", "texts": [ "Name" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [ "bold" ], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 18, "line-height-ratio": "", "line-height": 19.8 }, "affect-bottom-margin": true }, { "type": "text", "id": "", "x": 373.47, "y": 20.82, "width": 84.84, "height": 18, "description": "", "display": true, "follow-stretch": "none", "texts": [ "Unit Price" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [ "bold" ], "text-align": "right", "vertical-align": "top", "letter-spacing": "", "font-size": 18, "line-height-ratio": "", "line-height": 19.8 }, "affect-bottom-margin": true }, { "type": "line", "id": "", "x1": 20, "y1": 49, "x2": 580, "y2": 49, "description": "", "display": true, "follow-stretch": "none", "style": { "border-width": 1, "border-color": "#000000", "border-style": "solid" }, "affect-bottom-margin": true } ] }, { "id": "item_detail", "type": "detail", "height": 50, "auto-stretch": true, "items": [ { "type": "text-block", "id": "item_name", "x": 26.81, "y": 15, "width": 300, "height": 22, "description": "", "reference-id": "", "value": "", "multiple-line": true, "display": true, "format": { "base": "", "type": "" }, "follow-stretch": "none", "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "middle", "letter-spacing": "", "overflow": "truncate", "word-wrap": "break-word", "font-size": 18, "line-height-ratio": "", "line-height": 19.8 }, "affect-bottom-margin": true }, { "type": "text-block", "id": "item_unit_price", "x": 346.81, "y": 15, "width": 114.17, "height": 22, "description": "", "reference-id": "", "value": "", "multiple-line": true, "display": true, "format": { "base": "", "type": "" }, "follow-stretch": "none", "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "right", "vertical-align": "middle", "letter-spacing": "", "overflow": "truncate", "word-wrap": "break-word", "font-size": 18, "line-height-ratio": "", "line-height": 19.8 }, "affect-bottom-margin": true } ] }, { "id": "item_category", "type": "detail", "height": 50, "auto-stretch": true, "items": [ { "type": "rect", "id": "", "x": 20.89, "y": 10, "width": 551.5, "height": 32.5, "border-radius": 0, "description": "", "display": true, "follow-stretch": "none", "style": { "border-color": "", "border-style": "solid", "fill-color": "#dddddd", "border-width": 0 }, "affect-bottom-margin": true }, { "type": "text-block", "id": "category_name", "x": 26.81, "y": 15, "width": 295, "height": 22.5, "description": "", "reference-id": "", "value": "", "multiple-line": true, "display": true, "format": { "base": "", "type": "" }, "follow-stretch": "none", "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [ "bold" ], "text-align": "left", "vertical-align": "middle", "letter-spacing": "", "overflow": "truncate", "word-wrap": "break-word", "font-size": 18, "line-height-ratio": "", "line-height": 19.8 }, "affect-bottom-margin": true } ] }, { "id": "overall", "type": "footer", "height": 80, "display": true, "auto-stretch": true, "items": [ { "type": "text", "id": "", "x": 306.81, "y": 8.33, "width": 124.03, "height": 25, "description": "", "display": true, "follow-stretch": "none", "texts": [ "Number of Items" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "right", "vertical-align": "middle", "letter-spacing": "", "font-size": 14, "line-height-ratio": "", "line-height": 15.4 }, "affect-bottom-margin": true }, { "type": "text-block", "id": "number_of_items", "x": 442.64, "y": 8.33, "width": 95.83, "height": 25, "description": "", "reference-id": "", "value": "", "multiple-line": true, "display": true, "format": { "base": "", "type": "" }, "follow-stretch": "none", "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "right", "vertical-align": "middle", "letter-spacing": "", "overflow": "truncate", "word-wrap": "break-word", "font-size": 14, "line-height-ratio": "", "line-height": 15.4 }, "affect-bottom-margin": true }, { "type": "text", "id": "", "x": 294.83, "y": 41.66, "width": 137.45, "height": 25, "description": "", "display": true, "follow-stretch": "none", "texts": [ "Number of Categories" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "right", "vertical-align": "middle", "letter-spacing": "", "font-size": 14, "line-height-ratio": "", "line-height": 15.4 }, "affect-bottom-margin": true }, { "type": "text-block", "id": "number_of_categories", "x": 442.64, "y": 43.33, "width": 95.83, "height": 25, "description": "", "reference-id": "", "value": "", "multiple-line": true, "display": true, "format": { "base": "", "type": "" }, "follow-stretch": "none", "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "right", "vertical-align": "middle", "letter-spacing": "", "overflow": "truncate", "word-wrap": "break-word", "font-size": 14, "line-height-ratio": "", "line-height": 15.4 }, "affect-bottom-margin": true }, { "type": "line", "id": "", "x1": 20, "y1": 1, "x2": 580, "y2": 1, "description": "", "display": true, "follow-stretch": "none", "style": { "border-width": 1, "border-color": "#000000", "border-style": "solid" }, "affect-bottom-margin": true } ] } ], "state": { "layout-guides": [] } } ================================================ FILE: test/section_report/features/basic/test_feature.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::SectionReport::TestBasicFeature < Thinreports::FeatureTest[__dir__] def setup initialize_data end feature do params = { type: :section, layout_file: template_path, params: { groups: [ { details: build_details, footers: { overall: { items: { number_of_items: @categories.sum { |category| category.items.count }, number_of_categories: @categories.count } } } } ] } } assert_pdf Thinreports.generate(params) end private def build_details @categories.each_with_object([]) do |category, details| # Add category row details << { id: 'item_category', items: { category_name: category.name } } category.items.each do |item| # Add item row details << { id: 'item_detail', items: { item_name: item.name, item_unit_price: item.unit_price } } end end end Category = Struct.new(:name, :items) Item = Struct.new(:name, :unit_price) def initialize_data @categories = [ Category.new('Beauty', [ Item.new('Synergistic Rubber Bag', '1654.0'), Item.new('Incredible Bronze Shirt', '4369.0'), Item.new('Aerodynamic Wool Gloves', '9254.0'), Item.new('Fantastic Iron Pants', '597.0'), Item.new('Fantastic Marble Clock', '3489.0'), Item.new('Mediocre Steel Watch', '5147.0'), Item.new('Gorgeous Granite Plate', '792.0') ]), Category.new('Garden', [ Item.new('Intelligent Linen Coat', '8706.0'), Item.new('Sleek Copper Chair', '6810.0') ]), Category.new('Tools & Games', [ Item.new('Small Granite Clock', '6731.0'), Item.new('Aerodynamic Leather Bag', '6238.0'), Item.new('Fantastic Rubber Hat', '6198.0'), Item.new('Aerodynamic Marble Shoes', '9603.0') ]) ] end end ================================================ FILE: test/section_report/features/item_follow_stretch/README.md ================================================ # Item Follow Stretch 領域伸縮に追従を有効にすることで、配置された section 又は stack-view-row の伸縮に応じて、item の高さや位置を自動的に変化させることができる。 - [Example code](test_feature.rb) - [Example template file](template.tlf) - [Example PDF](expect.pdf) それぞれのオプションと伸縮による変化は次の通り。 ## 領域伸縮に追従: 高さ `follow-stretch: height` 領域の伸縮によって、item の高さが伸縮する。 以下の item をサポート: - text-block - text - rect - line ## 領域伸縮に追従: 上位置 `follow-stretch: y` 領域の伸縮によって、item の上位置が移動する。 以下の item をサポート: - line ## 領域伸縮に追従: なし (デフォルト) `follow-stretch: none` 領域の伸縮の影響を受けない ================================================ FILE: test/section_report/features/item_follow_stretch/template.tlf ================================================ { "schema-version": "1.0", "last-modified-by": "1.0.0-sectionreport.1", "title": "", "report": { "orientation": "portrait", "paper-type": "A4", "width": 0, "height": 0, "margin": [ 0, 0, 0, 0 ] }, "sections": [ { "id": "header1", "type": "header", "height": 80, "display": true, "auto-stretch": true, "every-page": false, "items": [ { "type": "text-block", "id": "text_expand", "x": 10, "y": 30, "width": 50, "height": 30, "description": "", "reference-id": "", "value": "", "multiple-line": true, "display": true, "format": { "base": "", "type": "" }, "follow-stretch": "none", "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "overflow": "expand", "word-wrap": "break-word", "font-size": 12, "line-height-ratio": "", "line-height": 13.2 }, "affect-bottom-margin": true }, { "type": "text-block", "id": "text_block", "x": 130, "y": 30, "width": 120, "height": 30, "description": "", "reference-id": "", "value": "", "multiple-line": true, "display": true, "format": { "base": "", "type": "" }, "follow-stretch": "height", "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "center", "vertical-align": "middle", "letter-spacing": "", "overflow": "truncate", "word-wrap": "break-word", "font-size": 18, "line-height-ratio": "", "line-height": 19.8 }, "affect-bottom-margin": true }, { "type": "text", "id": "", "x": 270, "y": 30, "width": 50, "height": 30, "description": "", "display": true, "follow-stretch": "height", "texts": [ "Text" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "center", "vertical-align": "middle", "letter-spacing": "", "font-size": 18, "line-height-ratio": "", "line-height": 19.8 }, "affect-bottom-margin": true }, { "type": "rect", "id": "", "x": 340, "y": 30, "width": 50, "height": 30, "border-radius": 0, "description": "", "display": true, "follow-stretch": "height", "style": { "border-color": "#000000", "border-style": "solid", "fill-color": "#ffffff", "border-width": 1 }, "affect-bottom-margin": true }, { "type": "line", "id": "", "x1": 410, "y1": 30, "x2": 410, "y2": 60, "description": "", "display": true, "follow-stretch": "height", "style": { "border-width": 1, "border-color": "#000000", "border-style": "solid" }, "affect-bottom-margin": true }, { "type": "text", "id": "", "x": 535.83, "y": 10, "width": 46.86, "height": 12, "description": "", "display": true, "follow-stretch": "none", "texts": [ "Header1" ], "style": { "font-family": [ "Helvetica" ], "color": "#aaaaaa", "font-style": [], "text-align": "right", "vertical-align": "top", "letter-spacing": "", "font-size": 12, "line-height-ratio": "", "line-height": 13.2 }, "affect-bottom-margin": true }, { "type": "text", "id": "", "x": 10, "y": 10, "width": 111, "height": 12, "description": "", "display": true, "follow-stretch": "none", "texts": [ "follow stretch: height" ], "style": { "font-family": [ "Helvetica" ], "color": "#aaaaaa", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 12, "line-height-ratio": "", "line-height": 13.2 }, "affect-bottom-margin": true } ] }, { "id": "header2", "type": "header", "height": 80, "display": true, "auto-stretch": true, "every-page": false, "items": [ { "type": "text-block", "id": "text_expand", "x": 10, "y": 30, "width": 50, "height": 30, "description": "", "reference-id": "", "value": "", "multiple-line": true, "display": true, "format": { "base": "", "type": "" }, "follow-stretch": "none", "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "overflow": "expand", "word-wrap": "break-word", "font-size": 12, "line-height-ratio": "", "line-height": 13.2 }, "affect-bottom-margin": true }, { "type": "line", "id": "", "x1": 130, "y1": 70, "x2": 410, "y2": 70, "description": "", "display": true, "follow-stretch": "y", "style": { "border-width": 1, "border-color": "#000000", "border-style": "solid" }, "affect-bottom-margin": true }, { "type": "line", "id": "", "x1": 10, "y1": 1, "x2": 584.17, "y2": 1, "description": "", "display": true, "follow-stretch": "none", "style": { "border-width": 1, "border-color": "#aaaaaa", "border-style": "dashed" }, "affect-bottom-margin": true }, { "type": "text", "id": "", "x": 535.83, "y": 10, "width": 46.86, "height": 12, "description": "", "display": true, "follow-stretch": "none", "texts": [ "Header2" ], "style": { "font-family": [ "Helvetica" ], "color": "#aaaaaa", "font-style": [], "text-align": "right", "vertical-align": "top", "letter-spacing": "", "font-size": 12, "line-height-ratio": "", "line-height": 13.2 }, "affect-bottom-margin": true }, { "type": "text", "id": "", "x": 10, "y": 10, "width": 111, "height": 12, "description": "", "display": true, "follow-stretch": "none", "texts": [ "follow stretch: y" ], "style": { "font-family": [ "Helvetica" ], "color": "#aaaaaa", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 12, "line-height-ratio": "", "line-height": 13.2 }, "affect-bottom-margin": true } ] }, { "id": "header3", "type": "header", "height": 180, "display": true, "auto-stretch": true, "every-page": false, "items": [ { "type": "stack-view", "id": "stackview", "x": 10, "y": 30, "width": 575.28, "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "rows": [ { "id": "row1", "height": 70, "auto-stretch": true, "display": true, "items": [ { "type": "text-block", "id": "text_expand", "x": 10, "y": 30, "width": 50, "height": 30, "description": "", "reference-id": "", "value": "", "multiple-line": true, "display": true, "format": { "base": "", "type": "" }, "follow-stretch": "none", "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "overflow": "expand", "word-wrap": "break-word", "font-size": 12, "line-height-ratio": "", "line-height": 13.2 }, "affect-bottom-margin": true }, { "type": "text-block", "id": "text_block", "x": 130, "y": 30, "width": 120, "height": 30, "description": "", "reference-id": "", "value": "", "multiple-line": true, "display": true, "format": { "base": "", "type": "" }, "follow-stretch": "height", "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "center", "vertical-align": "middle", "letter-spacing": "", "overflow": "truncate", "word-wrap": "break-word", "font-size": 18, "line-height-ratio": "", "line-height": 19.8 }, "affect-bottom-margin": true }, { "type": "text", "id": "", "x": 270, "y": 30, "width": 50, "height": 30, "description": "", "display": true, "follow-stretch": "height", "texts": [ "Text" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "center", "vertical-align": "middle", "letter-spacing": "", "font-size": 18, "line-height-ratio": "", "line-height": 19.8 }, "affect-bottom-margin": true }, { "type": "rect", "id": "", "x": 340, "y": 30, "width": 50, "height": 30, "border-radius": 0, "description": "", "display": true, "follow-stretch": "height", "style": { "border-color": "#000000", "border-style": "solid", "fill-color": "#ffffff", "border-width": 1 }, "affect-bottom-margin": true }, { "type": "line", "id": "", "x1": 410, "y1": 30, "x2": 410, "y2": 60, "description": "", "display": true, "follow-stretch": "height", "style": { "border-width": 1, "border-color": "#000000", "border-style": "solid" }, "affect-bottom-margin": true }, { "type": "text", "id": "", "x": 405, "y": 10, "width": 163.5, "height": 12, "description": "", "display": true, "follow-stretch": "none", "texts": [ "Row1 (stack-view)" ], "style": { "font-family": [ "Helvetica" ], "color": "#aaaaaa", "font-style": [], "text-align": "right", "vertical-align": "top", "letter-spacing": "", "font-size": 12, "line-height-ratio": "", "line-height": 13.2 }, "affect-bottom-margin": true }, { "type": "text", "id": "", "x": 10, "y": 10, "width": 111, "height": 12, "description": "", "display": true, "follow-stretch": "none", "texts": [ "follow stretch: height" ], "style": { "font-family": [ "Helvetica" ], "color": "#aaaaaa", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 12, "line-height-ratio": "", "line-height": 13.2 }, "affect-bottom-margin": true } ] }, { "id": "row2", "height": 70, "auto-stretch": true, "display": true, "items": [ { "type": "text-block", "id": "text_expand", "x": 10, "y": 30, "width": 50, "height": 30, "description": "", "reference-id": "", "value": "", "multiple-line": true, "display": true, "format": { "base": "", "type": "" }, "follow-stretch": "none", "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "overflow": "expand", "word-wrap": "break-word", "font-size": 12, "line-height-ratio": "", "line-height": 13.2 }, "affect-bottom-margin": true }, { "type": "line", "id": "", "x1": 130, "y1": 60, "x2": 410, "y2": 60, "description": "", "display": true, "follow-stretch": "y", "style": { "border-width": 1, "border-color": "#000000", "border-style": "solid" }, "affect-bottom-margin": true }, { "type": "text", "id": "", "x": 405, "y": 10, "width": 163.5, "height": 12, "description": "", "display": true, "follow-stretch": "none", "texts": [ "Row2 (stack-view)" ], "style": { "font-family": [ "Helvetica" ], "color": "#aaaaaa", "font-style": [], "text-align": "right", "vertical-align": "top", "letter-spacing": "", "font-size": 12, "line-height-ratio": "", "line-height": 13.2 }, "affect-bottom-margin": true }, { "type": "text", "id": "", "x": 10, "y": 10, "width": 111, "height": 12, "description": "", "display": true, "follow-stretch": "none", "texts": [ "follow stretch: y" ], "style": { "font-family": [ "Helvetica" ], "color": "#aaaaaa", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 12, "line-height-ratio": "", "line-height": 13.2 }, "affect-bottom-margin": true }, { "type": "line", "id": "", "x1": 10, "y1": 1, "x2": 570, "y2": 1, "description": "", "display": true, "follow-stretch": "none", "style": { "border-width": 1, "border-color": "#aaaaaa", "border-style": "dashed" }, "affect-bottom-margin": true } ] } ] }, { "type": "line", "id": "", "x1": 10, "y1": 1, "x2": 584.17, "y2": 1, "description": "", "display": true, "follow-stretch": "none", "style": { "border-width": 1, "border-color": "#aaaaaa", "border-style": "dashed" }, "affect-bottom-margin": true }, { "type": "text", "id": "", "x": 483.33, "y": 10, "width": 100, "height": 12, "description": "", "display": true, "follow-stretch": "none", "texts": [ "Header3" ], "style": { "font-family": [ "Helvetica" ], "color": "#aaaaaa", "font-style": [], "text-align": "right", "vertical-align": "top", "letter-spacing": "", "font-size": 12, "line-height-ratio": "", "line-height": 13.2 }, "affect-bottom-margin": true } ] }, { "id": "header4", "type": "header", "height": 30, "display": true, "auto-stretch": true, "every-page": false, "items": [ { "type": "text", "id": "header4", "x": 489.16, "y": 10, "width": 96.83, "height": 12, "description": "", "display": true, "follow-stretch": "none", "texts": [ "Header4 (dummy)" ], "style": { "font-family": [ "Helvetica" ], "color": "#aaaaaa", "font-style": [], "text-align": "right", "vertical-align": "top", "letter-spacing": "", "font-size": 12, "line-height-ratio": "", "line-height": 13.2 }, "affect-bottom-margin": true }, { "type": "line", "id": "", "x1": 10, "y1": 1, "x2": 584.17, "y2": 1, "description": "", "display": true, "follow-stretch": "none", "style": { "border-width": 1, "border-color": "#aaaaaa", "border-style": "dashed" }, "affect-bottom-margin": true } ] } ], "state": { "layout-guides": [] } } ================================================ FILE: test/section_report/features/item_follow_stretch/test_feature.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::SectionReport::TestItemFollowStretchFeature < Thinreports::FeatureTest[__dir__] feature do params = { type: :section, layout_file: template_path, params: { groups: [ { headers: { header1: { items: { text_expand: "Expand" * 10, text_block: 'Text Block' } }, header2: { items: { text_expand: "Expand" * 10, } }, header3: { items: { stackview: { rows: { row1: { items: { text_expand: "Expand" * 10, text_block: 'Text Block' } }, row2: { items: { text_expand: "Expand" * 10, } } } } } } } } ] } } assert_pdf Thinreports.generate(params) end end ================================================ FILE: test/section_report/features/item_parameters/README.md ================================================ # Item Paramater 各item の定義時のスタイルやプロパティはパラメータで変更することができる。 - [Example code](test_feature.rb) - [Example template file](template.tlf) - [Example PDF](expect.pdf) 変更できるスタイルやプロパティは item の種類によって異なる。 ## 共通 ``` all_item: { display: Boolean, # 表示状態を変更する } ``` ## テキストスタイル ``` text_block_and_text_item: { styles: { font_size: Number, # フォントサイズを変更する align: :left | :center | :right, # 水平方向の位置揃えを変更する valign: :top | :middle | :bottom, # 垂直方向の位置揃えを変更する color: String, # テキストカラーを変更する bold: Boolean, # 太字スタイルを変更する italic: Boolean, # 斜体スタイルを変更する linethrough: Boolean # 取消線スタイルを変更する } } ``` ## 位置の移動 ``` image_block_item: { styles: { offset_x: Number, # 水平方向に移動する offset_y: Number # 垂直方向に移動する } } ``` ## 値をセットする ``` text_block_item: { value: 'text value' }, text_block_item: 'text value' ``` ``` image_block_item: { value: '/path/to/image.jpg' }, image_block_item: { value: StringIO.new(File.binread('/path/to/image.jpg')) }, image_block_item: '/path/to/image.png', image_block_item: StringIO.new(File.binread('/path/to/image.png')) ``` ================================================ FILE: test/section_report/features/item_parameters/template.tlf ================================================ { "schema-version": "1.0", "last-modified-by": "1.0.0-sectionreport.1", "title": "", "report": { "orientation": "portrait", "paper-type": "A4", "width": 0, "height": 0, "margin": [ 20, 20, 20, 20 ] }, "sections": [ { "id": "detail1", "type": "detail", "height": 160, "auto-stretch": false, "items": [ { "type": "rect", "id": "", "x": 285, "y": 15, "width": 140, "height": 50, "border-radius": 0, "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "style": { "border-color": "#aaaaaa", "border-style": "dashed", "fill-color": "none", "border-width": 1 } }, { "type": "rect", "id": "rect", "x": 33.5, "y": 15, "width": 80.04, "height": 46.36, "border-radius": 0, "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "style": { "border-color": "#000000", "border-style": "solid", "fill-color": "#ffffff", "border-width": 1 } }, { "type": "ellipse", "id": "ellipse", "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "style": { "border-width": 1, "border-color": "#000000", "border-style": "solid", "fill-color": "#ffffff" }, "cx": 190.635, "cy": 46.99, "rx": 40.285, "ry": 31.99 }, { "type": "text", "id": "text", "x": 285, "y": 15, "width": 140, "height": 50, "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "texts": [ "static text" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 18, "line-height-ratio": "", "line-height": 19.8 } }, { "type": "line", "id": "line", "x1": 469.25, "y1": 15, "x2": 550.16, "y2": 83.72, "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "style": { "border-width": 1, "border-color": "#000000", "border-style": "solid" } }, { "type": "text-block", "id": "text_block", "x": 285, "y": 90, "width": 140, "height": 50, "description": "", "reference-id": "", "value": "text block", "multiple-line": true, "display": true, "format": { "base": "", "type": "" }, "follow-stretch": "none", "affect-bottom-margin": true, "style": { "font-family": [ "Helvetica" ], "color": "#ff0000", "font-style": [ "bold", "italic", "underline", "linethrough" ], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "overflow": "truncate", "word-wrap": "break-word", "font-size": 18, "line-height-ratio": "", "line-height": 19.8 } }, { "type": "image-block", "id": "image_block", "x": 146.59, "y": 90, "width": 112.62, "height": 63.07, "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "style": { "position-x": "left", "position-y": "top" } }, { "type": "image", "id": "image", "x": 26.21, "y": 90, "width": 100, "height": 50, "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "data": { "mime-type": "image/jpeg", "base64": "/9j/4AAQSkZJRgABAQAASABIAAD/4QDIRXhpZgAATU0AKgAAAAgABwESAAMAAAABAAEAAAEaAAUAAAABAAAAYgEbAAUAAAABAAAAagEoAAMAAAABAAIAAAExAAIAAAAPAAAAcgEyAAIAAAAUAAAAgodpAAQAAAABAAAAlgAAAAAAAABIAAAAAQAAAEgAAAABUGl4ZWxtYXRvciAzLjkAADIwMjA6MDY6MDggMTY6MDY6NzYAAAOgAQADAAAAAQABAACgAgAEAAAAAQAAAGSgAwAEAAAAAQAAADIAAAAA/+EJkGh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8APD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNS40LjAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcDpDcmVhdG9yVG9vbD0iUGl4ZWxtYXRvciAzLjkiIHhtcDpNb2RpZnlEYXRlPSIyMDIwLTA2LTA4VDE2OjA2Ojc2Ii8+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPD94cGFja2V0IGVuZD0idyI/PgD/wAARCAAyAGQDAREAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9sAQwABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEB/9sAQwEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEB/90ABAAN/9oADAMBAAIRAxEAPwD+eev+2g/yfCgAoAKACgAoAKACgAoAKACgAoAKAP/Q/nnr/toP8nwoAKACgAoAKACgAoAKACgAoAKACgD/0f556/7aD/J8KACgD3j9mj4UW3xu+M3hn4aXlvf3dvrmlePNQeDTfENn4VvHbwp8PfFXjCIx65f+F/GVraJHLoCSzwyeHb1tRt0l02GfS5ryPVrL888VOMavAXA2a8VUKuHo1MBjOHsNGpissr5xQis44lyfJJqeX4fN8irVpShmMoU5xzOgsNVlDFTp4unRng6/t8OZXHOc4w2XTjOca1LG1HGniIYWb+q4DE4tWrzwuMjFJ0E5J4eftIp006bkqsPp7wt/wTP+Mvi3SPDdxY/FL9nPT/Ffi/4KaB+0Z4d+Guu/E670bxrefBLVdEtPEmufEG+e98Lx+DvDmm+BNBk1TWfFuneKPF+h+IDpPhrxDq3hbR/E+mWdvd3f5Pm/0quB8mxuaU8Rwj4n4nJ8l48zLwwzPinL+E6OOyGhx7g8fWyvL+G8PGhmzzzNMVxDmMMJgcmxWUZLmGWrGZrlmDzfHZTi61WhQ+kw3h1m+KpYdwzLh+GKxWT0OIKGX1synSxk8nq0Y4itjpueGWEw9PBUHUq4qnicXSrulh69XDUsRShGZyV7+wP49t/GXgrRbT4xfs86t8O/HPwn8UfGvTP2gbPxz4ltPgzZeAPBHibV/BXjC81S613wNo/xEj1nw/4y0ebwvN4Xsfh3qHiPU9VvNN/sDS9Wsr6K8r2aH0iuHauR59jq/BHiXg+JuHuMcp4DxfhvX4fyutxziOI8+yrBZ9klDCUcu4gxvDM8DmWR46nm1PN8RxNh8rwmDoYr+0sVgq+GnQjyz4Hx0cXg6Mc2yGrgMbleJzmnnsMbiIZRDAYPE1cHi51ZVsHSx6rUMXRlhnhqeAqYipVnT9hTrQnGYl1+wH8T7bx+fDA+I/wSm+HkfwT039ou4/aKHivxNa/Ayz+DOr+Jm8C6b4xutT1LwTYfEOOa++Ia/wDCvrLwgPhw3ju+8XsumWXhi5jDXFFH6RvCdXhxZt/qvx7DiaXHuK8MaXhi8nyqt4gVuOcFlX+sOLyOlhcLn2J4alTw/DP/ABktfOnxRHh7D5Ini6+b0pWoyJcDZlHHfVv7QyZ4BZPT4gln/wBaxMclhlFXE/UqeLlUqYKnmClPH/7BDCPLnjp4v93DCyT5o6Gsf8E7/i1oWv8Aha11D4j/AAJXwB4m+Cd1+0XN8cLfx1rF38IfDHwcX4lav8JtJ8WeJNZt/CcviSG88TeNdNsdJ8P+DNI8J6x49uL/AMRaLod74X0/xMmt6HovPgfpMcG5jlub1sNwx4hPiTKuPaXhjDgCrw/gqHGua8bvhXBcZYzJsrwNXOY5VOjlWQ4rEYzMs8xuc4Lh2lhssx2YUM3xOUyy/MsfdbgLNKFfDRqZhkn1HE5PLP3nMcbVnlWGyn+0auVUsViK0cK8QpYnGU4UqGEpYWtjpTxFGjPDU8Sq9Cl82fHH4HeK/gN4q0nw54k1Xwp4n0zxT4T0Xx94E8d+A9Yl17wN4/8AA+vve22m+KPC2qXVjpWpSWP9p6XrOhahZazo+ja5oviDRNY0TWdI0/UtOubdP1LgDj/J/ETKMbmmV4POMpxWUZzj+HOIeHuIsFDLuIOHM/y2NCrispzfCUa+LwscR9UxeBzDDV8Djcfl+Oy3H4HH4HG4nC4qlVl89nOTYnJMTSw+Iq4XE08ThaOOwWNwNV18FjsFXc408ThqkqdKo4e0pVqM4VaNGtRr0atGtShUpyieNV9weQFABQAUAf/S/nnr/toP8nwoAKAPpD9kn4w+GfgL8fPCPxT8YWOu6l4f0DRfiPp15Z+GrbT7zWZZ/GHww8ZeCtMe2t9U1PR7J4oNV8RWVxfNLqELx6fFdS28d1cpDaT/AJd4zcE5r4ieHWdcI5JiMvwuZZjjuF8VQr5pVxNHAwp5JxZkefYpVamEwuNxEZ1MHllenh1DDVFLEzowqOlSlOrD6HhXNsNkeeYTM8XCvUoUKOYU5ww8YTrN4vLcXg6bjGpVowajVxEJTvUi1BScVKSjCX0lD+2H8NI/H+neKm0Px0dPs/8Agm7rX7HkkI0zQPtp+Jepfsz6/wDBmHXI4z4mEB8Cr4o1W3v5dTa4XXxoKzXKeGX1BU0x/wAun4JcVS4bxeTrMOH/AKzX+lFgPG2E3i8x9guFcL4rZbx1Uy+Uv7JdRcQvKcHVw8MIqUsueYyp0Xm0MM5YuP0K4ty5Y6nivY432cPD2twk17Ohz/2jU4dr5RGsl9Yt9SWJqxqOo5Kv7BOSw7mlSOu+B37Xn7PPhzwF8F/BPxE0LxnpviT4X/s5/Gz4Y6B8ULX4SfDL4yj4XfFP4h/tHS/Frw98RvBvw3+IXjbw/wCHvF/k/Dy61zwJdX2t6j4X1nwpq/ii68Q+GI7290XTr6XxuP8AwW8S804i46z7hnMMjxWV8WeJ/AfFuY8J1uM+LOBv9beEOGvC+HBuZcL55xTw1kOZZnkvtOJqOX8Q0aGAw2bYHOMFlNHLM2dHD4/FYeHXkvFeQ4fA5Pg8fQxlPE5bw/nOW0MyjlWW5v8A2ZmeP4geaUMwwmX4/G4ehi7YCdbBTnWqYWthauJlXw3POjTmfVsP7RHw+/bVutV+DPh2H9pf4qwa9+xBoXwT+JWr3Phn4VS/H658QfBX9qOf4y/D34lfCH4eSfFyx034vSatb+KINP8AEn7POia3omveD/BsGpjwt4u8X2fgp/EL/jtTwz4k8CKWD45zOfhXwhUy7x9zHj3hXBUs14wp+HFLLeO/COnwNxLwrxrxNHgvE4rgmODqZTUxOV+JmOy/MMuzrPJ4V5xkuTVs+jlkfpo5/gOMJVMooLiLM41uDKGTZjVlh8seeyr5PxLLN8BmOVZe80hTzV1Y4mNPEZBRr0q+Ewkan1XF4qGCeIN74gftQ/D79je9+D/7Pmk6r8X/AIcaXN+wZpnwi8b+JLDwz8M/Gfx5+C/xCf8Aa3+Kfx98N6z4s+F+s+KovAy614k0TUrHVPGvwZu/H1rqXgjw/wDEyz0VPG914o8IC61Dz+G/CXiXxxw/GviTjcJwVxRi4fSJxfGmQZXiM24ryLw7474aj4McIeHOZ4HJuLcDk8uIXgMqx2FxGDyHjqjw7XwufZlwnXx8uH6WUZ17Chvj+JcBwlPKcho1c2y+k+CKWVY3EQw+XYzO8ox74qzPPcPWxWWVsSsF7bEUakKmMyieOjUweHzGNFY14nCc8/yd/bH/AGh4/wBojx/4Q1Ox8ZfEzx/ofw/+HWj+ANJ8W/FjSfCPhjxTrrQaz4g8Ua1qcXgnwNdat4a8CaLda/4n1N9K8J6f4k8UtZQq1/feINQ1PUr11/sbwP8ADSXhnw3nWExGR8KcOY/iPifHcSY3JuDsZnWbZRl6qYHLMowGFnn3EFHB5txFj6OW5ThFjM5xOV5Qq9RrD4fLMNhMLh1L8v4tz5Z/jsJVhi8xx1HA5fSwNLFZpSwmGxNdxrV8TWqLB4KdXD4KjKvianssLDEYnkX7ydedSrPl+Rq/Zz5UKACgAoA//9P+eev+2g/yfCgAoAKACgAoAKACgAoAKACgAoAKAP/U/nnr/toP8nwoAKACgAoAKACgAoAKACgAoAKACgD/1f556/7aD/J8KACgAoAKACgAoAKACgAoAKACgAoA/9k=" } }, { "type": "stack-view", "id": "stackview", "x": 449.23, "y": 90, "width": 135.19, "description": "", "display": true, "affect-bottom-margin": true, "follow-stretch": "none", "rows": [ { "id": "", "height": 30, "auto-stretch": false, "display": true, "items": [ { "type": "text", "id": "", "x": 25.33, "y": 6, "width": 86.5, "height": 18.96, "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "texts": [ "StackViewRow1" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 12, "line-height-ratio": "", "line-height": 13.2 } } ] }, { "id": "", "height": 30, "auto-stretch": false, "display": true, "items": [ { "type": "text", "id": "", "x": 25.98, "y": 6, "width": 86.5, "height": 18.96, "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "texts": [ "StackViewRow2" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 12, "line-height-ratio": "", "line-height": 13.2 } } ] } ] }, { "type": "line", "id": "", "x1": 10, "y1": 1, "x2": 584.59, "y2": 1, "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "style": { "border-width": 1, "border-color": "#000000", "border-style": "solid" } }, { "type": "rect", "id": "", "x": 285, "y": 90, "width": 140, "height": 50, "border-radius": 0, "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "style": { "border-color": "#aaaaaa", "border-style": "dashed", "fill-color": "none", "border-width": 1 } } ] }, { "id": "detail2", "type": "detail", "height": 120, "auto-stretch": true, "items": [ { "type": "text-block", "id": "text_block1", "x": 26, "y": 14.34, "width": 116.46, "height": 44.32, "description": "", "reference-id": "", "value": "", "multiple-line": true, "display": true, "format": { "base": "", "type": "" }, "follow-stretch": "none", "affect-bottom-margin": true, "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "overflow": "truncate", "word-wrap": "break-word", "font-size": 18, "line-height-ratio": "", "line-height": 19.8 } }, { "type": "text-block", "id": "text_block2", "x": 26, "y": 65.13, "width": 116.46, "height": 44.32, "description": "", "reference-id": "", "value": "", "multiple-line": true, "display": true, "format": { "base": "", "type": "" }, "follow-stretch": "none", "affect-bottom-margin": true, "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "overflow": "truncate", "word-wrap": "break-word", "font-size": 18, "line-height-ratio": "", "line-height": 19.8 } }, { "type": "image-block", "id": "image_block1", "x": 152, "y": 14.97, "width": 100, "height": 75, "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "style": { "position-x": "left", "position-y": "top" } }, { "type": "image-block", "id": "image_block2", "x": 259.95, "y": 15, "width": 100, "height": 75, "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "style": { "position-x": "left", "position-y": "top" } }, { "type": "line", "id": "", "x1": 10, "y1": 1, "x2": 584.59, "y2": 1, "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "style": { "border-width": 1, "border-color": "#000000", "border-style": "solid" } }, { "type": "image-block", "id": "image_block3", "x": 366.37, "y": 14.27, "width": 100, "height": 75, "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "style": { "position-x": "left", "position-y": "top" } }, { "type": "image-block", "id": "image_block4", "x": 473.53, "y": 14.64, "width": 100, "height": 75, "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "style": { "position-x": "left", "position-y": "top" } } ] } ], "state": { "layout-guides": [] } } ================================================ FILE: test/section_report/features/item_parameters/test_feature.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::SectionReport::TestItemParametersFeature < Thinreports::FeatureTest[__dir__] feature do params = { type: :section, layout_file: template_path, params: { groups: [ { details: [ { id: :detail1, items: { image_block: image_block_jpg.to_path } }, { id: :detail1, items: { text: { styles: { font_size: 24, align: :center, valign: :middle, color: '#ff0000', bold: true, italic: true, underline: true, linethrough: true } }, text_block: { styles: { font_size: 24, align: :right, valign: :bottom, color: 'black', bold: false, italic: false, underline: false, linethrough: false } }, image_block: image_block_jpg.to_path } }, { id: :detail1, items: { image_block: { value: image_block_jpg.to_path, styles: { offset_x: 20, offset_y: -20 } } } }, { id: :detail1, items: { rect: { display: false }, ellipse: { display: false }, text: { display: false }, line: { display: false }, image: { display: false }, text_block: { display: false }, image_block: { display: false }, stackview: { display: false } } }, { id: :detail2, items: { text_block1: 'text-block1', text_block2: { value: 'text-block2' }, image_block1: image_block_jpg.to_path, image_block2: StringIO.new(image_block_jpg.binread), image_block3: { value: image_block_jpg.to_path, }, image_block4: { value: StringIO.new(image_block_jpg.binread) } } } ] } ] } } assert_pdf Thinreports.generate(params) end private def image_block_jpg dir.join('image-block.jpg') end end ================================================ FILE: test/section_report/features/multiple_groups/README.md ================================================ # Multiple Groups Multiple parameter sets can be passed as a group and drawn into a single PDF. - [Example code](test_feature.rb) - [Example template file](template.tlf) - [Example PDF](expect.pdf) The main functions of groups are as follows: - Page break for each group - Headers and footers for which the "show every page" is disabled will be drawn only once per group ================================================ FILE: test/section_report/features/multiple_groups/template.tlf ================================================ { "schema-version": "1.0", "last-modified-by": "1.0.0-sectionreport.1", "title": "", "report": { "orientation": "portrait", "paper-type": "A4", "width": 0, "height": 0, "margin": [ 20, 20, 20, 20 ] }, "sections": [ { "id": "header", "type": "header", "height": 60, "display": true, "auto-stretch": true, "every-page": false, "items": [ { "type": "text-block", "id": "text", "x": 20, "y": 20, "width": 550, "height": 20, "description": "", "reference-id": "", "value": "", "multiple-line": true, "display": true, "format": { "base": "", "type": "" }, "follow-stretch": "none", "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "overflow": "truncate", "word-wrap": "break-word", "font-size": 18, "line-height-ratio": "", "line-height": 19.8 }, "affect-bottom-margin": true } ] }, { "id": "detail", "type": "detail", "height": 60, "auto-stretch": true, "items": [ { "type": "text-block", "id": "text", "x": 20, "y": 20, "width": 550, "height": 20, "description": "", "reference-id": "", "value": "", "multiple-line": true, "display": true, "format": { "base": "", "type": "" }, "follow-stretch": "none", "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "overflow": "truncate", "word-wrap": "break-word", "font-size": 18, "line-height-ratio": "", "line-height": 19.8 }, "affect-bottom-margin": true } ] }, { "id": "footer", "type": "footer", "height": 60, "display": true, "auto-stretch": true, "items": [ { "type": "text", "id": "", "x": 20, "y": 20, "width": 200, "height": 20, "description": "", "display": true, "follow-stretch": "none", "texts": [ "Footer" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 18, "line-height-ratio": "", "line-height": 19.8 }, "affect-bottom-margin": true } ] } ], "state": { "layout-guides": [] } } ================================================ FILE: test/section_report/features/multiple_groups/test_feature.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::SectionReport::TestMultipleGroupsFeature < Thinreports::FeatureTest[__dir__] feature do params = { type: :section, layout_file: template_path, params: { groups: [ { headers: { header: { items: { text: 'Header of Group 1' } } }, details: 20.times.map { { id: :detail, items: { text: 'Detail of Group 1' } } }, footers: { footer: { display: false } } }, { headers: { header: { items: { text: 'Header of Group 2' } } }, details: 10.times.map { { id: :detail, items: { text: 'Detail of Group 2' } } } } ] } } assert_pdf Thinreports.generate(params) end end ================================================ FILE: test/section_report/features/nonexistent_id/README.md ================================================ # Nonexistent Id 存在しない section や item、stack-view-row の id がパラメータに含まれる場合、それらは無視される。 - [Example code](test_feature.rb) - [Example template file](template.tlf) - [Example PDF](expect.pdf) template に存在しない section や item、stack-view-row は描画されず、エラーも発生しない。 ================================================ FILE: test/section_report/features/nonexistent_id/template.tlf ================================================ { "schema-version": "1.0", "last-modified-by": "1.0.0-sectionreport.1", "title": "", "report": { "orientation": "portrait", "paper-type": "A4", "width": 0, "height": 0, "margin": [ 20, 20, 20, 20 ] }, "sections": [ { "id": "header", "type": "header", "height": 100, "display": true, "auto-stretch": true, "every-page": false, "items": [ { "type": "text", "id": "", "x": 30, "y": 30, "width": 87.31, "height": 51.18, "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "texts": [ "header" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 18, "line-height-ratio": "", "line-height": 19.8 } }, { "type": "text-block", "id": "text", "x": 136.53, "y": 30, "width": 86.81, "height": 34.69, "description": "", "reference-id": "", "value": "", "multiple-line": true, "display": true, "format": { "base": "", "type": "" }, "follow-stretch": "none", "affect-bottom-margin": true, "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "overflow": "truncate", "word-wrap": "break-word", "font-size": 18, "line-height-ratio": "", "line-height": 19.8 } }, { "type": "line", "id": "", "x1": 20, "y1": 1, "x2": 570, "y2": 1, "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "style": { "border-width": 1, "border-color": "#000000", "border-style": "solid" } } ] }, { "id": "detail", "type": "detail", "height": 100, "auto-stretch": true, "items": [ { "type": "text", "id": "", "x": 30, "y": 30, "width": 87.31, "height": 51.18, "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "texts": [ "detail" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 18, "line-height-ratio": "", "line-height": 19.8 } }, { "type": "text-block", "id": "text", "x": 136.53, "y": 30, "width": 86.81, "height": 34.69, "description": "", "reference-id": "", "value": "", "multiple-line": true, "display": true, "format": { "base": "", "type": "" }, "follow-stretch": "none", "affect-bottom-margin": true, "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "overflow": "truncate", "word-wrap": "break-word", "font-size": 18, "line-height-ratio": "", "line-height": 19.8 } }, { "type": "stack-view", "id": "stackview", "x": 284.82, "y": 17.66, "width": 224.05, "description": "", "display": true, "affect-bottom-margin": true, "follow-stretch": "none", "rows": [ { "id": "row1", "height": 46.77, "auto-stretch": false, "display": true, "items": [ { "type": "text", "id": "row1", "x": 13.66, "y": 12.27, "width": 47.73, "height": 21.65, "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "texts": [ "row1" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 18, "line-height-ratio": "", "line-height": 19.8 } }, { "type": "text-block", "id": "text", "x": 77.42, "y": 12.27, "width": 75.15, "height": 22.63, "description": "", "reference-id": "", "value": "", "multiple-line": true, "display": true, "format": { "base": "", "type": "" }, "follow-stretch": "none", "affect-bottom-margin": true, "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "overflow": "truncate", "word-wrap": "break-word", "font-size": 18, "line-height-ratio": "", "line-height": 19.8 } } ] } ] }, { "type": "line", "id": "", "x1": 20, "y1": 1, "x2": 570, "y2": 1, "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "style": { "border-width": 1, "border-color": "#000000", "border-style": "solid" } } ] }, { "id": "footer", "type": "footer", "height": 100, "display": true, "auto-stretch": true, "items": [ { "type": "text", "id": "", "x": 30, "y": 30, "width": 87.31, "height": 51.18, "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "texts": [ "footer" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 18, "line-height-ratio": "", "line-height": 19.8 } }, { "type": "text-block", "id": "text", "x": 136.53, "y": 30, "width": 86.81, "height": 34.69, "description": "", "reference-id": "", "value": "", "multiple-line": true, "display": true, "format": { "base": "", "type": "" }, "follow-stretch": "none", "affect-bottom-margin": true, "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "overflow": "truncate", "word-wrap": "break-word", "font-size": 18, "line-height-ratio": "", "line-height": 19.8 } }, { "type": "line", "id": "", "x1": 20, "y1": 1, "x2": 570, "y2": 1, "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "style": { "border-width": 1, "border-color": "#000000", "border-style": "solid" } } ] } ], "state": { "layout-guides": [] } } ================================================ FILE: test/section_report/features/nonexistent_id/test_feature.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::SectionReport::TestNonExistentIdFeature < Thinreports::FeatureTest[__dir__] feature do params = { type: :section, layout_file: template_path, params: { groups: [ { headers: { header: { items: { text: 'text', nonexistent_item: 'nonexistent items are ignored' } }, nonexistent_header: { items: { any_item: 'nonexistent sections and items within them are ignored' } } }, details: [ { id: 'detail', items: { text: 'text', nonexistent_item: 'nonexistent items are ignored', stackview: { rows: { row1: { items: { text: 'text', nonexistent_item: 'nonexistent items are ignored', } }, nonexistent_row: { items: { any_item: 'nonexistent rows and items within them are ignored' } } } } } }, { id: 'nonexistent_detail', items: { any_item: 'nonexistent sections and items within them are ignored' } } ], footers: { footer: { items: { text: 'text', nonexistent_item: 'nonexistent items are ignored' } }, nonexistent_footer: { items: { any_item: 'nonexistent sections and items within them are ignored' } } } } ] } } assert_pdf Thinreports.generate(params) end end ================================================ FILE: test/section_report/features/section_auto_stretch/README.md ================================================ # Section Auto Stretch By enabling auto-stretch, the height of the section can be automatically stretched according to the content after drawing. - [Example code](test_feature.rb) - [Example template file](template.tlf) - [Example PDF](expect.pdf) ## Calculating the height when stretching ### Bottom margin and bottom position of content The height of the section after it is stretched is determined by the bottom margin and the bottom position of the content. Bottom Position of Content - The bottom position of the item in the section (except for items for which "Affect bottom margin" is disabled) - Varies depending on the drawing result Bottom Margin - The height or area between the bottom position of the content and the bottom position of the section - Determined and fixed by the content of the template definition See [Section Bottom Margin](../section_report_section_bottom_margin/README.md) for details. ### Calculating the height The height of the section is defined as follows: ``` height of section = bottom of content + height of bottom margin ``` When an item such as text-block or stack-view is stretched, the bottom position of the content may change, and if auto-stretch of the section is enabled, the height of the section will be changed based on the changed bottom position of the content. ## Expanding the height Expanding the height occurs in the following cases: - When the height of the text-block after drawing becomes higher than the defined height, and as a result, the bottom position of the section content becomes larger - When the height of stack-view becomes higher than the defined height, and as a result, the bottom position of the section content becomes larger In these cases, the height of the section is expanded as follows: ![](images/auto-stretch-expand.png) ## Shrinking the height Shrinking the height occurs in the following cases: - When the height of the stack-view becomes smaller than the defined height, and as a result, the bottom position of the section content becomes smaller - When the image of an image-block whose vertical position is top becomes smaller than the height of the defined area, and as a result, the bottom position of the section content becomes smaller In these cases, the height of the section is shrinked as follows: ![](images/auto-stretch-shrink.png) ================================================ FILE: test/section_report/features/section_auto_stretch/template.tlf ================================================ { "schema-version": "1.0", "last-modified-by": "1.0.0-sectionreport.1", "title": "", "report": { "orientation": "portrait", "paper-type": "A4", "width": 0, "height": 0, "margin": [ 20, 20, 20, 20 ] }, "sections": [ { "id": "header1", "type": "header", "height": 70, "display": true, "auto-stretch": true, "every-page": false, "items": [ { "type": "text", "id": "", "x": 31.8, "y": 10, "width": 336.69, "height": 14, "description": "", "display": true, "follow-stretch": "none", "texts": [ "Expand height by text-block whose overflow is expand" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 14, "line-height-ratio": "", "line-height": 15.4 }, "affect-bottom-margin": true }, { "type": "text-block", "id": "text_overflow_expand", "x": 45.97, "y": 35, "width": 330, "height": 24.17, "description": "", "reference-id": "", "value": "", "multiple-line": true, "display": true, "format": { "base": "", "type": "" }, "follow-stretch": "none", "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "overflow": "expand", "word-wrap": "break-word", "font-size": 15, "line-height-ratio": "", "line-height": 16.5 }, "affect-bottom-margin": true }, { "type": "text", "id": "", "x": 434.32, "y": 10, "width": 140.83, "height": 21.67, "description": "", "display": true, "follow-stretch": "none", "texts": [ "Header1 (auto-stretch: on)" ], "style": { "font-family": [ "Helvetica" ], "color": "#aaaaaa", "font-style": [], "text-align": "right", "vertical-align": "top", "letter-spacing": "", "font-size": 12, "line-height-ratio": "", "line-height": 13.2 }, "affect-bottom-margin": true } ] }, { "id": "header2", "type": "header", "height": 70, "display": true, "auto-stretch": false, "every-page": false, "items": [ { "type": "text", "id": "", "x": 31.8, "y": 10, "width": 353.03, "height": 14, "description": "", "display": true, "follow-stretch": "none", "texts": [ "Does not expand by text-block whose overflow is expand" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 14, "line-height-ratio": "", "line-height": 15.4 }, "affect-bottom-margin": true }, { "type": "text", "id": "", "x": 440.15, "y": 10, "width": 140.83, "height": 21.67, "description": "", "display": true, "follow-stretch": "none", "texts": [ "Header2 (auto-stretch: off)" ], "style": { "font-family": [ "Helvetica" ], "color": "#aaaaaa", "font-style": [], "text-align": "right", "vertical-align": "top", "letter-spacing": "", "font-size": 12, "line-height-ratio": "", "line-height": 13.2 }, "affect-bottom-margin": true }, { "type": "text-block", "id": "text_overflow_expand", "x": 45.97, "y": 35, "width": 330, "height": 24.17, "description": "", "reference-id": "", "value": "", "multiple-line": true, "display": true, "format": { "base": "", "type": "" }, "follow-stretch": "none", "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "overflow": "expand", "word-wrap": "break-word", "font-size": 15, "line-height-ratio": "", "line-height": 16.5 }, "affect-bottom-margin": true }, { "type": "line", "id": "", "x1": 17.64, "y1": 1, "x2": 581.81, "y2": 1, "description": "", "display": true, "follow-stretch": "none", "style": { "border-width": 1, "border-color": "#aaaaaa", "border-style": "solid" }, "affect-bottom-margin": true } ] }, { "id": "detail1", "type": "detail", "height": 110, "auto-stretch": true, "items": [ { "type": "text", "id": "", "x": 31.8, "y": 15, "width": 178.97, "height": 14, "description": "", "display": true, "follow-stretch": "none", "texts": [ "Expand height by stack-view" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 14, "line-height-ratio": "", "line-height": 15.4 }, "affect-bottom-margin": true }, { "type": "stack-view", "id": "stackview", "x": 33.47, "y": 40, "width": 400, "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "rows": [ { "id": "row1", "height": 30, "auto-stretch": false, "display": true, "items": [ { "type": "text", "id": "", "x": 15.01, "y": 5, "width": 71.67, "height": 18, "description": "", "display": true, "follow-stretch": "none", "texts": [ "Row1" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 18, "line-height-ratio": "", "line-height": 19.8 }, "affect-bottom-margin": true } ] }, { "id": "row2", "height": 30, "auto-stretch": true, "display": true, "items": [ { "type": "text-block", "id": "text_overflow_expand", "x": 240, "y": 5, "width": 140, "height": 20, "description": "", "reference-id": "", "value": "", "multiple-line": true, "display": true, "format": { "base": "", "type": "" }, "follow-stretch": "none", "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "overflow": "expand", "word-wrap": "break-word", "font-size": 16, "line-height-ratio": "", "line-height": 17.6 }, "affect-bottom-margin": true }, { "type": "text", "id": "", "x": 15.84, "y": 5, "width": 188.05, "height": 18, "description": "", "display": true, "follow-stretch": "none", "texts": [ "Row2 (auto-stretch: on)" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 18, "line-height-ratio": "", "line-height": 19.8 }, "affect-bottom-margin": true } ] } ] }, { "type": "text", "id": "", "x": 449.31, "y": 10, "width": 132.17, "height": 21.67, "description": "", "display": true, "follow-stretch": "none", "texts": [ "Detail1 (auto-stretch: on)" ], "style": { "font-family": [ "Helvetica" ], "color": "#aaaaaa", "font-style": [], "text-align": "right", "vertical-align": "top", "letter-spacing": "", "font-size": 12, "line-height-ratio": "", "line-height": 13.2 }, "affect-bottom-margin": true }, { "type": "line", "id": "", "x1": 17.64, "y1": 1, "x2": 581.81, "y2": 1, "description": "", "display": true, "follow-stretch": "none", "style": { "border-width": 1, "border-color": "#aaaaaa", "border-style": "solid" }, "affect-bottom-margin": true } ] }, { "id": "detail2", "type": "detail", "height": 90, "auto-stretch": true, "items": [ { "type": "text", "id": "", "x": 449.31, "y": 13.33, "width": 132.17, "height": 21.67, "description": "", "display": true, "follow-stretch": "none", "texts": [ "Detail2 (auto-stretch: on)" ], "style": { "font-family": [ "Helvetica" ], "color": "#aaaaaa", "font-style": [], "text-align": "right", "vertical-align": "top", "letter-spacing": "", "font-size": 12, "line-height-ratio": "", "line-height": 13.2 }, "affect-bottom-margin": true }, { "type": "line", "id": "", "x1": 17.64, "y1": 1, "x2": 581.81, "y2": 1, "description": "", "display": true, "follow-stretch": "none", "style": { "border-width": 1, "border-color": "#aaaaaa", "border-style": "solid" }, "affect-bottom-margin": true }, { "type": "text-block", "id": "text_overflow_expand", "x": 45.97, "y": 35, "width": 200, "height": 24.17, "description": "", "reference-id": "", "value": "", "multiple-line": true, "display": true, "format": { "base": "", "type": "" }, "follow-stretch": "none", "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "overflow": "expand", "word-wrap": "break-word", "font-size": 15, "line-height-ratio": "", "line-height": 16.5 }, "affect-bottom-margin": true }, { "type": "text", "id": "", "x": 31.8, "y": 15, "width": 208.25, "height": 14, "description": "", "display": true, "follow-stretch": "none", "texts": [ "Expand height with multiple items" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 14, "line-height-ratio": "", "line-height": 15.4 }, "affect-bottom-margin": true }, { "type": "rect", "id": "", "x": 296.81, "y": 35.84, "width": 105.83, "height": 45, "border-radius": 0, "description": "", "display": true, "follow-stretch": "none", "style": { "border-color": "#000000", "border-style": "solid", "fill-color": "#ffffff", "border-width": 1 }, "affect-bottom-margin": true } ] }, { "id": "detail3", "type": "detail", "height": 150, "auto-stretch": true, "items": [ { "type": "text", "id": "", "x": 31.8, "y": 13.33, "width": 179.73, "height": 14, "description": "", "display": true, "follow-stretch": "none", "texts": [ "Reduce height by stack-view" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 14, "line-height-ratio": "", "line-height": 15.4 }, "affect-bottom-margin": true }, { "type": "stack-view", "id": "stackview", "x": 33.47, "y": 40, "width": 280, "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "rows": [ { "id": "row1", "height": 70, "auto-stretch": false, "display": true, "items": [ { "type": "text", "id": "", "x": 15.01, "y": 5, "width": 213.06, "height": 18, "description": "", "display": true, "follow-stretch": "none", "texts": [ "Row1 (hide when drawing)" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 18, "line-height-ratio": "", "line-height": 19.8 }, "affect-bottom-margin": true } ] }, { "id": "row2", "height": 30, "auto-stretch": false, "display": true, "items": [ { "type": "text", "id": "", "x": 15.84, "y": 5, "width": 71.67, "height": 18, "description": "", "display": true, "follow-stretch": "none", "texts": [ "Row2" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 18, "line-height-ratio": "", "line-height": 19.8 }, "affect-bottom-margin": true } ] } ] }, { "type": "text", "id": "", "x": 449.31, "y": 13.33, "width": 132.17, "height": 21.67, "description": "", "display": true, "follow-stretch": "none", "texts": [ "Detail3 (auto-stretch: on)" ], "style": { "font-family": [ "Helvetica" ], "color": "#aaaaaa", "font-style": [], "text-align": "right", "vertical-align": "top", "letter-spacing": "", "font-size": 12, "line-height-ratio": "", "line-height": 13.2 }, "affect-bottom-margin": true }, { "type": "line", "id": "", "x1": 17.64, "y1": 1, "x2": 581.81, "y2": 1, "description": "", "display": true, "follow-stretch": "none", "style": { "border-width": 1, "border-color": "#aaaaaa", "border-style": "solid" }, "affect-bottom-margin": true } ] }, { "id": "footer1", "type": "footer", "height": 140, "display": true, "auto-stretch": true, "items": [ { "type": "text", "id": "", "x": 31.8, "y": 10, "width": 334.34, "height": 14, "description": "", "display": true, "follow-stretch": "none", "texts": [ "Reduce height by image-block whose position-y is top" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 14, "line-height-ratio": "", "line-height": 15.4 }, "affect-bottom-margin": true }, { "type": "text", "id": "", "x": 445.15, "y": 14.16, "width": 136.17, "height": 21.67, "description": "", "display": true, "follow-stretch": "none", "texts": [ "Footer1 (auto-stretch: on)" ], "style": { "font-family": [ "Helvetica" ], "color": "#aaaaaa", "font-style": [], "text-align": "right", "vertical-align": "top", "letter-spacing": "", "font-size": 12, "line-height-ratio": "", "line-height": 13.2 }, "affect-bottom-margin": true }, { "type": "image-block", "id": "image200x100", "x": 50.98, "y": 35, "width": 200, "height": 100, "description": "", "display": true, "follow-stretch": "none", "style": { "position-x": "left", "position-y": "top" }, "affect-bottom-margin": true }, { "type": "line", "id": "", "x1": 17.64, "y1": 1, "x2": 581.81, "y2": 1, "description": "", "display": true, "follow-stretch": "none", "style": { "border-width": 1, "border-color": "#aaaaaa", "border-style": "solid" }, "affect-bottom-margin": true } ] }, { "id": "footer2", "type": "footer", "height": 140, "display": true, "auto-stretch": false, "items": [ { "type": "text", "id": "", "x": 31.8, "y": 10, "width": 356.45, "height": 14, "description": "", "display": true, "follow-stretch": "none", "texts": [ "Does not reduce by image-block whose position-y is top" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 14, "line-height-ratio": "", "line-height": 15.4 }, "affect-bottom-margin": true }, { "type": "text", "id": "", "x": 386.81, "y": 13.33, "width": 193.7, "height": 21.67, "description": "", "display": true, "follow-stretch": "none", "texts": [ "Footer2 (auto-stretch: off)" ], "style": { "font-family": [ "Helvetica" ], "color": "#aaaaaa", "font-style": [], "text-align": "right", "vertical-align": "top", "letter-spacing": "", "font-size": 12, "line-height-ratio": "", "line-height": 13.2 }, "affect-bottom-margin": true }, { "type": "image-block", "id": "image200x100", "x": 50.98, "y": 35, "width": 200, "height": 100, "description": "", "display": true, "follow-stretch": "none", "style": { "position-x": "left", "position-y": "top" }, "affect-bottom-margin": true }, { "type": "line", "id": "", "x1": 17.64, "y1": 1, "x2": 581.81, "y2": 1, "description": "", "display": true, "follow-stretch": "none", "style": { "border-width": 1, "border-color": "#aaaaaa", "border-style": "solid" }, "affect-bottom-margin": true } ] }, { "id": "footer3", "type": "footer", "height": 140, "display": true, "auto-stretch": true, "items": [ { "type": "text", "id": "", "x": 31.8, "y": 10, "width": 370.13, "height": 14, "description": "", "display": true, "follow-stretch": "none", "texts": [ "Does not reduce by image-block whose position-y is not top" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 14, "line-height-ratio": "", "line-height": 15.4 }, "affect-bottom-margin": true }, { "type": "text", "id": "", "x": 386.81, "y": 13.33, "width": 193.7, "height": 21.67, "description": "", "display": true, "follow-stretch": "none", "texts": [ "Footer3 (auto-stretch: on)" ], "style": { "font-family": [ "Helvetica" ], "color": "#aaaaaa", "font-style": [], "text-align": "right", "vertical-align": "top", "letter-spacing": "", "font-size": 12, "line-height-ratio": "", "line-height": 13.2 }, "affect-bottom-margin": true }, { "type": "line", "id": "", "x1": 17.64, "y1": 1, "x2": 581.81, "y2": 1, "description": "", "display": true, "follow-stretch": "none", "style": { "border-width": 1, "border-color": "#aaaaaa", "border-style": "solid" }, "affect-bottom-margin": true }, { "type": "image-block", "id": "image200x100", "x": 50.98, "y": 35, "width": 200, "height": 100, "description": "", "display": true, "follow-stretch": "none", "style": { "position-x": "left", "position-y": "middle" }, "affect-bottom-margin": true } ] }, { "id": "footer4", "type": "footer", "height": 150, "display": true, "auto-stretch": true, "items": [ { "type": "text", "id": "", "x": 31.8, "y": 13.33, "width": 209.02, "height": 14, "description": "", "display": true, "follow-stretch": "none", "texts": [ "Reduce height with multiple items" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 14, "line-height-ratio": "", "line-height": 15.4 }, "affect-bottom-margin": true }, { "type": "text", "id": "", "x": 386.81, "y": 13.33, "width": 193.7, "height": 21.67, "description": "", "display": true, "follow-stretch": "none", "texts": [ "Footer4 (auto-stretch: on)" ], "style": { "font-family": [ "Helvetica" ], "color": "#aaaaaa", "font-style": [], "text-align": "right", "vertical-align": "top", "letter-spacing": "", "font-size": 12, "line-height-ratio": "", "line-height": 13.2 }, "affect-bottom-margin": true }, { "type": "stack-view", "id": "stackview", "x": 33.47, "y": 40, "width": 150, "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "rows": [ { "id": "row1", "height": 50, "auto-stretch": false, "display": true, "items": [ { "type": "text", "id": "", "x": 15.01, "y": 5, "width": 71.67, "height": 18, "description": "", "display": true, "follow-stretch": "none", "texts": [ "Row1" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 18, "line-height-ratio": "", "line-height": 19.8 }, "affect-bottom-margin": true } ] }, { "id": "row2", "height": 50, "auto-stretch": false, "display": true, "items": [ { "type": "text", "id": "", "x": 15.84, "y": 5, "width": 100, "height": 18, "description": "", "display": true, "follow-stretch": "none", "texts": [ "Row2" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 18, "line-height-ratio": "", "line-height": 19.8 }, "affect-bottom-margin": true } ] } ] }, { "type": "rect", "id": "", "x": 245.97, "y": 41.67, "width": 114.17, "height": 80, "border-radius": 0, "description": "", "display": true, "follow-stretch": "none", "style": { "border-color": "#000000", "border-style": "solid", "fill-color": "#ffffff", "border-width": 1 }, "affect-bottom-margin": true }, { "type": "line", "id": "", "x1": 17.64, "y1": 1, "x2": 581.81, "y2": 1, "description": "", "display": true, "follow-stretch": "none", "style": { "border-width": 1, "border-color": "#aaaaaa", "border-style": "solid" }, "affect-bottom-margin": true } ] }, { "id": "footer5", "type": "footer", "height": 30, "display": true, "auto-stretch": false, "items": [ { "type": "text", "id": "", "x": 386.81, "y": 5, "width": 193.7, "height": 21.67, "description": "", "display": true, "follow-stretch": "none", "texts": [ "dummy" ], "style": { "font-family": [ "Helvetica" ], "color": "#aaaaaa", "font-style": [], "text-align": "right", "vertical-align": "top", "letter-spacing": "", "font-size": 12, "line-height-ratio": "", "line-height": 13.2 }, "affect-bottom-margin": true }, { "type": "line", "id": "", "x1": 17.64, "y1": 1, "x2": 581.81, "y2": 1, "description": "", "display": true, "follow-stretch": "none", "style": { "border-width": 1, "border-color": "#aaaaaa", "border-style": "solid" }, "affect-bottom-margin": true } ] } ], "state": { "layout-guides": [] } } ================================================ FILE: test/section_report/features/section_auto_stretch/test_feature.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::SectionReport::TestSectionAutoStretchFeature < Thinreports::FeatureTest[__dir__] feature do params = { type: :section, layout_file: template_path, params: { groups: [ { headers: { header1: { items: { text_overflow_expand: 'Extended text box height with long text. ' * 3 } }, header2: { items: { text_overflow_expand: 'Extended text box height with long text. ' * 3 } } }, details: [ { id: 'detail1', items: { stackview: { rows: { row2: { items: { text_overflow_expand: 'Extended text box height with long text. ' } } } } } }, { id: 'detail2', items: { text_overflow_expand: 'Extended text box height with long text. ' * 3 } }, { id: 'detail3', items: { stackview: { rows: { row1: { display: false } } } } } ], footers: { footer1: { items: { image200x100: image50x50 } }, footer2: { items: { image200x100: image50x50 } }, footer3: { items: { image200x100: image50x50 } }, footer4: { items: { stackview: { rows: { row2: { display: false } } } } } } } ] } } assert_pdf Thinreports.generate(params) end def image50x50 StringIO.new(dir.join('50x50.jpg').binread) end end ================================================ FILE: test/section_report/features/section_bottom_margin/README.md ================================================ # Section Bottom Margin The bottom margin of a section is the distance between the bottom position of the section content and the bottom position of the section. - [Example code](test_feature.rb) - [Example template file](template.tlf) - [Example PDF](expect.pdf) An item whose affect-bottom-margin is disabled does not affect the calculation of the bottom margin. ![](images/bottom-margin-diagram-1.png) In the example above, item3 does not affect the bottom margin because affect-bottom-margin is disabled. Since the bottom position of item2 is the bottom position of the section content, the space between the bottoom position of item2 and the bottom position of the section (the gray shaded area) is the bottom margin of this section. When auto-stretch is enabled, the section will be automatically stretched while keepking the bottom margin of the section. See [Section Auto Stretch](../section_report_section_auto_stretch/README.md) for details. ================================================ FILE: test/section_report/features/section_bottom_margin/template.tlf ================================================ { "schema-version": "1.0", "last-modified-by": "1.0.0-sectionreport.1", "title": "", "report": { "orientation": "portrait", "paper-type": "A4", "width": 0, "height": 0, "margin": [ 20, 20, 20, 20 ] }, "sections": [ { "id": "detail", "type": "detail", "height": 200, "auto-stretch": true, "items": [ { "type": "line", "id": "", "x1": 0, "y1": 0, "x2": 595.28, "y2": 0, "description": "", "display": true, "follow-stretch": "none", "style": { "border-width": 1, "border-color": "#000000", "border-style": "solid" }, "affect-bottom-margin": true }, { "type": "text-block", "id": "textblock", "x": 40, "y": 40, "width": 100, "height": 20, "description": "", "reference-id": "", "value": "", "multiple-line": true, "display": true, "format": { "base": "", "type": "" }, "follow-stretch": "none", "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "overflow": "expand", "word-wrap": "break-word", "font-size": 18, "line-height-ratio": "", "line-height": 19.8 }, "affect-bottom-margin": true }, { "type": "rect", "id": "", "x": 200, "y": 20, "width": 100, "height": 120, "border-radius": 0, "description": "", "display": true, "follow-stretch": "none", "style": { "border-color": "#000000", "border-style": "solid", "fill-color": "#ffffff", "border-width": 1 }, "affect-bottom-margin": true }, { "type": "rect", "id": "", "x": 360, "y": 40, "width": 160, "height": 150, "border-radius": 0, "description": "", "display": true, "follow-stretch": "none", "style": { "border-color": "#000000", "border-style": "dashed", "fill-color": "#ffffff", "border-width": 1 }, "affect-bottom-margin": false }, { "type": "text", "id": "", "x": 370, "y": 50, "width": 112.2, "height": 37.8, "description": "", "display": true, "follow-stretch": "none", "texts": [ "affect-bottom-", "margin: false" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 18, "line-height-ratio": "", "line-height": 19.8 }, "affect-bottom-margin": true } ] }, { "id": "", "type": "footer", "height": 50, "display": true, "auto-stretch": true, "items": [ { "type": "line", "id": "", "x1": 0, "y1": 0, "x2": 595.28, "y2": 0, "description": "", "display": true, "follow-stretch": "none", "style": { "border-width": 1, "border-color": "#000000", "border-style": "solid" }, "affect-bottom-margin": true } ] } ], "state": { "layout-guides": [] } } ================================================ FILE: test/section_report/features/section_bottom_margin/test_feature.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::SectionReport::TestSectionBottomMarginFeature < Thinreports::FeatureTest[__dir__] feature do params = { type: :section, layout_file: template_path, params: { groups: [ { details: [ { id: :detail, items: { textblock: 'short text' } }, { id: :detail, items: { textblock: 'long ' * 19 + 'text' } } ] } ] } } assert_pdf Thinreports.generate(params) end end ================================================ FILE: test/section_report/features/section_parameters/README.md ================================================ # Section Parameter Some properties in the definition of each section can be changed by parameters. - [Example code](test_feature.rb) - [Example template file](template.tlf) - [Example PDF](expect.pdf) ## Display Headers and footers can be shown or hidden with the display property. ``` header_and_footer: { display: Boolean } ``` ## Minimum height The min_height property can be used to set the minimum height of a section. ``` header_and_footer: { min_height: Number } ``` ``` { id: :detail, min_height: Number } ``` ================================================ FILE: test/section_report/features/section_parameters/template.tlf ================================================ { "schema-version": "1.0", "last-modified-by": "1.0.0-sectionreport.1", "title": "", "report": { "orientation": "portrait", "paper-type": "A4", "width": 0, "height": 0, "margin": [ 20, 20, 20, 20 ] }, "sections": [ { "id": "header1", "type": "header", "height": 50, "display": true, "auto-stretch": false, "every-page": false, "items": [ { "type": "text", "id": "", "x": 23.13, "y": 15, "width": 303.92, "height": 18, "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "texts": [ "(header1) display: true in the template" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 18, "line-height-ratio": "", "line-height": 19.8 } }, { "type": "line", "id": "", "x1": 20.31, "y1": 1, "x2": 573.04, "y2": 1, "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "style": { "border-width": 1, "border-color": "#000000", "border-style": "solid" } } ] }, { "id": "header2", "type": "header", "height": 50, "display": false, "auto-stretch": false, "every-page": false, "items": [ { "type": "text", "id": "", "x": 23.13, "y": 15, "width": 310.92, "height": 18, "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "texts": [ "(header2) display: false in the template" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 18, "line-height-ratio": "", "line-height": 19.8 } }, { "type": "line", "id": "", "x1": 20.31, "y1": 1, "x2": 573.04, "y2": 1, "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "style": { "border-width": 1, "border-color": "#000000", "border-style": "solid" } } ] }, { "id": "detail1_height80", "type": "detail", "height": 80, "auto-stretch": false, "items": [ { "type": "text", "id": "", "x": 23.81, "y": 15, "width": 266.16, "height": 18, "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "texts": [ "(detail1) auto-stretch: off" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 18, "line-height-ratio": "", "line-height": 19.8 } }, { "type": "rect", "id": "", "x": 26.39, "y": 45, "width": 48.07, "height": 35, "border-radius": 0, "description": "", "display": true, "follow-stretch": "height", "affect-bottom-margin": true, "style": { "border-color": "#000000", "border-style": "solid", "fill-color": "#ffffff", "border-width": 1 } }, { "type": "line", "id": "", "x1": 20.31, "y1": 1, "x2": 573.04, "y2": 1, "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "style": { "border-width": 1, "border-color": "#000000", "border-style": "solid" } } ] }, { "id": "detail2_height100", "type": "detail", "height": 100, "auto-stretch": true, "items": [ { "type": "text", "id": "detail3", "x": 23.81, "y": 15, "width": 195.13, "height": 18, "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "texts": [ "(detail2) auto-stretch: on" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 18, "line-height-ratio": "", "line-height": 19.8 } }, { "type": "image-block", "id": "image_block", "x": 26.19, "y": 40.25, "width": 100, "height": 60, "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "style": { "position-x": "left", "position-y": "top" } }, { "type": "line", "id": "", "x1": 20.31, "y1": 1, "x2": 573.04, "y2": 1, "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "style": { "border-width": 1, "border-color": "#000000", "border-style": "solid" } } ] }, { "id": "detail3_height70", "type": "detail", "height": 70, "auto-stretch": true, "items": [ { "type": "text", "id": "detail3", "x": 23.81, "y": 15, "width": 195.13, "height": 18, "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "texts": [ "(detail3) auto-stretch: on" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 18, "line-height-ratio": "", "line-height": 19.8 } }, { "type": "text-block", "id": "text_block", "x": 26.19, "y": 42, "width": 100, "height": 22, "description": "", "reference-id": "", "value": "", "multiple-line": true, "display": true, "format": { "base": "", "type": "" }, "follow-stretch": "none", "affect-bottom-margin": true, "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "overflow": "expand", "word-wrap": "break-word", "font-size": 16, "line-height-ratio": "", "line-height": 17.6 } }, { "type": "line", "id": "", "x1": 20.31, "y1": 1, "x2": 573.04, "y2": 1, "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "style": { "border-width": 1, "border-color": "#000000", "border-style": "solid" } }, { "type": "rect", "id": "", "x": 137.84, "y": 42, "width": 44.69, "height": 28, "border-radius": 0, "description": "", "display": true, "follow-stretch": "height", "affect-bottom-margin": true, "style": { "border-color": "#000000", "border-style": "solid", "fill-color": "#ffffff", "border-width": 1 } } ] }, { "id": "footer1", "type": "footer", "height": 50, "display": true, "auto-stretch": false, "items": [ { "type": "text", "id": "footer1", "x": 23.13, "y": 15, "width": 293.91, "height": 18, "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "texts": [ "(footer1) display: true in the template" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 18, "line-height-ratio": "", "line-height": 19.8 } }, { "type": "line", "id": "", "x1": 20.31, "y1": 1, "x2": 573.04, "y2": 1, "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "style": { "border-width": 1, "border-color": "#000000", "border-style": "solid" } } ] }, { "id": "footer2", "type": "footer", "height": 50, "display": false, "auto-stretch": false, "items": [ { "type": "text", "id": "footer2", "x": 23.13, "y": 15, "width": 300.91, "height": 18, "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "texts": [ "(footer2) display: false in the template" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 18, "line-height-ratio": "", "line-height": 19.8 } }, { "type": "line", "id": "", "x1": 20.31, "y1": 1, "x2": 573.04, "y2": 1, "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "style": { "border-width": 1, "border-color": "#000000", "border-style": "solid" } } ] } ], "state": { "layout-guides": [] } } ================================================ FILE: test/section_report/features/section_parameters/test_feature.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::SectionReport::TestSectionParametersFeature < Thinreports::FeatureTest[__dir__] feature do params = { type: :section, layout_file: template_path, params: { groups: [ { headers: { header1: { display: false }, header2: { display: true } }, details: [ { id: :detail1_height80, min_height: 40 }, { id: :detail1_height80, min_height: 160 }, { id: :detail2_height100, min_height: 100, items: { image_block: dir.join('20x20.jpg').to_path } }, { id: :detail2_height100, min_height: 20, items: { image_block: dir.join('20x20.jpg').to_path } }, { id: :detail3_height70, min_height: 140, items: { text_block: 'text ' * 9 } }, { id: :detail3_height70, min_height: 70, items: { text_block: 'text ' * 12 } } ], footers: { footer1: { display: false }, footer2: { display: true } } } ] } } assert_pdf Thinreports.generate(params) end end ================================================ FILE: test/section_report/features/stack_view/README.md ================================================ # StackView stack-view によって複数の row を縦に積み重ねたレイアウトを作ることができる。 - [Example code](test_feature.rb) - [Example template file](template.tlf) - [Example PDF](expect.pdf) stack-view の主な機能は以下の通り。 - row は、section 同様、四角形やテキストなどの item をその中にを配置できる - row はパラメータで動的に非表示にすることも可能。非表示にした row に続く row は、上にスライドして表示される - stack-view の row の中に、さらに入れ子の stack-view を配置することはできない ================================================ FILE: test/section_report/features/stack_view/template.tlf ================================================ { "schema-version": "1.0", "last-modified-by": "1.0.0-sectionreport.1", "title": "", "report": { "orientation": "portrait", "paper-type": "A4", "width": 0, "height": 0, "margin": [ 20, 20, 20, 20 ] }, "sections": [ { "id": "detail", "type": "detail", "height": 250, "auto-stretch": false, "items": [ { "type": "rect", "id": "", "x": 30, "y": 0.5, "width": 540, "height": 250, "border-radius": 0, "description": "", "display": true, "follow-stretch": "none", "style": { "border-color": "#aaaaaa", "border-style": "solid", "fill-color": "none", "border-width": 1 }, "affect-bottom-margin": true }, { "type": "stack-view", "id": "stackview1", "x": 104.67, "y": 47.17, "width": 210, "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "rows": [ { "id": "row1", "height": 50, "auto-stretch": false, "display": true, "items": [ { "type": "rect", "id": "", "x": 0, "y": 0, "width": 210, "height": 50, "border-radius": 0, "description": "", "display": true, "follow-stretch": "none", "style": { "border-color": "#000000", "border-style": "solid", "fill-color": "none", "border-width": 1 }, "affect-bottom-margin": true }, { "type": "text", "id": "", "x": 5, "y": 5, "width": 100, "height": 20, "description": "", "display": true, "follow-stretch": "none", "texts": [ "Row1" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 14, "line-height-ratio": "", "line-height": 15.4 }, "affect-bottom-margin": true }, { "type": "text", "id": "", "x": 109.33, "y": 5.33, "width": 93.69, "height": 18, "description": "", "display": true, "follow-stretch": "none", "texts": [ "StackView1" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [ "underline" ], "text-align": "right", "vertical-align": "top", "letter-spacing": "", "font-size": 14, "line-height-ratio": "", "line-height": 15.4 }, "affect-bottom-margin": true } ] }, { "id": "row2", "height": 50, "auto-stretch": false, "display": true, "items": [ { "type": "rect", "id": "", "x": 0, "y": 0, "width": 210, "height": 50, "border-radius": 0, "description": "", "display": true, "follow-stretch": "none", "style": { "border-color": "#000000", "border-style": "solid", "fill-color": "none", "border-width": 1 }, "affect-bottom-margin": true }, { "type": "text", "id": "", "x": 5, "y": 5, "width": 100, "height": 20, "description": "", "display": true, "follow-stretch": "none", "texts": [ "Row2" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 14, "line-height-ratio": "", "line-height": 15.4 }, "affect-bottom-margin": true }, { "type": "text-block", "id": "text", "x": 58.5, "y": 18, "width": 140, "height": 20, "description": "", "reference-id": "", "value": "", "multiple-line": true, "display": true, "format": { "base": "", "type": "" }, "follow-stretch": "none", "style": { "font-family": [ "Helvetica" ], "color": "#ff0000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "overflow": "truncate", "word-wrap": "break-word", "font-size": 18, "line-height-ratio": "", "line-height": 19.8 }, "affect-bottom-margin": true } ] }, { "id": "row3", "height": 50, "auto-stretch": false, "display": true, "items": [ { "type": "rect", "id": "", "x": 0, "y": 0, "width": 210, "height": 50, "border-radius": 0, "description": "", "display": true, "follow-stretch": "none", "style": { "border-color": "#000000", "border-style": "solid", "fill-color": "none", "border-width": 1 }, "affect-bottom-margin": true }, { "type": "text", "id": "", "x": 5, "y": 5, "width": 100, "height": 20, "description": "", "display": true, "follow-stretch": "none", "texts": [ "Row3" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 14, "line-height-ratio": "", "line-height": 15.4 }, "affect-bottom-margin": true }, { "type": "rect", "id": "", "x": 74.72, "y": 13.33, "width": 57, "height": 30, "border-radius": 0, "description": "", "display": true, "follow-stretch": "none", "style": { "border-color": "#000000", "border-style": "solid", "fill-color": "#ffffff", "border-width": 1 }, "affect-bottom-margin": true }, { "type": "ellipse", "id": "", "description": "", "display": true, "follow-stretch": "none", "style": { "border-width": 1, "border-color": "#000000", "border-style": "solid", "fill-color": "#ffffff" }, "cx": 140.97, "cy": 20.33, "rx": 17.25, "ry": 15.5, "affect-bottom-margin": true } ] } ] }, { "type": "stack-view", "id": "stackview2", "x": 362.67, "y": 104, "width": 150, "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "rows": [ { "id": "row1", "height": 30, "auto-stretch": false, "display": true, "items": [ { "type": "text", "id": "", "x": 5, "y": 5, "width": 100, "height": 20, "description": "", "display": true, "follow-stretch": "none", "texts": [ "Row1" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 14, "line-height-ratio": "", "line-height": 15.4 }, "affect-bottom-margin": true }, { "type": "text", "id": "", "x": 51.33, "y": 4.33, "width": 93.69, "height": 18, "description": "", "display": true, "follow-stretch": "none", "texts": [ "StackView2" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [ "underline" ], "text-align": "right", "vertical-align": "top", "letter-spacing": "", "font-size": 14, "line-height-ratio": "", "line-height": 15.4 }, "affect-bottom-margin": true } ] }, { "id": "row2", "height": 30, "auto-stretch": false, "display": true, "items": [ { "type": "text", "id": "", "x": 5, "y": 5, "width": 100, "height": 20, "description": "", "display": true, "follow-stretch": "none", "texts": [ "Row2" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 14, "line-height-ratio": "", "line-height": 15.4 }, "affect-bottom-margin": true } ] }, { "id": "row3", "height": 30, "auto-stretch": false, "display": true, "items": [ { "type": "text", "id": "", "x": 5, "y": 5, "width": 100, "height": 20, "description": "", "display": true, "follow-stretch": "none", "texts": [ "Row3" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 14, "line-height-ratio": "", "line-height": 15.4 }, "affect-bottom-margin": true } ] }, { "id": "row4", "height": 30, "auto-stretch": false, "display": true, "items": [ { "type": "text", "id": "", "x": 5, "y": 5, "width": 100, "height": 20, "description": "", "display": true, "follow-stretch": "none", "texts": [ "Row4" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 14, "line-height-ratio": "", "line-height": 15.4 }, "affect-bottom-margin": true } ] } ] }, { "type": "text", "id": "", "x": 44.89, "y": 9, "width": 60.06, "height": 18, "description": "", "display": true, "follow-stretch": "none", "texts": [ "Section" ], "style": { "font-family": [ "Helvetica" ], "color": "#aaaaaa", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 18, "line-height-ratio": "", "line-height": 19.8 }, "affect-bottom-margin": true } ] } ], "state": { "layout-guides": [] } } ================================================ FILE: test/section_report/features/stack_view/test_feature.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::SectionReport::TestStackViewFeature < Thinreports::FeatureTest[__dir__] feature do params = { type: :section, layout_file: template_path, params: { groups: [ { details: [ { id: :detail, items: { stackview1: { rows: { row2: { items: { text: 'text block' } } } }, stackview2: { rows: { row2: { display: false }, row3: { display: false } } } } } ] } ] } } assert_pdf Thinreports.generate(params) end end ================================================ FILE: test/section_report/features/stack_view_row_auto_stretch/README.md ================================================ # StackViewRow Auto Stretch 自動伸縮を有効にすることで、描画後の内容に応じて row の高さを自動的に伸縮させることができる。 - [Example code](test_feature.rb) - [Example template file](template.tlf) - [Example PDF](expect.pdf) row の伸縮によって stack-view 自体の高さも伸縮する。 row の伸縮の仕様は section と同様である。詳細は [Section Auto Stretch](../section_report_section_auto_stretch/README.md) を参照。 ただし、stack-view は入れ子にすることはできないため、stack-view によるrowの伸縮は起こらない。 ================================================ FILE: test/section_report/features/stack_view_row_auto_stretch/template.tlf ================================================ { "schema-version": "1.0", "last-modified-by": "1.0.0-sectionreport.1", "title": "", "report": { "orientation": "portrait", "paper-type": "A4", "width": 0, "height": 0, "margin": [ 20, 20, 20, 20 ] }, "sections": [ { "id": "detail1", "type": "detail", "height": 250, "auto-stretch": true, "items": [ { "type": "stack-view", "id": "stackview", "x": 49.31, "y": 10, "width": 500, "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "rows": [ { "id": "row1", "height": 60, "auto-stretch": true, "display": true, "items": [ { "type": "text", "id": "", "x": 15, "y": 5, "width": 336.69, "height": 14, "description": "", "display": true, "follow-stretch": "none", "texts": [ "Expand height by text-block whose overflow is expand" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 14, "line-height-ratio": "", "line-height": 15.4 }, "affect-bottom-margin": true }, { "type": "text", "id": "", "x": 374.16, "y": 4.99, "width": 122.16, "height": 15.83, "description": "", "display": true, "follow-stretch": "none", "texts": [ "Row1(auto-stretch: on)" ], "style": { "font-family": [ "Helvetica" ], "color": "#aaaaaa", "font-style": [], "text-align": "right", "vertical-align": "top", "letter-spacing": "", "font-size": 12, "line-height-ratio": "", "line-height": 13.2 }, "affect-bottom-margin": true }, { "type": "text-block", "id": "text_overflow_expand", "x": 16.66, "y": 24.99, "width": 290, "height": 30, "description": "", "reference-id": "", "value": "", "multiple-line": true, "display": true, "format": { "base": "", "type": "" }, "follow-stretch": "none", "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "overflow": "expand", "word-wrap": "break-word", "font-size": 14, "line-height-ratio": "", "line-height": 15.4 }, "affect-bottom-margin": true }, { "type": "line", "id": "", "x1": 0, "y1": 1.84, "x2": 500, "y2": 1, "description": "", "display": true, "follow-stretch": "none", "style": { "border-width": 1, "border-color": "#000000", "border-style": "solid" }, "affect-bottom-margin": true } ] }, { "id": "row2", "height": 80, "auto-stretch": true, "display": true, "items": [ { "type": "text", "id": "", "x": 15, "y": 5, "width": 208.23, "height": 14, "description": "", "display": true, "follow-stretch": "none", "texts": [ "Expand height with multiple items" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 14, "line-height-ratio": "", "line-height": 15.4 }, "affect-bottom-margin": true }, { "type": "text", "id": "", "x": 374.16, "y": 5.83, "width": 121.94, "height": 15.83, "description": "", "display": true, "follow-stretch": "none", "texts": [ "Row2(auto-stretch: off)" ], "style": { "font-family": [ "Helvetica" ], "color": "#aaaaaa", "font-style": [], "text-align": "right", "vertical-align": "top", "letter-spacing": "", "font-size": 12, "line-height-ratio": "", "line-height": 13.2 }, "affect-bottom-margin": true }, { "type": "text-block", "id": "text_overflow_expand", "x": 16.66, "y": 24.99, "width": 200, "height": 24, "description": "", "reference-id": "", "value": "", "multiple-line": true, "display": true, "format": { "base": "", "type": "" }, "follow-stretch": "none", "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "overflow": "expand", "word-wrap": "break-word", "font-size": 14, "line-height-ratio": "", "line-height": 15.4 }, "affect-bottom-margin": true }, { "type": "rect", "id": "", "x": 255.83, "y": 32.5, "width": 102.5, "height": 43.33, "border-radius": 0, "description": "", "display": true, "follow-stretch": "none", "style": { "border-color": "#000000", "border-style": "solid", "fill-color": "#ffffff", "border-width": 1 }, "affect-bottom-margin": true }, { "type": "line", "id": "", "x1": 0, "y1": 1.84, "x2": 500, "y2": 1, "description": "", "display": true, "follow-stretch": "none", "style": { "border-width": 1, "border-color": "#000000", "border-style": "solid" }, "affect-bottom-margin": true } ] }, { "id": "row3", "height": 60, "auto-stretch": false, "display": true, "items": [ { "type": "text", "id": "", "x": 15, "y": 5, "width": 353.02, "height": 14, "description": "", "display": true, "follow-stretch": "none", "texts": [ "Does not expand by text-block whose overflow is expand" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 14, "line-height-ratio": "", "line-height": 15.4 }, "affect-bottom-margin": true }, { "type": "text", "id": "", "x": 374.16, "y": 5.83, "width": 121.94, "height": 15.83, "description": "", "display": true, "follow-stretch": "none", "texts": [ "Row3(auto-stretch: off)" ], "style": { "font-family": [ "Helvetica" ], "color": "#aaaaaa", "font-style": [], "text-align": "right", "vertical-align": "top", "letter-spacing": "", "font-size": 12, "line-height-ratio": "", "line-height": 13.2 }, "affect-bottom-margin": true }, { "type": "text-block", "id": "text_overflow_expand", "x": 16.66, "y": 24.99, "width": 290, "height": 30, "description": "", "reference-id": "", "value": "", "multiple-line": true, "display": true, "format": { "base": "", "type": "" }, "follow-stretch": "none", "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "overflow": "expand", "word-wrap": "break-word", "font-size": 14, "line-height-ratio": "", "line-height": 15.4 }, "affect-bottom-margin": true }, { "type": "line", "id": "", "x1": 0, "y1": 1.84, "x2": 500, "y2": 1, "description": "", "display": true, "follow-stretch": "none", "style": { "border-width": 1, "border-color": "#000000", "border-style": "solid" }, "affect-bottom-margin": true } ] }, { "id": "row4", "height": 20, "auto-stretch": false, "display": true, "items": [ { "type": "line", "id": "", "x1": 0, "y1": 1.84, "x2": 500, "y2": 1, "description": "", "display": true, "follow-stretch": "none", "style": { "border-width": 1, "border-color": "#000000", "border-style": "solid" }, "affect-bottom-margin": true }, { "type": "text", "id": "", "x": 396.66, "y": 4.16, "width": 98.97, "height": 15.83, "description": "", "display": true, "follow-stretch": "none", "texts": [ "dummy" ], "style": { "font-family": [ "Helvetica" ], "color": "#aaaaaa", "font-style": [], "text-align": "right", "vertical-align": "top", "letter-spacing": "", "font-size": 12, "line-height-ratio": "", "line-height": 13.2 }, "affect-bottom-margin": true } ] } ] } ] }, { "id": "detail2", "type": "detail", "height": 550, "auto-stretch": true, "items": [ { "type": "stack-view", "id": "stackview", "x": 49.31, "y": 10, "width": 500, "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "rows": [ { "id": "row1", "height": 130, "auto-stretch": true, "display": true, "items": [ { "type": "text", "id": "", "x": 15, "y": 5, "width": 334.34, "height": 14, "description": "", "display": true, "follow-stretch": "none", "texts": [ "Reduce height by image-block whose position-y is top" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 14, "line-height-ratio": "", "line-height": 15.4 }, "affect-bottom-margin": true }, { "type": "text", "id": "", "x": 374.99, "y": 5.83, "width": 122.16, "height": 15.83, "description": "", "display": true, "follow-stretch": "none", "texts": [ "Row1(auto-stretch: on)" ], "style": { "font-family": [ "Helvetica" ], "color": "#aaaaaa", "font-style": [], "text-align": "right", "vertical-align": "top", "letter-spacing": "", "font-size": 12, "line-height-ratio": "", "line-height": 13.2 }, "affect-bottom-margin": true }, { "type": "image-block", "id": "image200x100", "x": 16.66, "y": 25, "width": 290, "height": 100, "description": "", "display": true, "follow-stretch": "none", "style": { "position-x": "left", "position-y": "top" }, "affect-bottom-margin": true }, { "type": "line", "id": "", "x1": 0, "y1": 1.84, "x2": 500, "y2": 1, "description": "", "display": true, "follow-stretch": "none", "style": { "border-width": 1, "border-color": "#000000", "border-style": "solid" }, "affect-bottom-margin": true } ] }, { "id": "row2", "height": 130, "auto-stretch": false, "display": true, "items": [ { "type": "text", "id": "", "x": 15, "y": 5, "width": 346.78, "height": 14, "description": "", "display": true, "follow-stretch": "none", "texts": [ "Does not reduce by image-block whose position-y is top" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 14, "line-height-ratio": "", "line-height": 15.4 }, "affect-bottom-margin": true }, { "type": "text", "id": "", "x": 374.99, "y": 4.99, "width": 121.94, "height": 15.83, "description": "", "display": true, "follow-stretch": "none", "texts": [ "Row2(auto-stretch: off)" ], "style": { "font-family": [ "Helvetica" ], "color": "#aaaaaa", "font-style": [], "text-align": "right", "vertical-align": "top", "letter-spacing": "", "font-size": 12, "line-height-ratio": "", "line-height": 13.2 }, "affect-bottom-margin": true }, { "type": "image-block", "id": "image200x100", "x": 16.66, "y": 25, "width": 290, "height": 100, "description": "", "display": true, "follow-stretch": "none", "style": { "position-x": "left", "position-y": "top" }, "affect-bottom-margin": true }, { "type": "line", "id": "", "x1": 0, "y1": 1.84, "x2": 500, "y2": 1, "description": "", "display": true, "follow-stretch": "none", "style": { "border-width": 1, "border-color": "#000000", "border-style": "solid" }, "affect-bottom-margin": true } ] }, { "id": "row3", "height": 130, "auto-stretch": true, "display": true, "items": [ { "type": "text", "id": "", "x": 15, "y": 5, "width": 361.13, "height": 14, "description": "", "display": true, "follow-stretch": "none", "texts": [ "Does not reduce by image-block whose position-y isn't top" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 14, "line-height-ratio": "", "line-height": 15.4 }, "affect-bottom-margin": true }, { "type": "text", "id": "", "x": 374.16, "y": 4.99, "width": 122.16, "height": 15.83, "description": "", "display": true, "follow-stretch": "none", "texts": [ "Row3(auto-stretch: on)" ], "style": { "font-family": [ "Helvetica" ], "color": "#aaaaaa", "font-style": [], "text-align": "right", "vertical-align": "top", "letter-spacing": "", "font-size": 12, "line-height-ratio": "", "line-height": 13.2 }, "affect-bottom-margin": true }, { "type": "image-block", "id": "image200x100", "x": 16.66, "y": 25, "width": 290, "height": 100, "description": "", "display": true, "follow-stretch": "none", "style": { "position-x": "left", "position-y": "middle" }, "affect-bottom-margin": true }, { "type": "line", "id": "", "x1": 0, "y1": 1.84, "x2": 500, "y2": 1, "description": "", "display": true, "follow-stretch": "none", "style": { "border-width": 1, "border-color": "#000000", "border-style": "solid" }, "affect-bottom-margin": true } ] }, { "id": "row4", "height": 130, "auto-stretch": true, "display": true, "items": [ { "type": "line", "id": "", "x1": 0, "y1": 1.84, "x2": 500, "y2": 1, "description": "", "display": true, "follow-stretch": "none", "style": { "border-width": 1, "border-color": "#000000", "border-style": "solid" }, "affect-bottom-margin": true }, { "type": "text", "id": "", "x": 15, "y": 5, "width": 209.02, "height": 14, "description": "", "display": true, "follow-stretch": "none", "texts": [ "Reduce height with multiple items" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 14, "line-height-ratio": "", "line-height": 15.4 }, "affect-bottom-margin": true }, { "type": "text", "id": "", "x": 374.16, "y": 4.99, "width": 122.16, "height": 15.83, "description": "", "display": true, "follow-stretch": "none", "texts": [ "Row4(auto-stretch: on)" ], "style": { "font-family": [ "Helvetica" ], "color": "#aaaaaa", "font-style": [], "text-align": "right", "vertical-align": "top", "letter-spacing": "", "font-size": 12, "line-height-ratio": "", "line-height": 13.2 }, "affect-bottom-margin": true }, { "type": "image-block", "id": "image200x100", "x": 16.66, "y": 25, "width": 200, "height": 100, "description": "", "display": true, "follow-stretch": "none", "style": { "position-x": "left", "position-y": "top" }, "affect-bottom-margin": true }, { "type": "rect", "id": "", "x": 260, "y": 25.83, "width": 118.33, "height": 80, "border-radius": 0, "description": "", "display": true, "follow-stretch": "none", "style": { "border-color": "#000000", "border-style": "solid", "fill-color": "#ffffff", "border-width": 1 }, "affect-bottom-margin": true } ] }, { "id": "row5", "height": 20, "auto-stretch": false, "display": true, "items": [ { "type": "text", "id": "row4", "x": 396.66, "y": 4.16, "width": 98.97, "height": 15.83, "description": "", "display": true, "follow-stretch": "none", "texts": [ "dummy" ], "style": { "font-family": [ "Helvetica" ], "color": "#aaaaaa", "font-style": [], "text-align": "right", "vertical-align": "top", "letter-spacing": "", "font-size": 12, "line-height-ratio": "", "line-height": 13.2 }, "affect-bottom-margin": true }, { "type": "line", "id": "", "x1": 0, "y1": 1.84, "x2": 500, "y2": 1, "description": "", "display": true, "follow-stretch": "none", "style": { "border-width": 1, "border-color": "#000000", "border-style": "solid" }, "affect-bottom-margin": true } ] } ] } ] } ], "state": { "layout-guides": [] } } ================================================ FILE: test/section_report/features/stack_view_row_auto_stretch/test_feature.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::SectionReport::TestStackViewRowAutoStretchFeature < Thinreports::FeatureTest[__dir__] feature do params = { type: :section, layout_file: template_path, params: { groups: [ { details: [ { id: 'detail1', items: { stackview: { rows: { row1: { items: { text_overflow_expand: 'Extended text box height with long text. ' * 4 } }, row2: { items: { text_overflow_expand: 'Extended text box height with long text. ' * 4 } }, row3: { items: { text_overflow_expand: 'Extended text box height with long text. ' * 4 } } } } } }, { id: 'detail2', items: { stackview: { rows: { row1: { items: { image200x100: image50x50 } }, row2: { items: { image200x100: image50x50 } }, row3: { items: { image200x100: image50x50 } }, row4: { items: { image200x100: image50x50 } } } } } } ] } ] } } assert_pdf Thinreports.generate(params) end def image50x50 StringIO.new(dir.join('50x50.jpg').binread) end end ================================================ FILE: test/section_report/features/stack_view_row_bottom_margin/README.md ================================================ # StackViewRow Bottom Margin row の下余白とは、row のコンテンツの下位置と、row の下位置との間の距離である。 - [Example code](test_feature.rb) - [Example template file](template.tlf) - [Example PDF](expect.pdf) row の下余白の仕様は section の下余白と同様である。詳細は [Section Bottom Margin](../section_report_section_bottom_margin/README.md) を参照。 ================================================ FILE: test/section_report/features/stack_view_row_bottom_margin/template.tlf ================================================ { "schema-version": "1.0", "last-modified-by": "1.0.0-sectionreport.1", "title": "", "report": { "orientation": "portrait", "paper-type": "A4", "width": 0, "height": 0, "margin": [ 20, 20, 20, 20 ] }, "sections": [ { "id": "header", "type": "header", "height": 240, "display": true, "auto-stretch": true, "every-page": false, "items": [ { "type": "stack-view", "id": "stackview", "x": 20, "y": 10, "width": 555, "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "rows": [ { "id": "row1", "height": 200, "auto-stretch": true, "display": true, "items": [ { "type": "line", "id": "", "x1": 0, "y1": 0, "x2": 555, "y2": 0, "description": "", "display": true, "follow-stretch": "none", "style": { "border-width": 1, "border-color": "#000000", "border-style": "solid" }, "affect-bottom-margin": true }, { "type": "text-block", "id": "textblock", "x": 40, "y": 40, "width": 100, "height": 20, "description": "", "reference-id": "", "value": "", "multiple-line": true, "display": true, "format": { "base": "", "type": "" }, "follow-stretch": "none", "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "overflow": "expand", "word-wrap": "break-word", "font-size": 18, "line-height-ratio": "", "line-height": 19.8 }, "affect-bottom-margin": true }, { "type": "rect", "id": "", "x": 200, "y": 20, "width": 100, "height": 120, "border-radius": 0, "description": "", "display": true, "follow-stretch": "none", "style": { "border-color": "#000000", "border-style": "solid", "fill-color": "#ffffff", "border-width": 1 }, "affect-bottom-margin": true }, { "type": "rect", "id": "", "x": 360, "y": 40, "width": 160, "height": 150, "border-radius": 0, "description": "", "display": true, "follow-stretch": "none", "style": { "border-color": "#000000", "border-style": "dashed", "fill-color": "#ffffff", "border-width": 1 }, "affect-bottom-margin": false }, { "type": "text", "id": "", "x": 370, "y": 50, "width": 112.2, "height": 37.8, "description": "", "display": true, "follow-stretch": "none", "texts": [ "affect-bottom-", "margin: false" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 18, "line-height-ratio": "", "line-height": 19.8 }, "affect-bottom-margin": true } ] }, { "id": "row2", "height": 20, "auto-stretch": false, "display": true, "items": [ { "type": "line", "id": "", "x1": 0, "y1": 0, "x2": 555, "y2": 0, "description": "", "display": true, "follow-stretch": "none", "style": { "border-width": 1, "border-color": "#000000", "border-style": "solid" }, "affect-bottom-margin": true } ] } ] } ] } ], "state": { "layout-guides": [] } } ================================================ FILE: test/section_report/features/stack_view_row_bottom_margin/test_feature.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::SectionReport::TestStackViewRowBottomMarginFeature < Thinreports::FeatureTest[__dir__] feature do params = { type: :section, layout_file: template_path, params: { groups: [ { headers: { header: { items: { stackview: { rows: { row1: { items: { textblock: 'long ' * 19 + 'text' } } } } } } } } ] } } assert_pdf Thinreports.generate(params) end end ================================================ FILE: test/section_report/features/stack_view_row_parameters/README.md ================================================ # StackViewRow Parameter row の定義時のプロパティの一部はパラメータで変更することができる。 - [Example code](test_feature.rb) - [Example template file](template.tlf) - [Example PDF](expect.pdf) ## 表示 `display` プロパティで表示または非表示にすることができる。 ``` row_id: { display: Boolean } ``` ## 最小の高さ `min_height` プロパティで row の描画時の最小の高さを指定することができる。 ``` row_id: { min_height: Number } ================================================ FILE: test/section_report/features/stack_view_row_parameters/template.tlf ================================================ { "schema-version": "1.0", "last-modified-by": "1.0.0-sectionreport.1", "title": "", "report": { "orientation": "portrait", "paper-type": "A4", "width": 0, "height": 0, "margin": [ 20, 20, 20, 20 ] }, "sections": [ { "id": "header", "type": "header", "height": 120, "display": true, "auto-stretch": false, "every-page": false, "items": [ { "type": "stack-view", "id": "stackview1", "x": 43.38, "y": 9.5, "width": 359.99, "description": "", "display": true, "affect-bottom-margin": true, "follow-stretch": "none", "rows": [ { "id": "row1", "height": 50, "auto-stretch": false, "display": true, "items": [ { "type": "text", "id": "", "x": 15, "y": 15, "width": 283.06, "height": 23.7, "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "texts": [ "Row1 (display: true in the template)" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 18, "line-height-ratio": "", "line-height": 19.8 } } ] }, { "id": "row2", "height": 50, "auto-stretch": false, "display": false, "items": [ { "type": "text", "id": "", "x": 15, "y": 15, "width": 290.06, "height": 23.7, "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "texts": [ "Row2 (display: false in the template)" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 18, "line-height-ratio": "", "line-height": 19.8 } } ] } ] } ] }, { "id": "detail", "type": "detail", "height": 500, "auto-stretch": false, "items": [ { "type": "stack-view", "id": "stackview2", "x": 42.02, "y": 10.2, "width": 359.99, "description": "", "display": true, "affect-bottom-margin": true, "follow-stretch": "none", "rows": [ { "id": "row1", "height": 35, "auto-stretch": false, "display": true, "items": [ { "type": "text", "id": "", "x": 15, "y": 5, "width": 187.77, "height": 23.7, "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "texts": [ "Row1 (auto-stretch: off)" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 18, "line-height-ratio": "", "line-height": 19.8 } } ] }, { "id": "row2", "height": 35, "auto-stretch": false, "display": true, "items": [ { "type": "text", "id": "", "x": 15.68, "y": 5, "width": 187.77, "height": 23.7, "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "texts": [ "Row2 (auto-stretch: off)" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 18, "line-height-ratio": "", "line-height": 19.8 } }, { "type": "line", "id": "", "x1": 6.8, "y1": 1, "x2": 350.16, "y2": 1, "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "style": { "border-width": 1, "border-color": "#000000", "border-style": "solid" } } ] }, { "id": "row3", "height": 90, "auto-stretch": true, "display": true, "items": [ { "type": "text", "id": "", "x": 15, "y": 5, "width": 188.08, "height": 23.7, "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "texts": [ "Row3 (auto-stretch: on)" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 18, "line-height-ratio": "", "line-height": 19.8 } }, { "type": "image-block", "id": "image_block", "x": 14.86, "y": 28.13, "width": 100, "height": 60, "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "style": { "position-x": "left", "position-y": "top" } }, { "type": "line", "id": "", "x1": 6.8, "y1": 1, "x2": 350.16, "y2": 1, "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "style": { "border-width": 1, "border-color": "#000000", "border-style": "solid" } } ] }, { "id": "row4", "height": 90, "auto-stretch": true, "display": true, "items": [ { "type": "text", "id": "", "x": 15, "y": 5, "width": 188.08, "height": 23.7, "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "texts": [ "Row4 (auto-stretch: on)" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 18, "line-height-ratio": "", "line-height": 19.8 } }, { "type": "image-block", "id": "image_block", "x": 14.86, "y": 28.13, "width": 100, "height": 60, "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "style": { "position-x": "left", "position-y": "top" } }, { "type": "line", "id": "", "x1": 6.8, "y1": 1, "x2": 350.16, "y2": 1, "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "style": { "border-width": 1, "border-color": "#000000", "border-style": "solid" } } ] }, { "id": "row5", "height": 55, "auto-stretch": true, "display": true, "items": [ { "type": "text", "id": "", "x": 15, "y": 5, "width": 188.08, "height": 23.7, "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "texts": [ "Row5 (auto-stretch: on)" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 18, "line-height-ratio": "", "line-height": 19.8 } }, { "type": "text-block", "id": "text_block", "x": 14.54, "y": 29.33, "width": 117.47, "height": 20, "description": "", "reference-id": "", "value": "", "multiple-line": true, "display": true, "format": { "base": "", "type": "" }, "follow-stretch": "none", "affect-bottom-margin": true, "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "overflow": "expand", "word-wrap": "break-word", "font-size": 14, "line-height-ratio": "", "line-height": 15.4 } }, { "type": "line", "id": "", "x1": 6.8, "y1": 1, "x2": 350.16, "y2": 1, "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "style": { "border-width": 1, "border-color": "#000000", "border-style": "solid" } } ] }, { "id": "row6", "height": 55, "auto-stretch": true, "display": true, "items": [ { "type": "text", "id": "", "x": 15, "y": 5, "width": 188.08, "height": 23.7, "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "texts": [ "Row6 (auto-stretch: on)" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 18, "line-height-ratio": "", "line-height": 19.8 } }, { "type": "text-block", "id": "text_block", "x": 14.54, "y": 29.33, "width": 117.47, "height": 20, "description": "", "reference-id": "", "value": "", "multiple-line": true, "display": true, "format": { "base": "", "type": "" }, "follow-stretch": "none", "affect-bottom-margin": true, "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "overflow": "expand", "word-wrap": "break-word", "font-size": 14, "line-height-ratio": "", "line-height": 15.4 } }, { "type": "line", "id": "", "x1": 6.8, "y1": 1, "x2": 350.16, "y2": 1, "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "style": { "border-width": 1, "border-color": "#000000", "border-style": "solid" } } ] }, { "id": "", "height": 35, "auto-stretch": false, "display": true, "items": [ { "type": "line", "id": "", "x1": 6.8, "y1": 1, "x2": 350.16, "y2": 1, "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "style": { "border-width": 1, "border-color": "#000000", "border-style": "solid" } }, { "type": "text", "id": "", "x": 15, "y": 5, "width": 46.03, "height": 23.7, "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "texts": [ "Row7" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 18, "line-height-ratio": "", "line-height": 19.8 } } ] } ] }, { "type": "line", "id": "", "x1": 12.07, "y1": 1, "x2": 581.54, "y2": 1, "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "style": { "border-width": 1, "border-color": "#000000", "border-style": "solid" } } ] } ], "state": { "layout-guides": [] } } ================================================ FILE: test/section_report/features/stack_view_row_parameters/test_feature.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::SectionReport::TestStackViewRowParametersFeature < Thinreports::FeatureTest[__dir__] feature do params = { type: :section, layout_file: template_path, params: { groups: [ { headers: { header: { items: { stackview1: { rows: { row1: { display: false }, row2: { display: true } } } } } }, details: [ { id: :detail, items: { stackview2: { rows: { row1: { min_height: 10 }, row2: { min_height: 70 }, row3: { min_height: 90, items: { image_block: dir.join('20x20.jpg').to_path } }, row4: { min_height: 20, items: { image_block: dir.join('20x20.jpg').to_path } }, row5: { min_height: 140, items: { text_block: 'text ' * 9 } }, row6: { min_height: 55, items: { text_block: 'text ' * 16 } } } } } } ] } ] } } assert_pdf Thinreports.generate(params) end end ================================================ FILE: test/section_report/features/stack_view_with_floating_item/README.md ================================================ # StackView With Floating Item item を stack-view の row から下にはみ出して配置することができる。 - [Example code](test_feature.rb) - [Example template file](template.tlf) - [Example PDF](expect.pdf) stack-view の row からはみ出して配置した場合でも、その状態を保って描画することができる。 ただし、はみ出す item は「下余白に影響」を無効にしておく必要がある。 「下余白に影響」が有効の場合、row の下余白が負の値となり、意図しない結果となる場合がある。 また、下図の通り、stack-view の高さは、はみ出した item 全体を含んだ高さとなる。 ![](images/stack-view-height-with-floting-item.png) ================================================ FILE: test/section_report/features/stack_view_with_floating_item/template.tlf ================================================ { "schema-version": "1.0", "last-modified-by": "1.0.0-sectionreport.1", "title": "", "report": { "orientation": "portrait", "paper-type": "A4", "width": 0, "height": 0, "margin": [ 20, 20, 20, 20 ] }, "sections": [ { "id": "detail", "type": "detail", "height": 130, "auto-stretch": true, "items": [ { "type": "line", "id": "", "x1": 0, "y1": 0, "x2": 595.28, "y2": 0, "description": "", "display": true, "follow-stretch": "none", "style": { "border-width": 1, "border-color": "#000000", "border-style": "solid" }, "affect-bottom-margin": true }, { "type": "stack-view", "id": "stackview", "x": 20, "y": 30, "width": 500, "description": "", "display": true, "follow-stretch": "none", "affect-bottom-margin": true, "rows": [ { "id": "row1", "height": 70, "auto-stretch": true, "display": true, "items": [ { "type": "line", "id": "", "x1": 0, "y1": 0, "x2": 500, "y2": 0, "description": "", "display": true, "follow-stretch": "none", "style": { "border-width": 1, "border-color": "#000000", "border-style": "solid" }, "affect-bottom-margin": true }, { "type": "text-block", "id": "textblock", "x": 40, "y": 30, "width": 100, "height": 20, "description": "", "reference-id": "", "value": "", "multiple-line": true, "display": true, "format": { "base": "", "type": "" }, "follow-stretch": "none", "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "overflow": "expand", "word-wrap": "break-word", "font-size": 18, "line-height-ratio": "", "line-height": 19.8 }, "affect-bottom-margin": true }, { "type": "rect", "id": "", "x": 200, "y": 20, "width": 170, "height": 150, "border-radius": 0, "description": "", "display": true, "follow-stretch": "none", "style": { "border-color": "#000000", "border-style": "dashed", "fill-color": "#ffffff", "border-width": 1 }, "affect-bottom-margin": false }, { "type": "text", "id": "", "x": 210, "y": 30, "width": 150, "height": 37.8, "description": "", "display": true, "follow-stretch": "none", "texts": [ "affect-bottom-", "margin: false" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 16, "line-height-ratio": "", "line-height": 17.6 }, "affect-bottom-margin": false }, { "type": "text", "id": "", "x": 400, "y": 3, "width": 90, "height": 15, "description": "", "display": true, "follow-stretch": "none", "texts": [ "row1" ], "style": { "font-family": [ "Helvetica" ], "color": "#aaaaaa", "font-style": [], "text-align": "right", "vertical-align": "top", "letter-spacing": "", "font-size": 12, "line-height-ratio": "", "line-height": 13.2 }, "affect-bottom-margin": true } ] }, { "id": "row2", "height": 20, "auto-stretch": false, "display": true, "items": [ { "type": "line", "id": "", "x1": 0, "y1": 0, "x2": 500, "y2": 0, "description": "", "display": true, "follow-stretch": "none", "style": { "border-width": 1, "border-color": "#000000", "border-style": "solid" }, "affect-bottom-margin": true }, { "type": "text", "id": "", "x": 400, "y": 3, "width": 90, "height": 15, "description": "", "display": true, "follow-stretch": "none", "texts": [ "row2 (dummy)" ], "style": { "font-family": [ "Helvetica" ], "color": "#aaaaaa", "font-style": [], "text-align": "right", "vertical-align": "top", "letter-spacing": "", "font-size": 12, "line-height-ratio": "", "line-height": 13.2 }, "affect-bottom-margin": true } ] } ] }, { "type": "text", "id": "", "x": 495, "y": 3, "width": 90, "height": 15, "description": "", "display": true, "follow-stretch": "none", "texts": [ "detail" ], "style": { "font-family": [ "Helvetica" ], "color": "#aaaaaa", "font-style": [], "text-align": "right", "vertical-align": "top", "letter-spacing": "", "font-size": 12, "line-height-ratio": "", "line-height": 13.2 }, "affect-bottom-margin": true } ] }, { "id": "fotter", "type": "footer", "height": 20, "display": true, "auto-stretch": false, "items": [ { "type": "line", "id": "", "x1": 0, "y1": 0, "x2": 595.28, "y2": 0, "description": "", "display": true, "follow-stretch": "none", "style": { "border-width": 1, "border-color": "#000000", "border-style": "solid" }, "affect-bottom-margin": true }, { "type": "text", "id": "", "x": 495, "y": 3, "width": 90, "height": 15, "description": "", "display": true, "follow-stretch": "none", "texts": [ "footer (dummy)" ], "style": { "font-family": [ "Helvetica" ], "color": "#aaaaaa", "font-style": [], "text-align": "right", "vertical-align": "top", "letter-spacing": "", "font-size": 12, "line-height-ratio": "", "line-height": 13.2 }, "affect-bottom-margin": true } ] } ], "state": { "layout-guides": [] } } ================================================ FILE: test/section_report/features/stack_view_with_floating_item/test_feature.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::SectionReport::TestStackViewWithFloatingItemFeature < Thinreports::FeatureTest[__dir__] feature do params = { type: :section, layout_file: template_path, params: { groups: [ { details: [ { id: 'detail', items: { stackview: { rows: { row1: { items: { textblock: 'short text' } } } } } }, { id: 'detail', items: { stackview: { rows: { row1: { items: { textblock: 'long ' * 19 + 'text' } } } } } } ] } ] } } assert_pdf Thinreports.generate(params) end def image50x50 StringIO.new(dir.join('50x50.jpg').binread) end end ================================================ FILE: test/section_report/features/text_block_vertical_align/README.md ================================================ # TextBlock Vertical-align Support section-report 形式の text-block は `overflow: expand` のときの縦揃え(中央、下揃え)をサポートする。 - [Example code](test_feature.rb) - [Example template file](template.tlf) - [Example PDF](expect.pdf) 通常の形式の text-block は、`overflow: expand` で縦位置が中央又は下揃えのとき意図通りに描画されない。これは prawn の仕様によるものである。 section-report 形式では、これを独自にサポートしている。 ================================================ FILE: test/section_report/features/text_block_vertical_align/template.tlf ================================================ { "schema-version": "1.0", "last-modified-by": "1.0.0-sectionreport.1", "title": "", "report": { "orientation": "portrait", "paper-type": "A4", "width": 0, "height": 0, "margin": [ 20, 20, 20, 20 ] }, "sections": [ { "id": "top", "type": "detail", "height": 100, "auto-stretch": true, "items": [ { "type": "rect", "id": "", "x": 20, "y": 0, "width": 555.28, "height": 100, "border-radius": 0, "description": "", "display": true, "follow-stretch": "height", "style": { "border-color": "#000000", "border-style": "solid", "fill-color": "none", "border-width": 1 }, "affect-bottom-margin": false }, { "type": "rect", "id": "", "x": 30, "y": 10, "width": 200, "height": 40, "border-radius": 0, "description": "", "display": true, "follow-stretch": "height", "style": { "border-color": "#000000", "border-style": "dashed", "fill-color": "none", "border-width": 1 }, "affect-bottom-margin": false }, { "type": "text-block", "id": "text", "x": 30, "y": 10, "width": 200, "height": 40, "description": "", "reference-id": "", "value": "", "multiple-line": true, "display": true, "format": { "base": "", "type": "" }, "follow-stretch": "none", "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "overflow": "expand", "word-wrap": "break-word", "font-size": 18, "line-height-ratio": "", "line-height": 19.8 }, "affect-bottom-margin": true }, { "type": "text", "id": "", "x": 250, "y": 10, "width": 200, "height": 20, "description": "", "display": true, "follow-stretch": "none", "texts": [ "vertical-align: top" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 18, "line-height-ratio": "", "line-height": 19.8 }, "affect-bottom-margin": true } ] }, { "id": "middle", "type": "detail", "height": 100, "auto-stretch": true, "items": [ { "type": "rect", "id": "", "x": 20, "y": 0, "width": 555.28, "height": 100, "border-radius": 0, "description": "", "display": true, "follow-stretch": "height", "style": { "border-color": "#000000", "border-style": "solid", "fill-color": "none", "border-width": 1 }, "affect-bottom-margin": false }, { "type": "rect", "id": "", "x": 30, "y": 10, "width": 200, "height": 40, "border-radius": 0, "description": "", "display": true, "follow-stretch": "height", "style": { "border-color": "#000000", "border-style": "dashed", "fill-color": "none", "border-width": 1 }, "affect-bottom-margin": false }, { "type": "text-block", "id": "text", "x": 30, "y": 10, "width": 200, "height": 40, "description": "", "reference-id": "", "value": "", "multiple-line": true, "display": true, "format": { "base": "", "type": "" }, "follow-stretch": "none", "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "middle", "letter-spacing": "", "overflow": "expand", "word-wrap": "break-word", "font-size": 18, "line-height-ratio": "", "line-height": 19.8 }, "affect-bottom-margin": true }, { "type": "text", "id": "", "x": 250, "y": 10, "width": 200, "height": 20, "description": "", "display": true, "follow-stretch": "none", "texts": [ "vertical-align: middle" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 18, "line-height-ratio": "", "line-height": 19.8 }, "affect-bottom-margin": true } ] }, { "id": "bottom", "type": "detail", "height": 100, "auto-stretch": true, "items": [ { "type": "rect", "id": "", "x": 20, "y": 0, "width": 555.28, "height": 100, "border-radius": 0, "description": "", "display": true, "follow-stretch": "height", "style": { "border-color": "#000000", "border-style": "solid", "fill-color": "none", "border-width": 1 }, "affect-bottom-margin": false }, { "type": "rect", "id": "", "x": 30, "y": 10, "width": 200, "height": 40, "border-radius": 0, "description": "", "display": true, "follow-stretch": "height", "style": { "border-color": "#000000", "border-style": "dashed", "fill-color": "none", "border-width": 1 }, "affect-bottom-margin": false }, { "type": "text-block", "id": "text", "x": 30, "y": 10, "width": 200, "height": 40, "description": "", "reference-id": "", "value": "", "multiple-line": true, "display": true, "format": { "base": "", "type": "" }, "follow-stretch": "none", "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "bottom", "letter-spacing": "", "overflow": "expand", "word-wrap": "break-word", "font-size": 18, "line-height-ratio": "", "line-height": 19.8 }, "affect-bottom-margin": true }, { "type": "text", "id": "", "x": 250, "y": 10, "width": 200, "height": 20, "description": "", "display": true, "follow-stretch": "none", "texts": [ "vertical-align: bottom" ], "style": { "font-family": [ "Helvetica" ], "color": "#000000", "font-style": [], "text-align": "left", "vertical-align": "top", "letter-spacing": "", "font-size": 18, "line-height-ratio": "", "line-height": 19.8 }, "affect-bottom-margin": true } ] } ], "state": { "layout-guides": [] } } ================================================ FILE: test/section_report/features/text_block_vertical_align/test_feature.rb ================================================ # frozen_string_literal: true require 'test_helper' class Thinreports::SectionReport::TestTextBlockVerticalAlignFeature < Thinreports::FeatureTest[__dir__] feature do params = { type: :section, layout_file: template_path, params: { groups: [ { details: %i(top middle bottom).flat_map { |id| [ { id: id, items: { text: 'short' } }, { id: id, items: { text: 'long' * 20 } } ] } } ] } } assert_pdf Thinreports.generate(params) end end ================================================ FILE: test/section_report/test_helper.rb ================================================ # frozen_string_literal: true require 'minitest/autorun' require 'minitest/unit' require 'mocha/minitest' require 'thinreports' require 'feature_test' Mocha.configure do |c| c.strict_keyword_argument_matching = true end ================================================ FILE: thinreports.gemspec ================================================ # frozen_string_literal: true rootdir = File.expand_path(File.dirname(__FILE__)) require "#{rootdir}/lib/thinreports/version" Gem::Specification.new do |s| s.name = 'thinreports' s.version = Thinreports::VERSION s.author = 'Matsukei Co.,Ltd.' s.email = 'thinreports@gmail.com' s.summary = 'An open source report generation tool for Ruby.' s.description = 'Thinreports is an open source report generation tool for Ruby.' s.homepage = 'http://www.thinreports.org' s.license = 'MIT' s.metadata = { 'rubygems_mfa_required' => 'true' } s.required_ruby_version = Gem::Requirement.new('>= 3.0.0') s.files = Dir.chdir(File.expand_path('..', __FILE__)) do `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^test/}) } end s.require_paths = ['lib'] s.add_dependency 'prawn', '>= 2.4.0' s.add_dependency 'prawn-disable_word_break', '>= 2.3.1' s.add_dependency 'base64' s.add_dependency 'bigdecimal' s.add_dependency 'matrix' s.add_dependency 'rexml' end