hadar 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 +121 -102
- data/lib/hadar/export/pdf.rb +15 -88
- data/lib/hadar/renderer.rb +4 -1
- data/lib/hadar/version.rb +1 -1
- data/sig/hadar.rbs +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: ca92c68fe912ebaddf03792345b9747c02f994a1134f47c4481313f8312d0fe2
|
|
4
|
+
data.tar.gz: 5e1e1e5dd08e6516af6a540dd86c262a6da4ba175e532301a4e3c7fd5d9ebc7a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: aa6fe7bc92f62e5d4780e6e2b6236a82949f6bab93858243d734a4953146028028ff8b45345829c774b0d35277f9bb459b005178133768559ab473c22d6ddcab
|
|
7
|
+
data.tar.gz: 62d2be12ec8b34ab18bcc71866edd8a4ce8e40d0fc67f3cb5d853dab520a5e4f8fd28cda7e8ea4648946f471f9b410aec0c1e07d85cb6dff9ac6cc8c368fded8
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.0 - 2026-09-24
|
|
4
|
+
|
|
5
|
+
- Use the preview's Zaniah element tree for searchable vector PDF export, including freeform slides.
|
|
6
|
+
|
|
3
7
|
## 0.3.0 - 2026-09-24
|
|
4
8
|
|
|
5
9
|
- Add opt-in freeform slide placement with explicit Markdown directives and an editor warning.
|
data/README.md
CHANGED
|
@@ -1,38 +1,58 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
the
|
|
1
|
+
<h1 align="center">Hadar</h1>
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<strong>Markdown-backed slide decks with source-preserving editing, live preview, and presentation export</strong>
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="https://rubygems.org/gems/hadar"><img src="https://img.shields.io/gem/v/hadar.svg" alt="Gem version"></a>
|
|
9
|
+
<a href="https://rubygems.org/gems/hadar"><img src="https://img.shields.io/gem/dt/hadar.svg" alt="Gem downloads"></a>
|
|
10
|
+
<a href="https://github.com/noxdea/hadar/actions/workflows/main.yml"><img src="https://github.com/noxdea/hadar/actions/workflows/main.yml/badge.svg" alt="CI"></a>
|
|
11
|
+
<img src="https://img.shields.io/badge/CRuby-%3E%3D%203.2-cc342d.svg" alt="CRuby 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>
|
|
14
|
+
|
|
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="#editing-and-presenting">Editing and presenting</a> ·
|
|
20
|
+
<a href="#export">Export</a>
|
|
21
|
+
</p>
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
Hadar is a Ruby presentation library built around ordinary Markdown files.
|
|
26
|
+
[Beid](https://github.com/noxdea/beid) keeps the source-positioned document as
|
|
27
|
+
the only editable copy; Hadar projects it into slides, slots, and a live
|
|
28
|
+
[Zaniah](https://github.com/noxdea/zaniah) preview. Use its API to embed a
|
|
29
|
+
presentation window in an app or export a deck without a GUI.
|
|
30
|
+
|
|
31
|
+
## Features
|
|
32
|
+
|
|
33
|
+
- Eight automatic or explicitly selected slide templates, plus opt-in freeform placement
|
|
34
|
+
- Source-backed text, image, table-cell, and fenced-code editing with speaker notes
|
|
35
|
+
- Three built-in JSONC themes (`minimal`, `dark`, `warm`) and custom themes
|
|
36
|
+
- Virtualized slide thumbnails, keyboard navigation, command palette, and a secondary-display presenter view
|
|
37
|
+
- Searchable PDF, PNG-sequence, and animated PNG (APNG) export
|
|
28
38
|
|
|
29
39
|
## Installation
|
|
30
40
|
|
|
31
|
-
|
|
32
|
-
|
|
41
|
+
Hadar requires CRuby 3.2 or newer. Install the gem or add `gem "hadar"` to
|
|
42
|
+
your Gemfile:
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
gem install hadar
|
|
33
46
|
```
|
|
34
47
|
|
|
35
|
-
|
|
48
|
+
Hadar is a library; it does not install a `hadar` command. Windowed use
|
|
49
|
+
requires a Zaniah-supported display backend. PNG and APNG export use Zaniah's
|
|
50
|
+
headless renderer.
|
|
51
|
+
|
|
52
|
+
## Quick start
|
|
53
|
+
|
|
54
|
+
Separate slides with Markdown thematic breaks. Hadar selects a layout from
|
|
55
|
+
each slide's content unless you add a `layout` comment:
|
|
36
56
|
|
|
37
57
|
```ruby
|
|
38
58
|
require "hadar"
|
|
@@ -59,21 +79,16 @@ deck = Hadar::Deck.parse(<<~MARKDOWN)
|
|
|
59
79
|
:::
|
|
60
80
|
MARKDOWN
|
|
61
81
|
|
|
62
|
-
deck.slide(1).layout
|
|
63
|
-
deck.slide(1).slot(:left).text
|
|
64
|
-
deck.slide(0).notes # => nil when no speaker notes are present
|
|
65
|
-
|
|
82
|
+
deck.slide(1).layout # => :two_column
|
|
83
|
+
deck.slide(1).slot(:left).text # => "Revenue rose 18% year over year."
|
|
66
84
|
tree = Hadar::Renderer.new.describe(deck.slide(0))
|
|
67
|
-
element = Hadar::Renderer.new.build(deck.slide(0))
|
|
68
|
-
|
|
69
|
-
thumbnails = Hadar::SlideList.new(deck, selected: 0,
|
|
70
|
-
on_select: ->(slide, index) { puts "Selected slide #{index + 1}: #{slide.title}" })
|
|
71
|
-
list_element = thumbnails.build(width: 280, height: 640)
|
|
72
85
|
```
|
|
73
86
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
87
|
+
`Renderer#build` returns a Zaniah element for embedding in a preview.
|
|
88
|
+
|
|
89
|
+
## Editing and presenting
|
|
90
|
+
|
|
91
|
+
### Source-backed slots
|
|
77
92
|
|
|
78
93
|
Slots remain projections of the current Beid document. `slot.rich_text` returns
|
|
79
94
|
a Zaniah rich-text editor whose bold, italic, link, and code spans come from the
|
|
@@ -89,6 +104,7 @@ paragraph styles are rejected rather than flattening or normalizing markup.
|
|
|
89
104
|
Rich-text projection covers headings, paragraphs, block quotes, and text lists;
|
|
90
105
|
tables, fenced code blocks, and Markdown strikethrough are not editable through
|
|
91
106
|
this API yet.
|
|
107
|
+
|
|
92
108
|
Slots returned before a successful edit are stale snapshots. Opened decks save
|
|
93
109
|
atomically, preserve file permissions, and refuse to overwrite external changes:
|
|
94
110
|
|
|
@@ -101,22 +117,22 @@ deck.save
|
|
|
101
117
|
```
|
|
102
118
|
|
|
103
119
|
Opened decks can detect and reload external changes directly or through the
|
|
104
|
-
Zaniah platform watcher.
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
versions untouched. The host can keep its current slide selection and rebuild
|
|
108
|
-
the preview after the callback:
|
|
120
|
+
Zaniah platform watcher. Reload refuses to discard unsaved local edits. A
|
|
121
|
+
watcher is polled by the host's UI loop; successful reloads update the same
|
|
122
|
+
deck object and invoke `on_reload`:
|
|
109
123
|
|
|
110
124
|
```ruby
|
|
111
125
|
watcher = deck.watch(on_reload: ->(_deck) { window.request_frame })
|
|
112
126
|
watcher.poll(timeout: 0)
|
|
113
127
|
```
|
|
114
128
|
|
|
115
|
-
`deck.reload_if_changed` performs the same safe check without a
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
129
|
+
`deck.reload_if_changed` performs the same safe check without a watcher.
|
|
130
|
+
|
|
131
|
+
### Application and controls
|
|
132
|
+
|
|
133
|
+
`Hadar::Application` wires the watcher into attached windows and keeps the
|
|
134
|
+
selected slide index on reload. Its presenter view shows the next slide,
|
|
135
|
+
current notes, and elapsed time:
|
|
120
136
|
|
|
121
137
|
```ruby
|
|
122
138
|
app = Hadar::Application.new(deck)
|
|
@@ -125,47 +141,17 @@ app.attach(main_window: main)
|
|
|
125
141
|
app.run
|
|
126
142
|
```
|
|
127
143
|
|
|
128
|
-
`Application#run` polls
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
used as-is and is never moved or closed by Hadar. The automatically created
|
|
134
|
-
presenter closes when the main window closes. Rich-text slot editors are backed
|
|
135
|
-
by the current Markdown source and recreated after an external reload.
|
|
136
|
-
They keep the caret or selection (and editor focus) when selected text maps
|
|
137
|
-
unambiguously around one contiguous external edit; if an edit overlaps the
|
|
138
|
-
selection or makes the mapping ambiguous, the selection and focus are cleared
|
|
139
|
-
rather than moved to unrelated text. Arrow, Page Up/Down, Home, and End navigate
|
|
140
|
-
slides; `P` or `F5` starts presentation,
|
|
141
|
-
`F11` toggles fullscreen, and `Escape` exits presentation or fullscreen.
|
|
142
|
-
`Ctrl/Cmd-K` opens the fuzzy command palette; `Ctrl/Cmd-S` saves an opened deck
|
|
143
|
-
through its conflict-aware atomic writer.
|
|
144
|
-
|
|
145
|
-
PDF export produces one searchable 16:9 page for every slide, including all
|
|
146
|
-
eight layouts. Pass a TrueType font that contains every visible character; the
|
|
147
|
-
default font is selected from Zaniah's local font database:
|
|
148
|
-
|
|
149
|
-
```ruby
|
|
150
|
-
Hadar::Export::PDF.write(deck, "slides.pdf", font: "/path/to/font.ttf")
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
Okab currently requires TrueType outlines for PDF embedding. Hadar exports local
|
|
154
|
-
PNG and JPEG images; remote images and other image formats are rejected.
|
|
155
|
-
|
|
156
|
-
PNG sequence export renders all slides at the requested dimensions using
|
|
157
|
-
Zaniah's headless renderer. It refuses to overwrite existing frames:
|
|
144
|
+
`Application#run` polls and ticks its windows. With a second display, Hadar
|
|
145
|
+
opens its own fullscreen presenter window there; an explicitly passed
|
|
146
|
+
`presenter_window:` stays under the host's control. After an external reload,
|
|
147
|
+
rich-text editors are recreated. Their selection and focus survive only when
|
|
148
|
+
they map unambiguously around the edit.
|
|
158
149
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
Animated PNG export holds each slide for three seconds by default, loops forever,
|
|
164
|
-
and refuses to replace an existing target unless `overwrite: true` is passed:
|
|
150
|
+
Arrow keys, Page Up/Down, Home, and End navigate slides. `P` or `F5` starts
|
|
151
|
+
presentation; `F11` toggles fullscreen; `Escape` exits either mode.
|
|
152
|
+
`Ctrl/Cmd-K` opens the command palette, and `Ctrl/Cmd-S` saves an opened deck.
|
|
165
153
|
|
|
166
|
-
|
|
167
|
-
app.export_apng("slides.apng", width: 1280, height: 720, duration_ms: 2500)
|
|
168
|
-
```
|
|
154
|
+
### Notes and block editors
|
|
169
155
|
|
|
170
156
|
Speaker notes can be written as a one-line or multiline HTML comment. Their
|
|
171
157
|
Markdown remains in the source unchanged and does not appear in slide slots or
|
|
@@ -206,7 +192,7 @@ For a deck created with `Deck.parse`, pass a new path to `app.save(path)`.
|
|
|
206
192
|
Replacing an existing unrelated path requires `overwrite: true`. Opened decks
|
|
207
193
|
can use `app.save` or `Ctrl/Cmd-S`; the same external-change check applies.
|
|
208
194
|
|
|
209
|
-
|
|
195
|
+
### Layouts and themes
|
|
210
196
|
|
|
211
197
|
`title`, `title+body`, `two-column`, `image+text`, `full-bleed-image`,
|
|
212
198
|
`quote`, `code`, and `blank` are available. Explicit layout directives take
|
|
@@ -222,6 +208,11 @@ the referenced assets are not copied. If an absolute selected asset lives
|
|
|
222
208
|
outside the deck directory, its saved relative reference points outside that
|
|
223
209
|
directory rather than copying the file.
|
|
224
210
|
|
|
211
|
+
Built-in themes are `minimal`, `dark`, and `warm`. Custom JSONC themes can be
|
|
212
|
+
loaded with `Hadar::Theme.load(path)` and passed to `Deck.parse` or
|
|
213
|
+
`Deck.open`. A deck can also set a built-in theme in YAML front matter with
|
|
214
|
+
`theme: dark`.
|
|
215
|
+
|
|
225
216
|
Freeform placement is an explicit per-slide opt-in. Add `<!-- layout: freeform -->`
|
|
226
217
|
and one `<!-- place: x,y,width,height -->` immediately before each Markdown
|
|
227
218
|
block. Coordinates are percentages of the slide's inner canvas (after theme
|
|
@@ -246,19 +237,47 @@ After an external edit reloads a freeform slide, select its block again before
|
|
|
246
237
|
editing; source-order item numbers may have changed.
|
|
247
238
|
|
|
248
239
|
`SlideList` creates thumbnail rows only for the visible viewport, using
|
|
249
|
-
`Zaniah::UniformList
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
240
|
+
`Zaniah::UniformList`. `build(width:, height:)` returns a Zaniah element;
|
|
241
|
+
`select(index)` updates the selection and invokes `on_select` when supplied:
|
|
242
|
+
|
|
243
|
+
```ruby
|
|
244
|
+
thumbnails = Hadar::SlideList.new(deck, selected: 0,
|
|
245
|
+
on_select: ->(slide, index) { puts "Selected slide #{index + 1}: #{slide.title}" })
|
|
246
|
+
element = thumbnails.build(width: 280, height: 640)
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## Export
|
|
250
|
+
|
|
251
|
+
```ruby
|
|
252
|
+
Hadar::Export::PDF.write(deck, "slides.pdf", font: "/path/to/font.ttf")
|
|
253
|
+
Hadar::Export::PNGSequence.write(deck, "slides-png", width: 1280, height: 720)
|
|
254
|
+
Hadar::Export::APNG.write(deck, "slides.apng", width: 1280, height: 720,
|
|
255
|
+
duration_ms: 2500)
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
PDF creates one searchable 16:9 page per slide from the same element tree used
|
|
259
|
+
by PNG export, including freeform placement.
|
|
260
|
+
It needs a TrueType-outline font containing every visible character; without
|
|
261
|
+
`font:`, Hadar uses Zaniah's local font database. PDF embeds local PNG and JPEG
|
|
262
|
+
images, but rejects remote and other image formats. PNG-sequence export refuses
|
|
263
|
+
to replace existing frames. APNG defaults to three seconds per slide and
|
|
264
|
+
infinite looping; an existing target requires `overwrite: true`.
|
|
265
|
+
`Application#export_png_sequence` and
|
|
266
|
+
`#export_apng` wrap the corresponding exporters.
|
|
253
267
|
|
|
254
268
|
## Development
|
|
255
269
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
270
|
+
```sh
|
|
271
|
+
bundle install
|
|
272
|
+
bundle exec rake
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
Check the 100-slide thumbnail layout/scene-build budget with
|
|
276
|
+
`BUDGET=1 bundle exec ruby bench/slide_list.rb`. See the
|
|
277
|
+
[template-layout](docs/adr/001-template-layouts.md) and
|
|
278
|
+
[freeform-placement](docs/adr/002-opt-in-freeform-layout.md) decisions for the
|
|
279
|
+
source model.
|
|
261
280
|
|
|
262
281
|
## License
|
|
263
282
|
|
|
264
|
-
|
|
283
|
+
Hadar is released under the [MIT License](LICENSE.txt).
|
data/lib/hadar/export/pdf.rb
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "okab/zaniah_vector"
|
|
4
|
+
require "zaniah/vector"
|
|
5
|
+
|
|
3
6
|
module Hadar
|
|
4
7
|
module Export
|
|
5
8
|
class PDF
|
|
@@ -23,10 +26,12 @@ module Hadar
|
|
|
23
26
|
|
|
24
27
|
def render
|
|
25
28
|
document = Okab::Document.new(title: @deck.slides.first&.title, creator: "Hadar")
|
|
29
|
+
renderer = Renderer.new(font: @font.face)
|
|
26
30
|
@deck.slides.each do |slide|
|
|
31
|
+
ensure_glyphs!(renderer.describe(slide))
|
|
32
|
+
vector = Zaniah::Vector.record(width: WIDTH, height: HEIGHT) { renderer.build(slide) }
|
|
27
33
|
page = document.page(width: WIDTH, height: HEIGHT)
|
|
28
|
-
|
|
29
|
-
paint_slide(page, slide)
|
|
34
|
+
Okab::ZaniahVector.draw(page, vector)
|
|
30
35
|
document.outline(slide.title, page: page) unless slide.title.empty?
|
|
31
36
|
end
|
|
32
37
|
document.render
|
|
@@ -52,94 +57,16 @@ module Hadar
|
|
|
52
57
|
raise Error, "cannot load PDF font: #{error.message}"
|
|
53
58
|
end
|
|
54
59
|
|
|
55
|
-
def
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def paint_slide(page, slide)
|
|
60
|
-
margin = slide.theme.spacing.fetch("margin")
|
|
61
|
-
gap = slide.theme.spacing.fetch("gap")
|
|
62
|
-
title_size = slide.theme.font.fetch("title_size")
|
|
63
|
-
body_size = slide.theme.font.fetch("body_size")
|
|
64
|
-
text_color = rgb(slide.theme.colors.fetch("text"))
|
|
65
|
-
muted = rgb(slide.theme.colors.fetch("muted"))
|
|
66
|
-
width = WIDTH - margin * 2
|
|
60
|
+
def ensure_glyphs!(node)
|
|
61
|
+
if %i[text code_token].include?(node.type)
|
|
62
|
+
missing = node.props.fetch(:text).codepoints.find do |codepoint|
|
|
63
|
+
next false if codepoint == 0x0a || variation_selector?(codepoint)
|
|
67
64
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
text(page, slide.slot(:subtitle).text, margin, HEIGHT * 0.40, width, body_size, muted, :center)
|
|
72
|
-
when :two_column
|
|
73
|
-
paint_title(page, slide, margin, width, title_size, text_color)
|
|
74
|
-
column_width = (width - gap) / 2
|
|
75
|
-
y = HEIGHT - margin - title_size - gap - body_size
|
|
76
|
-
text(page, slide.slot(:left).text, margin, y, column_width, body_size, text_color)
|
|
77
|
-
text(page, slide.slot(:right).text, margin + column_width + gap, y, column_width, body_size, text_color)
|
|
78
|
-
when :image_text
|
|
79
|
-
paint_title(page, slide, margin, width, title_size, text_color)
|
|
80
|
-
y = HEIGHT - margin - title_size - gap - body_size
|
|
81
|
-
text(page, slide.slot(:text).text, margin, y, width * 0.48, body_size, text_color)
|
|
82
|
-
paint_image(page, slide.slot(:image), margin + width * 0.52, margin,
|
|
83
|
-
width * 0.48, HEIGHT - margin * 2 - title_size - gap)
|
|
84
|
-
when :full_bleed_image
|
|
85
|
-
paint_image(page, slide.slot(:image), 0, 0, WIDTH, HEIGHT, cover: true)
|
|
86
|
-
when :quote
|
|
87
|
-
text(page, slide.slot(:quote).text, margin + width * 0.08, HEIGHT * 0.62,
|
|
88
|
-
width * 0.84, title_size, text_color, :center)
|
|
89
|
-
text(page, slide.slot(:attribution).text, margin, HEIGHT * 0.28, width, body_size, muted, :center)
|
|
90
|
-
when :code
|
|
91
|
-
paint_title(page, slide, margin, width, title_size, text_color)
|
|
92
|
-
y = HEIGHT - margin - title_size - gap - body_size
|
|
93
|
-
text(page, slide.slot(:code).text, margin, y, width, body_size, text_color)
|
|
94
|
-
when :blank
|
|
95
|
-
nil
|
|
96
|
-
else
|
|
97
|
-
paint_title(page, slide, margin, width, title_size, text_color)
|
|
98
|
-
y = HEIGHT - margin - title_size - gap - body_size
|
|
99
|
-
text(page, slide.slot(:body).text, margin, y, width, body_size, text_color)
|
|
65
|
+
@font.face.glyph_id(codepoint).zero?
|
|
66
|
+
end
|
|
67
|
+
raise Error, "PDF font does not contain U+#{missing.to_s(16).upcase}; provide a font that covers the deck text" if missing
|
|
100
68
|
end
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
def paint_title(page, slide, margin, width, size, color)
|
|
104
|
-
text(page, slide.slot(:title).text, margin, HEIGHT - margin - size, width, size, color)
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
def text(page, value, x, y, width, size, color, align = :left)
|
|
108
|
-
return if value.nil? || value.empty?
|
|
109
|
-
|
|
110
|
-
value = value.gsub(/\r\n?/, "\n")
|
|
111
|
-
ensure_glyphs!(value)
|
|
112
|
-
page.text_block(value, x: x, y: y, width: width, font: @font, size: size,
|
|
113
|
-
line_height: size * 1.3, align: align, color: color)
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
def paint_image(page, slot, x, y, width, height, cover: false)
|
|
117
|
-
return if slot.empty?
|
|
118
|
-
|
|
119
|
-
image = Okab::Image.decode(File.binread(slot.resolved_image_path))
|
|
120
|
-
scales = [width / image.width, height / image.height]
|
|
121
|
-
scale = cover ? scales.max : scales.min
|
|
122
|
-
draw_width, draw_height = image.width * scale, image.height * scale
|
|
123
|
-
page.clip { |path| path.rect(x, y, width, height) } if cover
|
|
124
|
-
page.image(image, x: x + (width - draw_width) / 2, y: y + (height - draw_height) / 2,
|
|
125
|
-
width: draw_width, height: draw_height)
|
|
126
|
-
rescue Okab::Error, SystemCallError => error
|
|
127
|
-
raise Error, "cannot export image: #{error.message}"
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
def ensure_glyphs!(value)
|
|
131
|
-
missing = value.codepoints.find do |codepoint|
|
|
132
|
-
next false if codepoint == 0x0a || variation_selector?(codepoint)
|
|
133
|
-
|
|
134
|
-
@font.face.glyph_id(codepoint).zero?
|
|
135
|
-
end
|
|
136
|
-
return unless missing
|
|
137
|
-
|
|
138
|
-
raise Error, "PDF font does not contain U+#{missing.to_s(16).upcase}; provide a font that covers the deck text"
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
def rgb(color)
|
|
142
|
-
color.delete_prefix("#").scan(/../).first(3).map { |component| component.to_i(16) / 255.0 }
|
|
69
|
+
node.children.each { |child| ensure_glyphs!(child) }
|
|
143
70
|
end
|
|
144
71
|
|
|
145
72
|
def variation_selector?(codepoint)
|
data/lib/hadar/renderer.rb
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
module Hadar
|
|
4
4
|
class Renderer
|
|
5
|
-
def initialize
|
|
5
|
+
def initialize(font: nil)
|
|
6
6
|
@font_db = nil
|
|
7
7
|
@fonts = {}
|
|
8
|
+
@font_override = font
|
|
8
9
|
@vocabulary = build_vocabulary
|
|
9
10
|
end
|
|
10
11
|
|
|
@@ -92,6 +93,8 @@ module Hadar
|
|
|
92
93
|
end
|
|
93
94
|
|
|
94
95
|
def font(family)
|
|
96
|
+
return @font_override if @font_override
|
|
97
|
+
|
|
95
98
|
@fonts[family] ||= begin
|
|
96
99
|
@font_db ||= Zaniah::TextSystem::FontDB.new
|
|
97
100
|
@font_db.find(family: family == "sans-serif" ? nil : family)
|
data/lib/hadar/version.rb
CHANGED
data/sig/hadar.rbs
CHANGED
|
@@ -184,7 +184,7 @@ module Hadar
|
|
|
184
184
|
end
|
|
185
185
|
|
|
186
186
|
class Renderer
|
|
187
|
-
def initialize: () -> void
|
|
187
|
+
def initialize: (?font: Alhena::Font?) -> void
|
|
188
188
|
def describe: (Slide slide) -> Zaniah::Describe::Node
|
|
189
189
|
def build: (Slide slide) -> Zaniah::Element
|
|
190
190
|
def surface: (Slide slide) -> Zaniah::Describe::Surface
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: hadar
|
|
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
|
|
@@ -57,14 +57,14 @@ dependencies:
|
|
|
57
57
|
requirements:
|
|
58
58
|
- - "~>"
|
|
59
59
|
- !ruby/object:Gem::Version
|
|
60
|
-
version: 0.
|
|
60
|
+
version: 0.2.0
|
|
61
61
|
type: :runtime
|
|
62
62
|
prerelease: false
|
|
63
63
|
version_requirements: !ruby/object:Gem::Requirement
|
|
64
64
|
requirements:
|
|
65
65
|
- - "~>"
|
|
66
66
|
- !ruby/object:Gem::Version
|
|
67
|
-
version: 0.
|
|
67
|
+
version: 0.2.0
|
|
68
68
|
- !ruby/object:Gem::Dependency
|
|
69
69
|
name: spica
|
|
70
70
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -113,14 +113,14 @@ dependencies:
|
|
|
113
113
|
requirements:
|
|
114
114
|
- - "~>"
|
|
115
115
|
- !ruby/object:Gem::Version
|
|
116
|
-
version: 0.
|
|
116
|
+
version: 0.9.0
|
|
117
117
|
type: :runtime
|
|
118
118
|
prerelease: false
|
|
119
119
|
version_requirements: !ruby/object:Gem::Requirement
|
|
120
120
|
requirements:
|
|
121
121
|
- - "~>"
|
|
122
122
|
- !ruby/object:Gem::Version
|
|
123
|
-
version: 0.
|
|
123
|
+
version: 0.9.0
|
|
124
124
|
description: Projects Beid Markdown ASTs into template-based slide decks, edits supported
|
|
125
125
|
source-backed text, and exports PDF or PNG.
|
|
126
126
|
email:
|