hadar 0.1.0 → 0.2.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 +12 -5
- data/lib/hadar/application.rb +20 -2
- data/lib/hadar/export/apng.rb +69 -0
- data/lib/hadar/version.rb +1 -1
- data/lib/hadar.rb +2 -0
- data/sig/hadar.rbs +11 -0
- metadata +17 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d7b4c769b40f5836617885858a50bc58e93ef8072b97430520ec2315ed81edbb
|
|
4
|
+
data.tar.gz: e8379cf1cc2f8039429d0fba6a650d8d7f059801e30c0df69864bf5ed94be8b0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b44313439e6fcc8a411c8b7fbc379a2050c843b8758dfa42352c4f007f5334141b45ced44d8d139a14dc030332d826744feaab621678e7e671760710f2ee1edd
|
|
7
|
+
data.tar.gz: 46f5d21160a6b804ba95067906abcc9f0277bf263a5aa51281ebe93ae0c1712982d8ebf0f328a3ca0dc2670a3f0a7573d67509b75f1787f6f477a64de1714d61
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
|
@@ -23,8 +23,8 @@ Missing, unreadable, and unsupported images fail preview construction with a
|
|
|
23
23
|
saving continues to write only the Markdown source. Its window host polls for
|
|
24
24
|
external Markdown edits and can show a next-slide, notes, and elapsed-time
|
|
25
25
|
presenter view. The host supports slide navigation, fullscreen, a fuzzy command
|
|
26
|
-
palette,
|
|
27
|
-
owned presenter window opens there fullscreen.
|
|
26
|
+
palette, PNG-sequence, and APNG export. When a secondary display is available,
|
|
27
|
+
the owned presenter window opens there fullscreen.
|
|
28
28
|
|
|
29
29
|
## Installation
|
|
30
30
|
|
|
@@ -160,6 +160,13 @@ Zaniah's headless renderer. It refuses to overwrite existing frames:
|
|
|
160
160
|
app.export_png_sequence("slides-png", width: 1280, height: 720)
|
|
161
161
|
```
|
|
162
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:
|
|
165
|
+
|
|
166
|
+
```ruby
|
|
167
|
+
app.export_apng("slides.apng", width: 1280, height: 720, duration_ms: 2500)
|
|
168
|
+
```
|
|
169
|
+
|
|
163
170
|
Speaker notes can be written as a one-line or multiline HTML comment. Their
|
|
164
171
|
Markdown remains in the source unchanged and does not appear in slide slots or
|
|
165
172
|
the preview tree:
|
|
@@ -219,15 +226,15 @@ directory rather than copying the file.
|
|
|
219
226
|
`Zaniah::UniformList`; `build(width:, height:)` returns the Zaniah element for
|
|
220
227
|
embedding in an application layout. Its `select(index)` method updates the
|
|
221
228
|
selection and invokes the optional `on_select` callback. Wezen is not involved
|
|
222
|
-
in live thumbnails; it encodes
|
|
229
|
+
in live thumbnails; it encodes rendered slide frames as APNG.
|
|
223
230
|
|
|
224
231
|
## Development
|
|
225
232
|
|
|
226
233
|
Run the specs with `bundle exec rake`. Check the 100-slide virtual-list
|
|
227
234
|
layout/scene-build budget with `BUDGET=1 bundle exec ruby bench/slide_list.rb`;
|
|
228
235
|
the headless benchmark skips software pixel rasterization. Hadar depends on
|
|
229
|
-
Beid, Antares, Kochab, Okab, Spica, Xamidimura, and Zaniah; its
|
|
230
|
-
`UniformList`, and input keymap APIs are reused directly.
|
|
236
|
+
Beid, Antares, Kochab, Okab, Spica, Wezen, Xamidimura, and Zaniah; its
|
|
237
|
+
declarative `Describe`, `UniformList`, and input keymap APIs are reused directly.
|
|
231
238
|
|
|
232
239
|
## License
|
|
233
240
|
|
data/lib/hadar/application.rb
CHANGED
|
@@ -115,7 +115,12 @@ module Hadar
|
|
|
115
115
|
def command_palette = @command_palette
|
|
116
116
|
|
|
117
117
|
def export_png_sequence(directory, width: 1280, height: 720)
|
|
118
|
-
PNGSequence.write(deck, directory, renderer: renderer, width: width, height: height)
|
|
118
|
+
Export::PNGSequence.write(deck, directory, renderer: renderer, width: width, height: height)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def export_apng(path, width: 1280, height: 720, duration_ms: Export::APNG::DEFAULT_DURATION_MS, loop: 0, overwrite: false)
|
|
122
|
+
Export::APNG.write(deck, path, renderer: renderer, width: width, height: height,
|
|
123
|
+
duration_ms: duration_ms, loop: loop, overwrite: overwrite)
|
|
119
124
|
end
|
|
120
125
|
|
|
121
126
|
def save(path = nil, overwrite: false)
|
|
@@ -393,7 +398,8 @@ module Hadar
|
|
|
393
398
|
"Toggle presentation" => ->(*) { presenter.started? ? stop_presentation : start_presentation },
|
|
394
399
|
"Toggle fullscreen" => ->(*) { toggle_fullscreen(window: :main) },
|
|
395
400
|
"Save" => ->(*) { save },
|
|
396
|
-
"Export PNG sequence…" => ->(*) { prompt_png_sequence_directory }
|
|
401
|
+
"Export PNG sequence…" => ->(*) { prompt_png_sequence_directory },
|
|
402
|
+
"Export APNG…" => ->(*) { prompt_apng_path }
|
|
397
403
|
}
|
|
398
404
|
end
|
|
399
405
|
|
|
@@ -406,6 +412,18 @@ module Hadar
|
|
|
406
412
|
export_png_sequence(directory) if directory && !directory.empty?
|
|
407
413
|
end
|
|
408
414
|
|
|
415
|
+
def prompt_apng_path
|
|
416
|
+
unless @main_window.respond_to?(:prompt_for_paths)
|
|
417
|
+
raise Error, "this window backend cannot choose a file; call export_apng(path)"
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
path = @main_window.prompt_for_paths(save: true).first
|
|
421
|
+
return unless path && !path.empty?
|
|
422
|
+
|
|
423
|
+
path = "#{path}.apng" if File.extname(path).empty?
|
|
424
|
+
export_apng(path, overwrite: true)
|
|
425
|
+
end
|
|
426
|
+
|
|
409
427
|
def slot_editor_for_selected_slide
|
|
410
428
|
return [nil, nil] unless selected_index
|
|
411
429
|
unless selected_slot_name
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tempfile"
|
|
4
|
+
|
|
5
|
+
module Hadar
|
|
6
|
+
module Export
|
|
7
|
+
class APNG
|
|
8
|
+
MAX_PIXELS = 32_000_000
|
|
9
|
+
DEFAULT_DURATION_MS = 3_000
|
|
10
|
+
|
|
11
|
+
def self.render(deck, renderer: Renderer.new, width: 1280, height: 720,
|
|
12
|
+
duration_ms: DEFAULT_DURATION_MS, loop: 0)
|
|
13
|
+
raise TypeError, "deck must be a Hadar::Deck" unless deck.is_a?(Deck)
|
|
14
|
+
raise Error, "cannot export an empty deck" if deck.empty?
|
|
15
|
+
raise TypeError, "renderer must respond to build" unless renderer.respond_to?(:build)
|
|
16
|
+
unless [width, height].all? { |dimension| dimension.is_a?(Integer) && dimension.positive? } && width * height <= MAX_PIXELS
|
|
17
|
+
raise ArgumentError, "APNG dimensions must be positive integers totaling at most #{MAX_PIXELS} pixels"
|
|
18
|
+
end
|
|
19
|
+
raise ArgumentError, "duration_ms must be a positive Integer" unless duration_ms.is_a?(Integer) && duration_ms.positive?
|
|
20
|
+
if duration_ms / duration_ms.gcd(1_000) > 65_535
|
|
21
|
+
raise ArgumentError, "duration_ms exceeds the APNG frame-delay limit"
|
|
22
|
+
end
|
|
23
|
+
raise ArgumentError, "loop must be a non-negative Integer" unless loop.is_a?(Integer) && loop >= 0
|
|
24
|
+
|
|
25
|
+
animation = Wezen::Animation.new(width: width, height: height, loop: loop)
|
|
26
|
+
window = Zaniah::Platform.open_window(backend: :headless, width: width, height: height)
|
|
27
|
+
window.text_system = Zaniah::TextSystem::Renderer.new
|
|
28
|
+
deck.slides.each do |slide|
|
|
29
|
+
window.render(renderer.build(slide), clear: slide.theme.colors.fetch("background"))
|
|
30
|
+
animation.add(window.device.pixels, delay_ms: duration_ms)
|
|
31
|
+
end
|
|
32
|
+
Wezen::APNG.encode(animation)
|
|
33
|
+
ensure
|
|
34
|
+
window&.close
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.write(deck, path, overwrite: false, **options)
|
|
38
|
+
raise TypeError, "path must be a String" unless path.is_a?(String)
|
|
39
|
+
raise ArgumentError, "path must not be empty or contain NUL" if path.empty? || path.include?("\0")
|
|
40
|
+
raise TypeError, "overwrite must be true or false" unless overwrite == true || overwrite == false
|
|
41
|
+
|
|
42
|
+
target = File.expand_path(path)
|
|
43
|
+
existing = begin
|
|
44
|
+
File.lstat(target)
|
|
45
|
+
rescue Errno::ENOENT
|
|
46
|
+
nil
|
|
47
|
+
end
|
|
48
|
+
raise Error, "refusing to write through a symbolic link" if existing&.symlink?
|
|
49
|
+
raise Error, "APNG target must be a regular file" if existing && !existing.file?
|
|
50
|
+
raise Error, "APNG target already exists; pass overwrite: true to replace it" if existing && !overwrite
|
|
51
|
+
|
|
52
|
+
bytes = render(deck, **options)
|
|
53
|
+
mode = existing ? existing.mode & 0o777 : (0o666 & ~File.umask)
|
|
54
|
+
Tempfile.create([".#{File.basename(target)}.", ".tmp"], File.dirname(target)) do |file|
|
|
55
|
+
file.binmode
|
|
56
|
+
file.write(bytes)
|
|
57
|
+
file.flush
|
|
58
|
+
file.chmod(mode)
|
|
59
|
+
file.fsync
|
|
60
|
+
file.close
|
|
61
|
+
overwrite ? File.rename(file.path, target) : File.link(file.path, target)
|
|
62
|
+
end
|
|
63
|
+
path
|
|
64
|
+
rescue SystemCallError => error
|
|
65
|
+
raise Error, "cannot write APNG: #{error.message}"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
data/lib/hadar/version.rb
CHANGED
data/lib/hadar.rb
CHANGED
|
@@ -7,6 +7,7 @@ require "antares"
|
|
|
7
7
|
require "kochab"
|
|
8
8
|
require "okab"
|
|
9
9
|
require "xamidimura"
|
|
10
|
+
require "wezen"
|
|
10
11
|
require "zaniah"
|
|
11
12
|
require "zaniah/ui"
|
|
12
13
|
|
|
@@ -29,3 +30,4 @@ end
|
|
|
29
30
|
|
|
30
31
|
require_relative "hadar/export/pdf"
|
|
31
32
|
require_relative "hadar/export/png_sequence"
|
|
33
|
+
require_relative "hadar/export/apng"
|
data/sig/hadar.rbs
CHANGED
|
@@ -131,6 +131,7 @@ module Hadar
|
|
|
131
131
|
def main_view: (width: Numeric, height: Numeric) -> Zaniah::Element
|
|
132
132
|
def command_palette: () -> CommandPalette?
|
|
133
133
|
def export_png_sequence: (String directory, ?width: Integer, ?height: Integer) -> Array[String]
|
|
134
|
+
def export_apng: (String path, ?width: Integer, ?height: Integer, ?duration_ms: Integer, ?loop: Integer, ?overwrite: bool) -> String
|
|
134
135
|
def save: (?String path, ?overwrite: bool) -> Deck
|
|
135
136
|
def selected_slot: () -> Slot?
|
|
136
137
|
def select_slot: (String | Symbol name) -> Slot
|
|
@@ -201,6 +202,16 @@ module Hadar
|
|
|
201
202
|
end
|
|
202
203
|
|
|
203
204
|
module Export
|
|
205
|
+
class APNG
|
|
206
|
+
MAX_PIXELS: Integer
|
|
207
|
+
DEFAULT_DURATION_MS: Integer
|
|
208
|
+
|
|
209
|
+
def self.render: (Deck deck, ?renderer: Renderer, ?width: Integer, ?height: Integer,
|
|
210
|
+
?duration_ms: Integer, ?loop: Integer) -> String
|
|
211
|
+
def self.write: (Deck deck, String path, ?overwrite: bool, ?renderer: Renderer, ?width: Integer,
|
|
212
|
+
?height: Integer, ?duration_ms: Integer, ?loop: Integer) -> String
|
|
213
|
+
end
|
|
214
|
+
|
|
204
215
|
class PNGSequence
|
|
205
216
|
MAX_PIXELS: Integer
|
|
206
217
|
|
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.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yudai Takada
|
|
@@ -93,6 +93,20 @@ dependencies:
|
|
|
93
93
|
- - "~>"
|
|
94
94
|
- !ruby/object:Gem::Version
|
|
95
95
|
version: 0.1.0
|
|
96
|
+
- !ruby/object:Gem::Dependency
|
|
97
|
+
name: wezen
|
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - "~>"
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: 0.1.1
|
|
103
|
+
type: :runtime
|
|
104
|
+
prerelease: false
|
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - "~>"
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: 0.1.1
|
|
96
110
|
- !ruby/object:Gem::Dependency
|
|
97
111
|
name: zaniah
|
|
98
112
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -127,6 +141,7 @@ files:
|
|
|
127
141
|
- lib/hadar/command_palette.rb
|
|
128
142
|
- lib/hadar/deck.rb
|
|
129
143
|
- lib/hadar/deck_watcher.rb
|
|
144
|
+
- lib/hadar/export/apng.rb
|
|
130
145
|
- lib/hadar/export/pdf.rb
|
|
131
146
|
- lib/hadar/export/png_sequence.rb
|
|
132
147
|
- lib/hadar/layout.rb
|
|
@@ -162,7 +177,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
162
177
|
- !ruby/object:Gem::Version
|
|
163
178
|
version: '0'
|
|
164
179
|
requirements: []
|
|
165
|
-
rubygems_version:
|
|
180
|
+
rubygems_version: 3.6.9
|
|
166
181
|
specification_version: 4
|
|
167
182
|
summary: Markdown-backed presentation editor and preview
|
|
168
183
|
test_files: []
|