rukbat 0.3.0 → 0.4.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 +4 -4
- data/CHANGELOG.md +4 -0
- data/README.md +118 -78
- data/lib/rukbat/pdf_file.rb +47 -24
- data/lib/rukbat/version.rb +1 -1
- metadata +5 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3a2a475aecf838cbaf8d29c8f72a546dfb56bf3db1fe006693ce4f7dd6a9edc3
|
|
4
|
+
data.tar.gz: 85e3dd7726823898c5f92072752c97ada40deb073d7f4c557a1f0792c6bf4327
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7c3b8efedcdcc9792c74fbcd3df9d038534b2dddaff73df32feb1e322261a4d2550d642e0ef6989d2c2de714ba8e2fb9dc219ae3f5ad55cf803382575c5b8305
|
|
7
|
+
data.tar.gz: 89ebb5d63ac35ae5bae9425103bf78a2f3f09039fd152c734d5d5e12b0cae0bd2f40469d88d8deebdf460104ca2261c0634ca8974ff5d9e4042ea19b64a07a7d
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.0 — 2026-09-24
|
|
4
|
+
|
|
5
|
+
- Export spreadsheet pages through Zaniah vector recording and Okab PDF drawing while preserving pagination, formatting, and searchable text.
|
|
6
|
+
|
|
3
7
|
## 0.3.0 — 2026-09-24
|
|
4
8
|
|
|
5
9
|
- Add range-based whole-number input validation with undo support.
|
data/README.md
CHANGED
|
@@ -1,29 +1,76 @@
|
|
|
1
|
-
|
|
1
|
+
<h1 align="center">Rukbat</h1>
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
<p align="center">
|
|
4
|
+
<strong>Ruby spreadsheet editor with sparse sheets, live formulas, and CSV/TSV workflows</strong>
|
|
5
|
+
</p>
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
against a 16.67 ms budget, and the 100,000-cell edit/recalculation benchmark
|
|
15
|
-
measured 1.773 s against a 3 s budget. Formula compatibility, public CI, and
|
|
16
|
-
dependency releases remain before the first release.
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="https://rubygems.org/gems/rukbat"><img src="https://img.shields.io/gem/v/rukbat.svg" alt="Gem version"></a>
|
|
9
|
+
<a href="https://rubygems.org/gems/rukbat"><img src="https://img.shields.io/gem/dt/rukbat.svg" alt="Gem downloads"></a>
|
|
10
|
+
<a href="https://github.com/noxdea/rukbat/actions/workflows/main.yml"><img src="https://github.com/noxdea/rukbat/actions/workflows/main.yml/badge.svg" alt="CI"></a>
|
|
11
|
+
<img src="https://img.shields.io/badge/Ruby-%3E%3D%203.2-cc342d.svg" alt="Ruby 3.2 or newer">
|
|
12
|
+
<a href="LICENSE.txt"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="MIT license"></a>
|
|
13
|
+
</p>
|
|
17
14
|
|
|
18
|
-
|
|
15
|
+
<p align="center">
|
|
16
|
+
<a href="#features">Features</a> ·
|
|
17
|
+
<a href="#installation">Installation</a> ·
|
|
18
|
+
<a href="#quick-start">Quick start</a> ·
|
|
19
|
+
<a href="#workbook-api">Workbook API</a> ·
|
|
20
|
+
<a href="#development">Development</a>
|
|
21
|
+
</p>
|
|
19
22
|
|
|
20
|
-
|
|
21
|
-
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
Rukbat combines [Denebola](https://github.com/noxdea/denebola)'s persistent,
|
|
26
|
+
sparse sheets with [Furud](https://github.com/noxdea/furud)'s formula engine.
|
|
27
|
+
It opens CSV and TSV files in a graphical editor or an interactive terminal,
|
|
28
|
+
and also exposes workbooks as a Ruby API.
|
|
29
|
+
Its name comes from Rukbat (α Sagittarii), Arabic *rukbat al-rāmī*
|
|
30
|
+
(“the archer’s knee”).
|
|
31
|
+
|
|
32
|
+
## Features
|
|
33
|
+
|
|
34
|
+
- A virtualized million-row grid with formula editing, completion, multi-sheet references, and range summaries
|
|
35
|
+
- Undo/redo, row and column edits, formatting, sorting, filtering, duplicate removal, find/replace, comments, and named ranges
|
|
36
|
+
- Whole-number input validation, conditional formatting, freeze panes, and static pivot tables
|
|
37
|
+
- Line, bar, pie, donut, scatter, area, and stacked charts in the editor
|
|
38
|
+
- CSV/TSV import and export, plus searchable PDF export with an embedded font
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
Rukbat requires Ruby 3.2 or newer. Install the published gem:
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
gem install rukbat
|
|
46
|
+
rukbat --version
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Or add `gem "rukbat"` to your Gemfile. The editor needs a supported desktop
|
|
50
|
+
display or an interactive terminal; the workbook API can be used without one.
|
|
51
|
+
|
|
52
|
+
## Quick start
|
|
53
|
+
|
|
54
|
+
Open a CSV file, start a new one, or select tab-delimited input and output:
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
rukbat sales.csv
|
|
58
|
+
rukbat new.csv
|
|
59
|
+
rukbat --tsv data.tsv
|
|
22
60
|
```
|
|
23
61
|
|
|
24
|
-
|
|
62
|
+
Use the mouse to select cells, arrow keys to move, and Enter or a double-click
|
|
63
|
+
to edit. The formula bar's **Apply** button commits its value. Save with
|
|
64
|
+
Ctrl-S (Cmd-S on macOS); Ctrl-Z/Cmd-Z undoes, and the shifted shortcut redoes.
|
|
65
|
+
The status bar shows the selected range's count, numeric count, sum, and average.
|
|
25
66
|
|
|
26
|
-
|
|
67
|
+
The toolbar provides formats, sorting, filtering, duplicate removal,
|
|
68
|
+
search/replace, comments, named ranges, conditional highlighting, freeze
|
|
69
|
+
panes, print areas, and charts.
|
|
70
|
+
|
|
71
|
+
## Workbook API
|
|
72
|
+
|
|
73
|
+
Coordinates and formula references are one-based:
|
|
27
74
|
|
|
28
75
|
```ruby
|
|
29
76
|
require "rukbat"
|
|
@@ -40,12 +87,28 @@ book.undo
|
|
|
40
87
|
book.redo
|
|
41
88
|
```
|
|
42
89
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
90
|
+
Denebola stores zero-based, immutable sheet snapshots internally. Undo and
|
|
91
|
+
redo retain prior roots instead of copying every cell; Rukbat feeds sparse
|
|
92
|
+
ranges to Furud through `CellSource`.
|
|
93
|
+
|
|
94
|
+
### Validation and pivots
|
|
95
|
+
|
|
96
|
+
Select a range in the editor, enter inclusive bounds, and choose **Apply
|
|
97
|
+
whole-number rule**. Nonblank edits, including calculated formula results,
|
|
98
|
+
must then be whole numbers within the bounds. Invalid edits are rejected
|
|
99
|
+
atomically; existing values are not changed when a rule is added. **Clear
|
|
100
|
+
rule** removes validation from the selected range. Rules follow structural
|
|
101
|
+
edits and undo/redo, but reset when a CSV/TSV workbook is reopened.
|
|
47
102
|
|
|
48
|
-
|
|
103
|
+
To create a pivot, select a rectangle including its header row, enter the
|
|
104
|
+
one-based key and value column positions within that selection, choose **Sum**
|
|
105
|
+
or **Count**, then **Create pivot**. The result is a static `Pivot` sheet (or
|
|
106
|
+
the next unused `Pivot2`, etc.), not a live link. Formula results are used;
|
|
107
|
+
groups retain first-seen order and match exact key values. Sum accepts finite
|
|
108
|
+
real numbers, while Count counts nonblank values. One Undo removes the
|
|
109
|
+
generated sheet.
|
|
110
|
+
|
|
111
|
+
## Files and export
|
|
49
112
|
|
|
50
113
|
```ruby
|
|
51
114
|
book = Rukbat::CSVFile.read("sales.csv", hint: "Windows-31J")
|
|
@@ -53,57 +116,34 @@ Rukbat::CSVFile.write(book, "sales-export.csv")
|
|
|
53
116
|
Rukbat::CSVFile.read("data.tsv", delimiter: :tsv)
|
|
54
117
|
```
|
|
55
118
|
|
|
56
|
-
Import
|
|
57
|
-
decimal literals
|
|
58
|
-
with CRLF row separators
|
|
59
|
-
default
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
key and value column positions relative to that selection, choose **Sum** or **Count**,
|
|
85
|
-
and choose **Create pivot**. Formula results are used; groups match exact key values
|
|
86
|
-
(text is case-sensitive), retain their first-seen order, and blank keys form their own
|
|
87
|
-
group. **Sum** adds finite real numbers only, and **Count**
|
|
88
|
-
counts nonblank values in the selected value column. The output is static data in a new
|
|
89
|
-
`Pivot` sheet (or the next unused `Pivot2`, etc.), not a live link to the source. At most
|
|
90
|
-
100,000 data rows and 10,000 groups can be summarized.
|
|
91
|
-
Creating a pivot leaves its source sheet active. CSV/TSV saves the active sheet only;
|
|
92
|
-
select the pivot sheet before saving if you want to export the summary instead. One
|
|
93
|
-
Undo removes the generated pivot sheet.
|
|
94
|
-
Freeze panes are kept per sheet and restored by undo/redo; the selected cell and
|
|
95
|
-
the row/column headers before it stay visible while scrolling. **Unfreeze** removes
|
|
96
|
-
all frozen rows and columns. Clear highlights removes conditional formatting
|
|
97
|
-
from the active sheet.
|
|
98
|
-
Select a range and choose **Set print area** to constrain PDF export; **Clear
|
|
99
|
-
print area** restores full-sheet output. **Export PDF** prompts for an embeddable
|
|
100
|
-
font and output path, applying the current print area. The CLI can also export
|
|
101
|
-
with `bundle exec rukbat --export-pdf report.pdf --font /path/to/font.ttf`.
|
|
102
|
-
Print areas are also session metadata and reset when the workbook is reopened.
|
|
103
|
-
PDF export embeds the supplied font, resolves formatted font families from the
|
|
104
|
-
system font database, and applies bold/italic along with cell formatting; bold
|
|
105
|
-
and italic are synthesized in the PDF when the selected face has no matching
|
|
106
|
-
variant. Charts are not yet rendered.
|
|
119
|
+
Import uses [Menkar](https://github.com/noxdea/menkar) to detect and decode
|
|
120
|
+
text, infers integer and decimal literals, and leaves other fields as strings.
|
|
121
|
+
Export writes UTF-8 with CRLF row separators and uses calculated formula
|
|
122
|
+
values by default; pass `values: :input` to export stored formulas. Saving an
|
|
123
|
+
opened file rejects external changes made since it was loaded.
|
|
124
|
+
|
|
125
|
+
CSV/TSV saves only the active sheet. Pivot creation leaves the source sheet
|
|
126
|
+
active, so activate the pivot sheet before exporting its summary. Formatting,
|
|
127
|
+
validation rules, print areas, and other workbook metadata are not stored in
|
|
128
|
+
CSV/TSV.
|
|
129
|
+
|
|
130
|
+
Set a print area in the editor to limit PDF output, then choose **Export PDF**
|
|
131
|
+
and supply an embeddable font. The command line can export directly:
|
|
132
|
+
|
|
133
|
+
```sh
|
|
134
|
+
rukbat --export-pdf report.pdf --font /path/to/font.ttf sales.csv
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
PDF export draws formatted cells from Zaniah's vector recording and keeps text
|
|
138
|
+
searchable in Okab's PDF output. Charts are not rendered
|
|
139
|
+
in PDFs. Print areas reset when the workbook is reopened.
|
|
140
|
+
|
|
141
|
+
## Limits
|
|
142
|
+
|
|
143
|
+
- Workbooks support 1,048,576 rows and 16,384 columns; the grid renders visible cells rather than all rows at once.
|
|
144
|
+
- A workbook supports up to 256 validation ranges. A pivot handles up to 100,000 data rows and 10,000 groups.
|
|
145
|
+
- CSV/TSV export is limited to 10 million cells; PDF export is limited to 100,000 cells.
|
|
146
|
+
- Rukbat does not open or save XLSX/ODS files. CSV/TSV does not preserve multiple sheets or workbook metadata.
|
|
107
147
|
|
|
108
148
|
## Development
|
|
109
149
|
|
|
@@ -115,6 +155,6 @@ bundle exec rbs -I sig -I "$(bundle info --path furud)/sig" -I "$(bundle info --
|
|
|
115
155
|
gem build --strict rukbat.gemspec
|
|
116
156
|
```
|
|
117
157
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
158
|
+
## License
|
|
159
|
+
|
|
160
|
+
Rukbat is released under the [MIT License](LICENSE.txt).
|
data/lib/rukbat/pdf_file.rb
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "tempfile"
|
|
4
|
+
require "zaniah/vector"
|
|
5
|
+
require "okab/zaniah_vector"
|
|
4
6
|
|
|
5
7
|
module Rukbat
|
|
6
8
|
module PDFFile
|
|
@@ -25,14 +27,17 @@ module Rukbat
|
|
|
25
27
|
columns = area ? area.right - area.left + 1 : [current.column_count, 1].max
|
|
26
28
|
raise Error, "PDF export exceeds #{MAX_CELLS} cells" if rows * columns > MAX_CELLS
|
|
27
29
|
|
|
30
|
+
renderer = Zaniah::TextSystem::Renderer.new(font: font.face)
|
|
28
31
|
document = Okab::Document.new(title: "#{sheet} — Rukbat", author: "Yudai Takada", creator: "Rukbat")
|
|
29
32
|
(0...rows).step(ROWS_PER_PAGE) do |row_offset|
|
|
30
33
|
(0...columns).step(COLUMNS_PER_PAGE) do |column_offset|
|
|
31
|
-
render_page(document, workbook, font, font_db, font_cache, sheet, origin_row, origin_column, row_offset, column_offset,
|
|
34
|
+
render_page(document, workbook, font, renderer, font_db, font_cache, sheet, origin_row, origin_column, row_offset, column_offset,
|
|
32
35
|
[rows - row_offset, ROWS_PER_PAGE].min, [columns - column_offset, COLUMNS_PER_PAGE].min)
|
|
33
36
|
end
|
|
34
37
|
end
|
|
35
38
|
document.render
|
|
39
|
+
ensure
|
|
40
|
+
renderer&.close
|
|
36
41
|
end
|
|
37
42
|
|
|
38
43
|
def write(workbook, path, font:, sheet: workbook.active_sheet, font_db: nil)
|
|
@@ -55,22 +60,35 @@ module Rukbat
|
|
|
55
60
|
raise Error, "cannot write PDF: #{error.message}"
|
|
56
61
|
end
|
|
57
62
|
|
|
58
|
-
def render_page(document, workbook, font, font_db, font_cache, sheet, origin_row, origin_column,
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
63
|
+
def render_page(document, workbook, font, renderer, font_db, font_cache, sheet, origin_row, origin_column,
|
|
64
|
+
row_offset, column_offset, row_count, column_count)
|
|
65
|
+
styled_text = []
|
|
66
|
+
vector = Zaniah::Vector.record(width: PAGE_WIDTH, height: PAGE_HEIGHT, text_system: renderer) do
|
|
67
|
+
Zaniah::Canvas.new do |_bounds, cx|
|
|
68
|
+
scene = cx.scene
|
|
69
|
+
first_row = origin_row + row_offset + 1
|
|
70
|
+
last_row = first_row + row_count - 1
|
|
71
|
+
header = "#{sheet} — rows #{first_row}-#{last_row}"
|
|
72
|
+
renderer.paint_line(scene, renderer.layout_line(header, font: font.face, size: 10),
|
|
73
|
+
x: MARGIN, y: MARGIN, color: "#000000")
|
|
74
|
+
data_top = PAGE_HEIGHT - MARGIN - 24
|
|
75
|
+
(0..row_count).each do |row_index|
|
|
76
|
+
(0..column_count).each do |column_index|
|
|
77
|
+
x = MARGIN + (column_index.zero? ? 0 : 38 + (column_index - 1) * CELL_WIDTH)
|
|
78
|
+
width = column_index.zero? ? 38 : CELL_WIDTH
|
|
79
|
+
y = data_top - row_index * CELL_HEIGHT
|
|
80
|
+
text, style = cell_text(workbook, sheet, origin_row, origin_column,
|
|
81
|
+
row_offset, column_offset, row_index, column_index)
|
|
82
|
+
paint_cell(scene, styled_text, renderer, text, style,
|
|
83
|
+
font_for_style(style, font, font_db, font_cache), x, y, width)
|
|
84
|
+
end
|
|
73
85
|
end
|
|
86
|
+
end.w(PAGE_WIDTH).h(PAGE_HEIGHT)
|
|
87
|
+
end
|
|
88
|
+
document.page(width: PAGE_WIDTH, height: PAGE_HEIGHT) do |page|
|
|
89
|
+
Okab::ZaniahVector.draw(page, vector)
|
|
90
|
+
styled_text.each do |text, x, y, font, size, color, bold, italic|
|
|
91
|
+
page.text(text, x: x, y: y, font: font, size: size, color: color, bold: bold, italic: italic)
|
|
74
92
|
end
|
|
75
93
|
end
|
|
76
94
|
end
|
|
@@ -87,13 +105,11 @@ module Rukbat
|
|
|
87
105
|
end
|
|
88
106
|
private_class_method :cell_text
|
|
89
107
|
|
|
90
|
-
def paint_cell(
|
|
108
|
+
def paint_cell(scene, styled_text, renderer, text, style, font, x, top, width)
|
|
91
109
|
y = top - CELL_HEIGHT
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
page.rect(x, y, width, CELL_HEIGHT).stroke(rgb(style[:border_color] || "#B8BEC8"),
|
|
96
|
-
width: style[:border_width] || 0.5)
|
|
110
|
+
scene.quad(x, PAGE_HEIGHT - top, width, CELL_HEIGHT,
|
|
111
|
+
color: style[:background] || "#0000", border_color: style[:border_color] || "#B8BEC8",
|
|
112
|
+
border_width: style[:border_width] || 0.5)
|
|
97
113
|
text = text.to_s.lines.first.to_s.chomp
|
|
98
114
|
text = text.encode(Encoding::UTF_8)
|
|
99
115
|
# ponytail: fixed-height PDF rows cap text at 13pt; variable page geometry can lift the ceiling.
|
|
@@ -111,8 +127,15 @@ module Rukbat
|
|
|
111
127
|
when :bottom then y + 2
|
|
112
128
|
else y + (CELL_HEIGHT - size) / 2
|
|
113
129
|
end
|
|
114
|
-
|
|
115
|
-
|
|
130
|
+
color = style[:color] || "#20242C"
|
|
131
|
+
if style[:bold] || style[:italic]
|
|
132
|
+
# GlyphRun has no synthetic-style fields; retain Okab's searchable
|
|
133
|
+
# faux-bold/italic text operator for those cells only.
|
|
134
|
+
styled_text << [text, text_x, baseline, font, size, rgb(color), !!style[:bold], !!style[:italic]]
|
|
135
|
+
else
|
|
136
|
+
renderer.paint_line(scene, renderer.layout_line(text, font: font.face, size: size),
|
|
137
|
+
x: text_x, y: PAGE_HEIGHT - baseline, color: color)
|
|
138
|
+
end
|
|
116
139
|
end
|
|
117
140
|
end
|
|
118
141
|
private_class_method :paint_cell
|
data/lib/rukbat/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rukbat
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yudai Takada
|
|
@@ -100,14 +100,14 @@ dependencies:
|
|
|
100
100
|
requirements:
|
|
101
101
|
- - "~>"
|
|
102
102
|
- !ruby/object:Gem::Version
|
|
103
|
-
version: 0.
|
|
103
|
+
version: 0.2.0
|
|
104
104
|
type: :runtime
|
|
105
105
|
prerelease: false
|
|
106
106
|
version_requirements: !ruby/object:Gem::Requirement
|
|
107
107
|
requirements:
|
|
108
108
|
- - "~>"
|
|
109
109
|
- !ruby/object:Gem::Version
|
|
110
|
-
version: 0.
|
|
110
|
+
version: 0.2.0
|
|
111
111
|
- !ruby/object:Gem::Dependency
|
|
112
112
|
name: spica
|
|
113
113
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -142,14 +142,14 @@ dependencies:
|
|
|
142
142
|
requirements:
|
|
143
143
|
- - "~>"
|
|
144
144
|
- !ruby/object:Gem::Version
|
|
145
|
-
version: 0.
|
|
145
|
+
version: 0.9.0
|
|
146
146
|
type: :runtime
|
|
147
147
|
prerelease: false
|
|
148
148
|
version_requirements: !ruby/object:Gem::Requirement
|
|
149
149
|
requirements:
|
|
150
150
|
- - "~>"
|
|
151
151
|
- !ruby/object:Gem::Version
|
|
152
|
-
version: 0.
|
|
152
|
+
version: 0.9.0
|
|
153
153
|
description: A Ruby spreadsheet app built on persistent sparse sheets and the Furud
|
|
154
154
|
calculation engine.
|
|
155
155
|
email:
|