reports_web 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: cce3c314a64cb98332c0bd5f20ee01090efcb661fec493cd956200fec8ca554e
4
+ data.tar.gz: 56a79b610b2312d3b830276daca73404ccec9b6c776f40c9db953fe9dd6cc875
5
+ SHA512:
6
+ metadata.gz: 17ca2e2979d7c46b2ae716a4fd048113bbf5d307098fd7304120b1c892a408052b9d7f636c96015d5b9a784090d28c29ff6434611cb79a64e7ec14fb92ad7d63
7
+ data.tar.gz: a7c0cf4f546619cdb3f19c2e2a56c82fc93ba197e3e3c19fce540965cef75576eb45363db74fa2a1e92ddd86c797a815ae33e7f10a3de436c4651a9fd4690b63
data/LICENSE.md ADDED
@@ -0,0 +1,22 @@
1
+ # Reports.Web License / 使用許諾
2
+
3
+ Copyright (c) Pao@Office. All rights reserved.
4
+
5
+ This package is part of **Reports.Web**, a commercial product of Pao@Office.
6
+ Use of this package is governed by the Reports.Web End User License Agreement:
7
+
8
+ - 使用許諾(正式な条件): https://www.pao.ac/reports.web/manual/index.html#16
9
+ - 製品サイト: https://www.pao.ac/reports.web/
10
+ - ご購入・お見積もり: https://www.pao.ac/reports.web/buy.html
11
+
12
+ ## 要点 / Summary
13
+
14
+ - **体験版 / Trial** — 出力される帳票には赤い「SAMPLE」の印が付きます。購入前の評価に、無償でお使いいただけます。
15
+ Output carries a red "SAMPLE" mark. You may evaluate it free of charge before purchase.
16
+ - **製品版 / Product** — ご購入時に納品するライセンスファイルを Reports.Web エンジンに設定すると、「SAMPLE」の印が消えます。
17
+ Configure the license file delivered on purchase in the Reports.Web engine to remove the mark.
18
+ - **ライセンスの数え方 / Licensing** — 開発用パソコン 1 台につき 1 開発ライセンスです。運用専用のサーバーやエンドユーザーのパソコンには不要です(ランタイムライセンスフリー)。
19
+ One development license per development PC. Production servers and end-user PCs need no license (royalty-free runtime).
20
+
21
+ 上記の要点と使用許諾が異なる場合は、使用許諾が優先します。
22
+ If this summary and the license agreement differ, the license agreement prevails.
data/README.md ADDED
@@ -0,0 +1,93 @@
1
+ # reports_web
2
+
3
+ **Reports.Web for Ruby** — 帳票定義(`.prepdj`)に値を入れて印刷データ(PREPEJ)を作り、Reports.Web エンジン(C++ WebAssembly)で PDF にします。請求書・見積書・納品書・宛名ラベル・名刺など、日本の業務帳票向けです。
4
+
5
+ Build print data (PREPEJ) from a report definition (`.prepdj`) and render it to PDF with the Reports.Web engine (C++ WebAssembly). Designed for Japanese business forms: invoices, estimates, delivery slips, address labels, business cards.
6
+
7
+ - 製品サイト / Product: https://www.pao.ac/reports.web/
8
+ - サンプル / Samples: https://github.com/ReportsWeb/wasm-ruby
9
+ - オンラインデモ / Live demo: https://www.pao.ac/demo/reports.web/samples/ruby/
10
+
11
+ ## しくみ / How it works
12
+
13
+ ```
14
+ your Ruby app ──(PREPEJ JSON)──▶ Reports.Web engine (Docker: ghcr.io/reportsweb/engine) ──▶ PDF
15
+ ▲
16
+ report definition (.prepdj) designed in the browser designer
17
+ ```
18
+
19
+ 帳票のデザインは、ブラウザで動くデザイナーで作ります。アプリは値を入れて印刷データを作り、エンジンに送るだけです。描画はすべてエンジンが行います。
20
+
21
+ ## インストール / Install
22
+
23
+ ```sh
24
+ gem install reports_web # or: gem 'reports_web', '~> 1.0' in your Gemfile
25
+ docker run -d -p 3107:3107 ghcr.io/reportsweb/engine:1.0.0
26
+ ```
27
+
28
+ Ruby 3.1 以降。エンジンは Docker イメージで動きます。追加の gem は不要です(json・net/http を使います)。
29
+ Requires Ruby 3.1+. The engine runs as a Docker image. No extra gem dependencies.
30
+
31
+ ## 使い方 / Quick start
32
+
33
+ ```ruby
34
+ require 'json'
35
+ require 'reports_web'
36
+
37
+ definition = JSON.parse(File.read('report.prepdj', encoding: 'UTF-8'))
38
+
39
+ print_data = PaoReportsWeb::PrintData.new
40
+ .set_definition(definition)
41
+ .page_start
42
+ .set_value('Title', 'あっという間に帳票出力')
43
+ .set_value('CustomerName', '株式会社パオ')
44
+ .page_end
45
+
46
+ engine = PaoReportsWeb::ReportsWebEngine.new(url: 'http://127.0.0.1:3107')
47
+ File.binwrite('report.pdf', engine.render_pdf(print_data))
48
+ ```
49
+
50
+ ## API
51
+
52
+ ### `PaoReportsWeb::PrintData`
53
+
54
+ | メソッド / Method | 内容 / Description |
55
+ |---|---|
56
+ | `set_definition(definition)` | 帳票定義(`.prepdj` の内容)を設定 / Set the report definition |
57
+ | `page_start(definition = nil)` / `page_end` | ページの開始と終了。ページごとに別の定義も使えます / Begin and end a page |
58
+ | `set_value(name, value, index = 0, drawing = true)` | 文字などの値を入れる / Set a value |
59
+ | `set_repeated_value(name, value, x, y)` | 繰り返し(明細行など)の値 / Value for a repeated object |
60
+ | `set_image(name, source)` / `set_barcode(name, code)` | 画像・バーコード / Image and barcode |
61
+ | `change_attributes(name, values)` | 色・位置などの属性を変える / Change attributes |
62
+ | `to_h` / `to_json` / `save(path)` | 印刷データ(PREPEJ)として取り出す・保存する / Get or save the PREPEJ |
63
+
64
+ ### `PaoReportsWeb::ReportsWebEngine`
65
+
66
+ | メソッド / Method | 内容 / Description |
67
+ |---|---|
68
+ | `new(url:, timeout: 90, max_bytes:)` | 既定の URL は `REPORTS_ENGINE_URL`、なければ `http://127.0.0.1:3107` |
69
+ | `render_pdf(print_data)` | PDF のバイト列(String)を返します / Returns the PDF bytes |
70
+ | `health` | エンジンの状態 / Engine status |
71
+
72
+ エラーは `PaoReportsWeb::ReportsWebEngineError`(`status` に HTTP ステータス)で返ります。
73
+ Errors are raised as `PaoReportsWeb::ReportsWebEngineError` with the HTTP `status`.
74
+
75
+ ## 体験版と製品版 / Trial and product
76
+
77
+ この gem とエンジンのイメージは体験版です。出力には赤い「SAMPLE」の印が付きます。ご購入後に納品するライセンスファイルをエンジンに設定すると、印が消えます。
78
+
79
+ The gem and the engine image are trial builds: output carries a red "SAMPLE" mark. Mount the license file delivered on purchase to remove it:
80
+
81
+ ```sh
82
+ docker run -d -p 3107:3107 -v /path/to/reports-web.license:/app/reports-web.license:ro ghcr.io/reportsweb/engine:1.0.0
83
+ ```
84
+
85
+ 開発用パソコン 1 台につき 1 ライセンス、運用環境はランタイムライセンスフリーです。詳しくは [LICENSE.md](LICENSE.md) と [使用許諾](https://www.pao.ac/reports.web/manual/index.html#16) をご覧ください。
86
+
87
+ ## 他の言語 / Other languages
88
+
89
+ Reports.Web は同じ帳票定義と印刷データを、PHP・Java・.NET・Go・Ruby・Rust・Node.js から使えます。Pure Java・Pure Python・WPF 版もあります。
90
+ https://github.com/ReportsWeb
91
+
92
+ ---
93
+ Pao@Office — https://www.pao.ac/ — info@pao.ac
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+ require 'json'
3
+ require 'net/http'
4
+ require 'uri'
5
+
6
+ module PaoReportsWeb
7
+ # Error returned by the Reports.Web engine. +status+ is the HTTP status, or nil for transport errors.
8
+ class ReportsWebEngineError < StandardError
9
+ attr_reader :status
10
+
11
+ def initialize(message, status = nil)
12
+ super(message)
13
+ @status = status
14
+ end
15
+ end
16
+
17
+ # Client for the Reports.Web engine (ghcr.io/reportsweb/engine).
18
+ # Sends PREPEJ print data to POST /render/pdf and returns the PDF bytes.
19
+ class ReportsWebEngine
20
+ DEFAULT_MAX = 32 * 1024 * 1024
21
+
22
+ attr_reader :url
23
+
24
+ # url: engine base URL (default: REPORTS_ENGINE_URL or http://127.0.0.1:3107)
25
+ # timeout: seconds; max_bytes: largest PDF accepted from the engine
26
+ def initialize(url: nil, timeout: 90, max_bytes: DEFAULT_MAX)
27
+ @url = (url || ENV['REPORTS_ENGINE_URL'] || 'http://127.0.0.1:3107').sub(%r{/+\z}, '')
28
+ @timeout = timeout
29
+ @max_bytes = max_bytes
30
+ end
31
+
32
+ # GET /health. Returns the engine status, e.g. {"status"=>"ok", "engine"=>"C++ WebAssembly"}.
33
+ def health
34
+ response = request(Net::HTTP::Get.new(uri('/health')))
35
+ raise ReportsWebEngineError.new("Engine health check failed (HTTP #{response.code}).", response.code.to_i) unless response.is_a?(Net::HTTPSuccess)
36
+ JSON.parse(response.body)
37
+ end
38
+
39
+ # Render PREPEJ print data (PrintData, its Hash form, or JSON text) to a PDF (binary String).
40
+ def render_pdf(data, swap_pdf_image: nil)
41
+ payload = case data
42
+ when String then data
43
+ when PrintData then data.to_json
44
+ else JSON.generate(data)
45
+ end
46
+ raise ReportsWebEngineError, 'Print data exceeds 32 MiB.' if payload.bytesize > DEFAULT_MAX
47
+ req = Net::HTTP::Post.new(uri('/render/pdf'))
48
+ req['Content-Type'] = 'application/json'
49
+ req['X-Reports-Swap-Pdf-Image'] = swap_pdf_image.to_s unless swap_pdf_image.nil?
50
+ req.body = payload
51
+ response = request(req)
52
+ unless response.is_a?(Net::HTTPSuccess)
53
+ detail = begin
54
+ JSON.parse(response.body.to_s)['error']
55
+ rescue JSON::ParserError, TypeError
56
+ response.body.to_s
57
+ end
58
+ message = "Engine could not create the PDF (HTTP #{response.code})"
59
+ message += detail.to_s.empty? ? '.' : ": #{detail}"
60
+ raise ReportsWebEngineError.new(message, response.code.to_i)
61
+ end
62
+ pdf = response.body.to_s.b
63
+ raise ReportsWebEngineError, "PDF exceeds #{@max_bytes} bytes." if pdf.bytesize > @max_bytes
64
+ raise ReportsWebEngineError, 'Engine response is not a PDF.' unless pdf.start_with?('%PDF-')
65
+ pdf
66
+ end
67
+
68
+ private
69
+
70
+ def uri(path) = URI(@url + path)
71
+
72
+ def request(req)
73
+ target = req.uri
74
+ Net::HTTP.start(target.host, target.port, use_ssl: target.scheme == 'https',
75
+ open_timeout: [@timeout, 10].min, read_timeout: @timeout) { |http| http.request(req) }
76
+ rescue StandardError => e
77
+ raise ReportsWebEngineError, "Cannot reach the Reports.Web engine at #{@url}: #{e.message}"
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+ require 'json'
3
+ require 'tempfile'
4
+
5
+ module PaoReportsWeb
6
+ # Framework-independent builder for the shared PREPEJ print-document format.
7
+ class PrintData
8
+ FORMAT = 'Reports.net PrintData'
9
+ VERSION = '1.0'
10
+
11
+ def initialize
12
+ @definition = nil
13
+ @pages = []
14
+ @page = nil
15
+ end
16
+
17
+ def set_definition(definition)
18
+ raise ArgumentError, 'Cannot change the definition inside a page' if @page
19
+ self.class.validate_definition!(definition)
20
+ @pages.each { |page| page['Definition'] = deep_copy(@definition) if page['Definition'].nil? } if @definition
21
+ @definition = deep_copy(definition)
22
+ self
23
+ end
24
+
25
+ def page_start(definition = nil)
26
+ raise ArgumentError, 'Call page_end before starting another page' if @page
27
+ self.class.validate_definition!(definition) if definition
28
+ raise ArgumentError, 'Set a definition first' unless definition || @definition
29
+ @page = {'Index' => @pages.length + 1, 'Definition' => definition && deep_copy(definition), 'Values' => [], 'DynamicAttributes' => []}
30
+ self
31
+ end
32
+
33
+ def set_value(name, value, index = 0, drawing = true)
34
+ add_value(name, nil, value, index, drawing)
35
+ end
36
+
37
+ def set_repeated_value(name, value, x, y, drawing = true)
38
+ add_value(name, nil, value, 0, drawing, x, y)
39
+ end
40
+
41
+ def set_typed_value(name, kind, value, index = 0, drawing = true)
42
+ add_value(name, kind, value, index, drawing)
43
+ end
44
+
45
+ def set_image(name, source) = set_typed_value(name, 'DynamicImage', source)
46
+ def set_barcode(name, code) = set_typed_value(name, 'DynamicBarcode', code)
47
+
48
+ def change_attributes(name, values, index = 0)
49
+ change_repeated_attributes(name, values, index)
50
+ end
51
+
52
+ def change_repeated_attributes(name, values, index = 0, x: nil, y: nil, kind: nil)
53
+ page = current_page
54
+ validate_indices!(index, x, y)
55
+ raise ArgumentError, 'Attributes must be an object' unless values.is_a?(Hash)
56
+ normalized = values.to_h { |key, value| raise(ArgumentError, 'Attribute values cannot be null') if value.nil?; [key.to_s, scalar(value)] }
57
+ item = {'Name' => required(name), 'Type' => resolve_type(name, kind), 'Index' => index, 'Values' => normalized}
58
+ item.merge!('IndexX' => x, 'IndexY' => y) unless x.nil?
59
+ page['DynamicAttributes'] << item
60
+ self
61
+ end
62
+
63
+ def page_end
64
+ @pages << current_page
65
+ @page = nil
66
+ self
67
+ end
68
+
69
+ def to_h
70
+ raise ArgumentError, 'An unfinished page exists' if @page
71
+ raise ArgumentError, 'A definition is required' unless @definition
72
+ raise ArgumentError, 'At least one completed page is required' if @pages.empty?
73
+ deep_copy({'Format' => FORMAT, 'Version' => VERSION, 'Definition' => @definition, 'Pages' => @pages})
74
+ end
75
+
76
+ def to_json(pretty: false)
77
+ (pretty ? JSON.pretty_generate(to_h) : JSON.generate(to_h)) + "\n"
78
+ end
79
+
80
+ def save(path)
81
+ Tempfile.create(['.print-', '.tmp'], File.dirname(path)) do |file|
82
+ file.write(to_json(pretty: true)); file.flush; file.close
83
+ File.rename(file.path, path)
84
+ end
85
+ end
86
+
87
+ def self.validate_definition!(definition)
88
+ portable = definition.is_a?(Hash) && definition.key?('Version') && definition['Objects'].is_a?(Array) && %w[mm px].include?((definition['CoordinateUnit'] || 'mm').downcase)
89
+ web = definition.is_a?(Hash) && definition['format'] == 'pao-reports-web' && definition['version'] == 1 && definition['pages'].is_a?(Array) && !definition['pages'].empty?
90
+ raise ArgumentError, 'Invalid report definition' unless portable || web
91
+ end
92
+
93
+ private
94
+ def add_value(name, kind, value, index, drawing, x = nil, y = nil)
95
+ validate_indices!(index, x, y)
96
+ item = {'Name' => required(name), 'Type' => resolve_type(name, kind), 'Index' => index, 'Drawing' => drawing, 'Value' => scalar(value)}
97
+ item.merge!('IndexX' => x, 'IndexY' => y) unless x.nil?
98
+ current_page['Values'] << item
99
+ self
100
+ end
101
+ # 型省略時だけ、当該ページの定義を参照する。明示指定は保持する。
102
+ def resolve_type(name, explicit)
103
+ return required(explicit) unless explicit.nil?
104
+ d = current_page['Definition'] || @definition
105
+ objects = (d['Objects'] || []) + (d['pages'] || []).flat_map { |p| p['objects'] || [] }
106
+ kinds = %w[Text Line Square Circle Image Barcode]
107
+ web = {'text'=>'Text', 'line'=>'Line', 'rectangle'=>'Square', 'ellipse'=>'Circle', 'image'=>'Image', 'barcode'=>'Barcode'}
108
+ objects.each do |o|
109
+ next unless (o['Name'] || o['name']) == required(name)
110
+ return 'Dynamic' + kinds[o['Kind'].to_i] if /\A[0-5]\z/.match?(o['Kind'].to_s)
111
+ return 'Dynamic' + web[o['kind']] if web.key?(o['kind'])
112
+ end
113
+ 'DynamicText'
114
+ end
115
+ def current_page = @page || raise(ArgumentError, 'Call page_start first')
116
+ def required(value) = value.to_s.strip.then { |v| v.empty? ? raise(ArgumentError, 'A name is required') : v }
117
+ def scalar(value)
118
+ return nil if value.nil?
119
+ raise ArgumentError, 'A finite scalar is required' if value.is_a?(Float) && !value.finite?
120
+ return value.dup if value.is_a?(String)
121
+ return value.to_s if value.is_a?(Numeric) || value == true || value == false
122
+ raise ArgumentError, 'Values must be scalar'
123
+ end
124
+ def deep_copy(value) = Marshal.load(Marshal.dump(value))
125
+ def validate_indices!(index, x, y)
126
+ valid = index.is_a?(Integer) && index >= 0 && x.nil? == y.nil?
127
+ valid &&= x.is_a?(Integer) && x >= 0 && y.is_a?(Integer) && y >= 0 unless x.nil?
128
+ raise ArgumentError, 'Indices must be non-negative integers; supply both x and y, or neither' unless valid
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+ module PaoReportsWeb
3
+ GEM_VERSION = '1.0.0'
4
+ end
@@ -0,0 +1,2 @@
1
+ # frozen_string_literal: true
2
+ require_relative 'reports_web'
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+ # Reports.Web for Ruby: PaoReportsWeb::PrintData builds PREPEJ print data,
3
+ # PaoReportsWeb::ReportsWebEngine renders it to PDF with the Reports.Web engine.
4
+ require_relative 'pao_reports_web/version'
5
+ require_relative 'pao_reports_web/print_data'
6
+ require_relative 'pao_reports_web/engine'
metadata ADDED
@@ -0,0 +1,59 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: reports_web
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Pao@Office
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-10-02 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: 'Reports.Web is a report designer and PDF engine for Japanese business
14
+ forms (invoices, estimates, delivery slips, labels, business cards). This gem builds
15
+ print data (PREPEJ JSON) and renders it to PDF with the Reports.Web engine (C++
16
+ WebAssembly, Docker image ghcr.io/reportsweb/engine). Trial build: output carries
17
+ a red SAMPLE mark until a purchased license file is configured in the engine.'
18
+ email:
19
+ - info@pao.ac
20
+ executables: []
21
+ extensions: []
22
+ extra_rdoc_files: []
23
+ files:
24
+ - LICENSE.md
25
+ - README.md
26
+ - lib/pao_reports_web.rb
27
+ - lib/pao_reports_web/engine.rb
28
+ - lib/pao_reports_web/print_data.rb
29
+ - lib/pao_reports_web/version.rb
30
+ - lib/reports_web.rb
31
+ homepage: https://www.pao.ac/reports.web/
32
+ licenses:
33
+ - Nonstandard
34
+ metadata:
35
+ homepage_uri: https://www.pao.ac/reports.web/
36
+ source_code_uri: https://github.com/ReportsWeb/wasm-ruby
37
+ documentation_uri: https://github.com/ReportsWeb/wasm-ruby#readme
38
+ bug_tracker_uri: https://www.pao.ac/reports.web/
39
+ post_install_message:
40
+ rdoc_options: []
41
+ require_paths:
42
+ - lib
43
+ required_ruby_version: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '3.1'
48
+ required_rubygems_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: '0'
53
+ requirements: []
54
+ rubygems_version: 3.5.22
55
+ signing_key:
56
+ specification_version: 4
57
+ summary: 'Reports.Web for Ruby: build PREPEJ print data from .prepdj report definitions
58
+ and render PDF with the Reports.Web engine.'
59
+ test_files: []