jot_pdf 0.1.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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +63 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +121 -0
- data/Rakefile +19 -0
- data/Steepfile +11 -0
- data/lib/jot_pdf/core.rb +281 -0
- data/lib/jot_pdf/document.rb +508 -0
- data/lib/jot_pdf/version.rb +5 -0
- data/lib/jot_pdf.rb +8 -0
- data/sig/jot_pdf/core.rbs +96 -0
- data/sig/jot_pdf/document.rbs +129 -0
- data/sig/jot_pdf.rbs +3 -0
- metadata +89 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 3d54ecbe709e2588c085509efb20725711f4d5a3480d1767c5f5131c63c28437
|
4
|
+
data.tar.gz: 3189d100f3c69f510edcc26104052280b52462880e01e0d16133895ddb63ba51
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: fce06e57874121739df0f30b446e19693b121259b2898fa302e08a79d875cc1bd440824e05eb7614715e9ad3f4c207bd80d2d53cadfa0f49f6d24438f15deebe
|
7
|
+
data.tar.gz: 46372e13fb42152db255094a8822fd7431dac84b59ba5fc9ee3e51ffc27f35f45ae0c0d0ecb2d5aff1cf6f99315b2e76cc231a6d157e49e25e060cba5fa44195
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
AllCops:
|
2
|
+
NewCops: enable
|
3
|
+
TargetRubyVersion: 3.1
|
4
|
+
|
5
|
+
Style/StringLiterals:
|
6
|
+
Enabled: true
|
7
|
+
EnforcedStyle: double_quotes
|
8
|
+
|
9
|
+
Style/StringLiteralsInInterpolation:
|
10
|
+
Enabled: true
|
11
|
+
EnforcedStyle: double_quotes
|
12
|
+
|
13
|
+
Style/TrailingCommaInArguments:
|
14
|
+
EnforcedStyleForMultiline: consistent_comma
|
15
|
+
|
16
|
+
Style/TrailingCommaInArrayLiteral:
|
17
|
+
EnforcedStyleForMultiline: consistent_comma
|
18
|
+
|
19
|
+
Style/TrailingCommaInHashLiteral:
|
20
|
+
EnforcedStyleForMultiline: consistent_comma
|
21
|
+
|
22
|
+
Metrics/AbcSize:
|
23
|
+
Enabled: false
|
24
|
+
|
25
|
+
Metrics/CyclomaticComplexity:
|
26
|
+
Enabled: false
|
27
|
+
|
28
|
+
Metrics/MethodLength:
|
29
|
+
Enabled: false
|
30
|
+
|
31
|
+
Metrics/BlockLength:
|
32
|
+
Enabled: false
|
33
|
+
|
34
|
+
Metrics/ModuleLength:
|
35
|
+
Enabled: false
|
36
|
+
|
37
|
+
Metrics/PerceivedComplexity:
|
38
|
+
Enabled: false
|
39
|
+
|
40
|
+
Metrics/ParameterLists:
|
41
|
+
Enabled: false
|
42
|
+
|
43
|
+
Naming/BlockForwarding:
|
44
|
+
EnforcedStyle: explicit
|
45
|
+
|
46
|
+
Style/ArgumentsForwarding:
|
47
|
+
UseAnonymousForwarding: false
|
48
|
+
|
49
|
+
Style/Semicolon:
|
50
|
+
AllowAsExpressionSeparator: true
|
51
|
+
|
52
|
+
Naming/MethodParameterName:
|
53
|
+
Enabled: false
|
54
|
+
|
55
|
+
Lint/AmbiguousOperatorPrecedence:
|
56
|
+
Enabled: false
|
57
|
+
|
58
|
+
Lint/EmptyBlock:
|
59
|
+
Enabled: false
|
60
|
+
|
61
|
+
# TODO: enable this
|
62
|
+
Style/Documentation:
|
63
|
+
Enabled: false
|
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2024 coord_e
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
# JotPDF
|
2
|
+
|
3
|
+
Streaming PDF writer DSL for Ruby. Check out a live-editing demo with ruby.wasm: https://jotpdf.coord-e.dev/
|
4
|
+
|
5
|
+
## Status
|
6
|
+
|
7
|
+
JotPDF is in its early stages, and the API is fairly unstable.
|
8
|
+
|
9
|
+
## Usage
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
# Gemfile
|
13
|
+
gem 'jot_pdf'
|
14
|
+
```
|
15
|
+
|
16
|
+
JotPDF offers two types of DSLs. One is `JotPDF::Core`, a low-level API that directly exposes the structure of PDFs. The other is `JotPDF::Document`, a relatively high-level API built on top of `JotPDF::Core`, designed to be useful for actual document generation.
|
17
|
+
|
18
|
+
### JotPDF::Core
|
19
|
+
|
20
|
+
A PDF consists of a header, object definitions, `xref` (cross-reference table), and a trailer (including `trailer`, `startxref`, and an EOF marker).
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
require "jot_pdf"
|
24
|
+
|
25
|
+
JotPDF::Core.write($stdout) do
|
26
|
+
header
|
27
|
+
|
28
|
+
# Emit your objects here
|
29
|
+
|
30
|
+
xref
|
31
|
+
trailer do
|
32
|
+
end
|
33
|
+
end
|
34
|
+
```
|
35
|
+
|
36
|
+
Use `obj` to define an object; it returns a reference to the object.
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
require "jot_pdf"
|
40
|
+
|
41
|
+
JotPDF::Core.write($stdout) do
|
42
|
+
header
|
43
|
+
|
44
|
+
obj.of_dict do
|
45
|
+
entry("Type").of_name "Pages"
|
46
|
+
entry("Kids").of_array {}
|
47
|
+
entry("Count").of_int 0
|
48
|
+
end => pages_obj
|
49
|
+
|
50
|
+
obj.of_dict do
|
51
|
+
entry("Type").of_name "Catalog"
|
52
|
+
entry("Pages").of_ref pages_obj
|
53
|
+
end => catalog_obj
|
54
|
+
|
55
|
+
xref
|
56
|
+
trailer do
|
57
|
+
# JotPDF automatically inserts Size entry in the trailer dictionary
|
58
|
+
entry("Root").of_ref catalog_obj
|
59
|
+
end
|
60
|
+
end
|
61
|
+
```
|
62
|
+
|
63
|
+
Use `alloc_obj` to declare an object and emit its contents later.
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
require "jot_pdf"
|
67
|
+
|
68
|
+
JotPDF::Core.write($stdout) do
|
69
|
+
header
|
70
|
+
|
71
|
+
alloc_obj => pages_obj
|
72
|
+
|
73
|
+
obj.of_dict do
|
74
|
+
entry("Type").of_name "Catalog"
|
75
|
+
entry("Pages").of_ref pages_obj
|
76
|
+
end => catalog_obj
|
77
|
+
|
78
|
+
obj(pages_obj).of_dict do
|
79
|
+
entry("Type").of_name "Pages"
|
80
|
+
entry("Kids").of_array {}
|
81
|
+
entry("Count").of_int 0
|
82
|
+
end
|
83
|
+
|
84
|
+
xref
|
85
|
+
trailer do
|
86
|
+
entry("Root").of_ref catalog_obj
|
87
|
+
end
|
88
|
+
end
|
89
|
+
```
|
90
|
+
|
91
|
+
### JotPDF::Document
|
92
|
+
|
93
|
+
Use `page` to emit a page.
|
94
|
+
|
95
|
+
```ruby
|
96
|
+
require "jot_pdf"
|
97
|
+
|
98
|
+
JotPDF::Document.write($stdout) do
|
99
|
+
page width: 210, height: 297 do
|
100
|
+
text "Hello, World!", x: 10, y: 200
|
101
|
+
end
|
102
|
+
end
|
103
|
+
```
|
104
|
+
|
105
|
+
## Development
|
106
|
+
|
107
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
108
|
+
|
109
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
110
|
+
|
111
|
+
## Contributing
|
112
|
+
|
113
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/coord-e/jot_pdf. 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/coord-e/jot_pdf/blob/master/CODE_OF_CONDUCT.md).
|
114
|
+
|
115
|
+
## License
|
116
|
+
|
117
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
118
|
+
|
119
|
+
## Code of Conduct
|
120
|
+
|
121
|
+
Everyone interacting in the JotPDF project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/coord-e/jot_pdf/blob/master/CODE_OF_CONDUCT.md).
|
data/Rakefile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/gem_tasks"
|
4
|
+
require "rspec/core/rake_task"
|
5
|
+
|
6
|
+
RSpec::Core::RakeTask.new(:spec)
|
7
|
+
|
8
|
+
require "rubocop/rake_task"
|
9
|
+
|
10
|
+
RuboCop::RakeTask.new
|
11
|
+
|
12
|
+
require "steep/rake_task"
|
13
|
+
|
14
|
+
Steep::RakeTask.new do |t|
|
15
|
+
t.check.severity_level = :error
|
16
|
+
t.watch.verbose
|
17
|
+
end
|
18
|
+
|
19
|
+
task default: %i[steep spec rubocop]
|
data/Steepfile
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
D = Steep::Diagnostic
|
4
|
+
|
5
|
+
target :lib do
|
6
|
+
signature "sig"
|
7
|
+
check "lib"
|
8
|
+
configure_code_diagnostics(D::Ruby.default)
|
9
|
+
# TODO: How to type instance variables inside the DSL?
|
10
|
+
# configure_code_diagnostics(D::Ruby.strict)
|
11
|
+
end
|
data/lib/jot_pdf/core.rb
ADDED
@@ -0,0 +1,281 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "docile"
|
4
|
+
|
5
|
+
module JotPDF
|
6
|
+
module Core
|
7
|
+
class CrossReferenceTableEntry < Data.define(:offset, :generation, :usage)
|
8
|
+
end
|
9
|
+
|
10
|
+
# separating definition for steep
|
11
|
+
class CrossReferenceTableEntry
|
12
|
+
def self.default
|
13
|
+
CrossReferenceTableEntry.new(
|
14
|
+
offset: 0,
|
15
|
+
generation: 65_535,
|
16
|
+
usage: :free,
|
17
|
+
)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class ObjectRef < Data.define(:number, :generation)
|
22
|
+
end
|
23
|
+
|
24
|
+
class Writer
|
25
|
+
attr_reader :objects, :io, :offset
|
26
|
+
|
27
|
+
def initialize(io)
|
28
|
+
@objects = [
|
29
|
+
CrossReferenceTableEntry.default,
|
30
|
+
]
|
31
|
+
@section = [0]
|
32
|
+
@io = io
|
33
|
+
@offset = 0
|
34
|
+
end
|
35
|
+
|
36
|
+
def new_object
|
37
|
+
number = @objects.size
|
38
|
+
@objects << CrossReferenceTableEntry.default
|
39
|
+
ObjectRef.new(number:, generation: 0)
|
40
|
+
end
|
41
|
+
|
42
|
+
def update_object_entry(object_ref)
|
43
|
+
@section << object_ref.number
|
44
|
+
@objects[object_ref.number] = CrossReferenceTableEntry.new(
|
45
|
+
generation: object_ref.generation,
|
46
|
+
offset: @offset,
|
47
|
+
usage: :in_use,
|
48
|
+
)
|
49
|
+
end
|
50
|
+
|
51
|
+
def finish_section
|
52
|
+
result = @section.dup
|
53
|
+
@section.clear
|
54
|
+
result
|
55
|
+
end
|
56
|
+
|
57
|
+
def <<(data)
|
58
|
+
s = data.to_s
|
59
|
+
@io << s
|
60
|
+
@offset += s.bytesize
|
61
|
+
self
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
class WriteContext
|
66
|
+
def initialize(writer)
|
67
|
+
@writer = writer
|
68
|
+
end
|
69
|
+
|
70
|
+
def objects
|
71
|
+
@writer.objects
|
72
|
+
end
|
73
|
+
|
74
|
+
def dsl(&block)
|
75
|
+
Docile.dsl_eval(self, &block)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
class DictionaryWriteContext < WriteContext
|
80
|
+
def entry(name, &block)
|
81
|
+
@writer << "/#{name}"
|
82
|
+
if block
|
83
|
+
ObjectWriteContext.new(@writer).dsl(&block)
|
84
|
+
@writer << "\n"
|
85
|
+
else
|
86
|
+
# TODO: Why does this need annotation?
|
87
|
+
# @type var finalizer: ^() -> ObjectRef
|
88
|
+
finalizer = proc { @writer << "\n" }
|
89
|
+
ObjectInterm.new(writer: @writer, finalizer:)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
class ContentStreamWriteContext < WriteContext
|
95
|
+
def op(operator, &block)
|
96
|
+
ObjectWriteContext.new(@writer).dsl(&block) if block
|
97
|
+
@writer << " " << operator << "\n"
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
class ObjectWriteContext < WriteContext
|
102
|
+
def null
|
103
|
+
@writer << " null"
|
104
|
+
end
|
105
|
+
|
106
|
+
def bool(value)
|
107
|
+
@writer << " #{value}"
|
108
|
+
end
|
109
|
+
|
110
|
+
def name(name)
|
111
|
+
@writer << " /#{name}"
|
112
|
+
end
|
113
|
+
|
114
|
+
def num(value)
|
115
|
+
@writer << " " << value.to_s
|
116
|
+
end
|
117
|
+
|
118
|
+
def str(value)
|
119
|
+
@writer << " (" << value.to_s << ")"
|
120
|
+
end
|
121
|
+
|
122
|
+
def hexstr(value)
|
123
|
+
@writer << " <" << value.to_s << ">"
|
124
|
+
end
|
125
|
+
|
126
|
+
def ref(object_ref)
|
127
|
+
@writer << " #{object_ref.number} #{object_ref.generation} R"
|
128
|
+
end
|
129
|
+
|
130
|
+
def array(&block)
|
131
|
+
@writer << " ["
|
132
|
+
ObjectWriteContext.new(@writer).dsl(&block)
|
133
|
+
@writer << "]"
|
134
|
+
end
|
135
|
+
|
136
|
+
def dict(&block)
|
137
|
+
@writer << " <<\n"
|
138
|
+
DictionaryWriteContext.new(@writer).dsl(&block)
|
139
|
+
@writer << ">>"
|
140
|
+
end
|
141
|
+
|
142
|
+
def stream
|
143
|
+
@writer << "\nstream\n"
|
144
|
+
stream_start = @writer.offset
|
145
|
+
yield @writer
|
146
|
+
stream_size = @writer.offset - stream_start
|
147
|
+
@writer << "endstream"
|
148
|
+
stream_size
|
149
|
+
end
|
150
|
+
|
151
|
+
def content_stream(&block)
|
152
|
+
stream do |w|
|
153
|
+
ContentStreamWriteContext.new(w).dsl(&block)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
class ObjectInterm
|
159
|
+
def initialize(writer:, finalizer:)
|
160
|
+
@writer = writer
|
161
|
+
@finalizer = finalizer
|
162
|
+
end
|
163
|
+
|
164
|
+
def of_null
|
165
|
+
@writer << " null"
|
166
|
+
@finalizer.call
|
167
|
+
end
|
168
|
+
|
169
|
+
def of_bool(value)
|
170
|
+
@writer << " #{value}"
|
171
|
+
@finalizer.call
|
172
|
+
end
|
173
|
+
|
174
|
+
def of_name(name)
|
175
|
+
@writer << " /#{name}"
|
176
|
+
@finalizer.call
|
177
|
+
end
|
178
|
+
|
179
|
+
def of_num(i)
|
180
|
+
@writer << " #{i}"
|
181
|
+
@finalizer.call
|
182
|
+
end
|
183
|
+
|
184
|
+
def of_str(value)
|
185
|
+
@writer << " (" << value.to_s << ")"
|
186
|
+
@finalizer.call
|
187
|
+
end
|
188
|
+
|
189
|
+
def of_hexstr(value)
|
190
|
+
@writer << " <" << value.to_s << ">"
|
191
|
+
@finalizer.call
|
192
|
+
end
|
193
|
+
|
194
|
+
def of_ref(object_ref)
|
195
|
+
@writer << " #{object_ref.number} #{object_ref.generation} R"
|
196
|
+
@finalizer.call
|
197
|
+
end
|
198
|
+
|
199
|
+
def of_dict(&block)
|
200
|
+
@writer << " <<\n"
|
201
|
+
DictionaryWriteContext.new(@writer).dsl(&block)
|
202
|
+
@writer << ">>"
|
203
|
+
@finalizer.call
|
204
|
+
end
|
205
|
+
|
206
|
+
def of_array(&block)
|
207
|
+
@writer << " ["
|
208
|
+
ObjectWriteContext.new(@writer).dsl(&block)
|
209
|
+
@writer << " ]"
|
210
|
+
@finalizer.call
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
class DocumentContext < WriteContext
|
215
|
+
def header(version = "1.4")
|
216
|
+
@writer << "%PDF-#{version}\n"
|
217
|
+
@writer << "%\xff\xff\xff\xff\n"
|
218
|
+
end
|
219
|
+
|
220
|
+
def alloc_obj
|
221
|
+
@writer.new_object
|
222
|
+
end
|
223
|
+
|
224
|
+
def obj(object_ref = nil, &block)
|
225
|
+
object_ref ||= @writer.new_object
|
226
|
+
@writer.update_object_entry(object_ref)
|
227
|
+
@writer << "#{object_ref.number} #{object_ref.generation} obj"
|
228
|
+
if block
|
229
|
+
ObjectWriteContext.new(@writer).dsl(&block)
|
230
|
+
@writer << "\nendobj\n"
|
231
|
+
object_ref
|
232
|
+
else
|
233
|
+
# TODO: Why does this need annotation?
|
234
|
+
# @type var finalizer: ^() -> ObjectRef
|
235
|
+
finalizer = proc { @writer << "\nendobj\n"; object_ref }
|
236
|
+
ObjectInterm.new(writer: @writer, finalizer:)
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
def xref
|
241
|
+
@prev_xref_offset = @xref_offset
|
242
|
+
@xref_offset = @writer.offset
|
243
|
+
@writer << "xref\n"
|
244
|
+
section_objs = @writer.finish_section
|
245
|
+
section_objs.sort.slice_when { |prev, curr| curr != prev.next }.each do |subsection|
|
246
|
+
@writer << "#{subsection.first} #{subsection.size}\n"
|
247
|
+
subsection.each do |n|
|
248
|
+
object = objects[n]
|
249
|
+
u = object.usage == :in_use ? "n" : "f"
|
250
|
+
# each entry ends with SP LF
|
251
|
+
@writer << "#{object.offset.to_s.rjust(10, "0")} #{object.generation.to_s.rjust(5, "0")} #{u} \n"
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
def trailer(&block)
|
257
|
+
@writer << "trailer\n<<\n"
|
258
|
+
DictionaryWriteContext.new(@writer).dsl do
|
259
|
+
entry("Size").of_num objects.size
|
260
|
+
entry("Prev").of_num @prev_xref_offset if @prev_xref_offset
|
261
|
+
dsl(&block)
|
262
|
+
end
|
263
|
+
@writer << ">>\n"
|
264
|
+
|
265
|
+
@writer << "startxref\n"
|
266
|
+
@writer << @xref_offset.to_s
|
267
|
+
@writer << "\n"
|
268
|
+
|
269
|
+
@writer << "%%EOF\n"
|
270
|
+
end
|
271
|
+
|
272
|
+
def dsl(&block)
|
273
|
+
Docile.dsl_eval(self, &block)
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
def self.write(io, &block)
|
278
|
+
Docile.dsl_eval(DocumentContext.new(Writer.new(io)), &block)
|
279
|
+
end
|
280
|
+
end
|
281
|
+
end
|
@@ -0,0 +1,508 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "ttfunk"
|
4
|
+
require "ttfunk/subset"
|
5
|
+
|
6
|
+
module JotPDF
|
7
|
+
module Document
|
8
|
+
class StandardFont
|
9
|
+
attr_reader :name
|
10
|
+
|
11
|
+
def initialize(name)
|
12
|
+
@name = name
|
13
|
+
end
|
14
|
+
|
15
|
+
def unicode_to_code(codepoint)
|
16
|
+
raise if codepoint > 255
|
17
|
+
|
18
|
+
codepoint
|
19
|
+
end
|
20
|
+
|
21
|
+
def use(text)
|
22
|
+
text.each_codepoint.map { |c| format("%02x", unicode_to_code(c)) }.join
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class NonstandardFont
|
27
|
+
attr_reader :subset
|
28
|
+
|
29
|
+
def initialize(font)
|
30
|
+
@subset = TTFunk::Subset.for(font, :unicode_8bit)
|
31
|
+
end
|
32
|
+
|
33
|
+
def name
|
34
|
+
@subset.original.name.postscript_name
|
35
|
+
end
|
36
|
+
|
37
|
+
def unicode_to_code(codepoint)
|
38
|
+
@subset.from_unicode(codepoint)
|
39
|
+
end
|
40
|
+
|
41
|
+
def use(text)
|
42
|
+
text.each_codepoint.map { |c| @subset.use(c); format("%02x", unicode_to_code(c)) }.join
|
43
|
+
end
|
44
|
+
|
45
|
+
def encode_subset
|
46
|
+
@subset.encode
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
class FontManager
|
51
|
+
attr_writer :default_font
|
52
|
+
|
53
|
+
def initialize(default_font:)
|
54
|
+
@fonts = {}
|
55
|
+
@default_font = default_font
|
56
|
+
end
|
57
|
+
|
58
|
+
def load_font(path)
|
59
|
+
font = TTFunk::File.open(path)
|
60
|
+
name = font.name.postscript_name
|
61
|
+
@fonts[name] = NonstandardFont.new(font)
|
62
|
+
end
|
63
|
+
|
64
|
+
def require(spec = nil)
|
65
|
+
spec ||= @default_font
|
66
|
+
if spec.is_a? Symbol
|
67
|
+
@fonts[spec] ||= StandardFont.new(spec)
|
68
|
+
else
|
69
|
+
@fonts.fetch(spec)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def loaded_fonts
|
74
|
+
@fonts
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
class PageContext
|
79
|
+
def initialize(ctx, font_manager:)
|
80
|
+
@ctx = ctx
|
81
|
+
@font_manager = font_manager
|
82
|
+
end
|
83
|
+
|
84
|
+
def color(color = nil, r: nil, g: nil, b: nil)
|
85
|
+
# rubocop:disable Style/RedundantCondition
|
86
|
+
r, g, b =
|
87
|
+
if color
|
88
|
+
colorspec(color)
|
89
|
+
else
|
90
|
+
colorspec(r:, g:, b:)
|
91
|
+
end
|
92
|
+
# rubocop:enable Style/RedundantCondition
|
93
|
+
@ctx.dsl do
|
94
|
+
op("rg") { num r; num g; num b }
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def stroke_color(color = nil, r: nil, g: nil, b: nil)
|
99
|
+
# rubocop:disable Style/RedundantCondition
|
100
|
+
r, g, b =
|
101
|
+
if color
|
102
|
+
colorspec(color)
|
103
|
+
else
|
104
|
+
colorspec(r:, g:, b:)
|
105
|
+
end
|
106
|
+
# rubocop:enable Style/RedundantCondition
|
107
|
+
@ctx.dsl do
|
108
|
+
op("RG") { num r; num g; num b }
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def stroke_width(width)
|
113
|
+
@ctx.dsl do
|
114
|
+
op("w") { num width }
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def rect(x:, y:, width:, height:)
|
119
|
+
@ctx.dsl do
|
120
|
+
op("re") { num x; num y; num width; num height }
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def path(*args)
|
125
|
+
@ctx.dsl do
|
126
|
+
x0, y0 = args.shift
|
127
|
+
op("m") { num x0; num y0 }
|
128
|
+
args.each do |x, y|
|
129
|
+
op("l") { num x; num y }
|
130
|
+
end
|
131
|
+
op("h")
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def stroke
|
136
|
+
@ctx.dsl do
|
137
|
+
op("s")
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def fill
|
142
|
+
@ctx.dsl do
|
143
|
+
op("f")
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def text(text = nil, **kwargs, &block)
|
148
|
+
@ctx.dsl do
|
149
|
+
op("BT")
|
150
|
+
tc = TextContext.new(@ctx, font_manager: @font_manager, **kwargs)
|
151
|
+
tc.show text if text
|
152
|
+
tc.dsl(&block) if block
|
153
|
+
op("ET")
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def image(n, x:, y:, width:, height:)
|
158
|
+
@ctx.dsl do
|
159
|
+
op("cm") { num width; num 0; num 0; num height; num x; num y }
|
160
|
+
op("Do") { name n }
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def dsl(&block)
|
165
|
+
Docile.dsl_eval(self, &block)
|
166
|
+
end
|
167
|
+
|
168
|
+
private
|
169
|
+
|
170
|
+
def colorspec(color = nil, r: nil, g: nil, b: nil)
|
171
|
+
r ||= 0.0
|
172
|
+
g ||= 0.0
|
173
|
+
b ||= 0.0
|
174
|
+
|
175
|
+
if color
|
176
|
+
b = (color & 0xff).to_f
|
177
|
+
g = ((color >> 8) & 0xff).to_f
|
178
|
+
r = ((color >> 16) & 0xff).to_f
|
179
|
+
end
|
180
|
+
|
181
|
+
r /= 256.0 if r > 1
|
182
|
+
g /= 256.0 if g > 1
|
183
|
+
b /= 256.0 if b > 1
|
184
|
+
|
185
|
+
[r, g, b]
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
class TextContext < PageContext
|
190
|
+
undef_method :text
|
191
|
+
|
192
|
+
attr_reader :base_x, :base_y
|
193
|
+
|
194
|
+
def initialize(ctx, font_manager:, x: 0.0, y: 0.0, font: nil, size: nil, line_height: nil)
|
195
|
+
super(ctx, font_manager:)
|
196
|
+
@base_x = 0.0
|
197
|
+
@base_y = 0.0
|
198
|
+
move(x:, y:)
|
199
|
+
font(font, **{ size: }.compact)
|
200
|
+
@line_height = line_height
|
201
|
+
end
|
202
|
+
|
203
|
+
def font(spec, size: nil)
|
204
|
+
@font = @font_manager.require(spec)
|
205
|
+
size ||= @size
|
206
|
+
@size ||= size
|
207
|
+
@ctx.dsl do
|
208
|
+
op("Tf") { name @font.name; num(size || 15) }
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
def move(x:, y:)
|
213
|
+
@base_x += x
|
214
|
+
@base_y += y
|
215
|
+
@ctx.dsl do
|
216
|
+
op("Td") { num x; num y }
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
def linebreak(factor: 1.0)
|
221
|
+
# @type var factor: ::Numeric
|
222
|
+
move x: 0.0, y: -line_height * factor
|
223
|
+
end
|
224
|
+
|
225
|
+
def show(text)
|
226
|
+
@ctx.dsl do
|
227
|
+
text.each_line(chomp: true).with_index do |line, idx|
|
228
|
+
# @type self: JotPDF::Core::ContentStreamWriteContext & TextContext
|
229
|
+
move x: 0.0, y: -line_height unless idx.zero?
|
230
|
+
op("Tj") { hexstr @font.use(line) }
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
private
|
236
|
+
|
237
|
+
def line_height
|
238
|
+
@line_height || @size
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
class ImageContext
|
243
|
+
attr_reader :image_obj
|
244
|
+
|
245
|
+
def initialize(core, width:, height:)
|
246
|
+
@core = core
|
247
|
+
@width = width
|
248
|
+
@height = height
|
249
|
+
@mask_obj = core.alloc_obj
|
250
|
+
@image_obj = core.alloc_obj
|
251
|
+
end
|
252
|
+
|
253
|
+
def alpha(&block)
|
254
|
+
@core.dsl do
|
255
|
+
alloc_obj => length_obj
|
256
|
+
stream_size = nil
|
257
|
+
obj(@mask_obj) do
|
258
|
+
dict do
|
259
|
+
entry("Type").of_name "XObject"
|
260
|
+
entry("Subtype").of_name "Image"
|
261
|
+
entry("Width").of_num @width
|
262
|
+
entry("Height").of_num @height
|
263
|
+
entry("ColorSpace").of_name "DeviceGray"
|
264
|
+
entry("Decode").of_array { num 0; num 1 }
|
265
|
+
entry("BitsPerComponent").of_num 8
|
266
|
+
entry("Length").of_ref length_obj
|
267
|
+
end
|
268
|
+
stream(&block) => stream_size
|
269
|
+
end
|
270
|
+
obj(length_obj) { num stream_size }
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
def rgb(&block)
|
275
|
+
@core.dsl do
|
276
|
+
alloc_obj => length_obj
|
277
|
+
stream_size = nil
|
278
|
+
obj(@image_obj) do
|
279
|
+
dict do
|
280
|
+
entry("Type").of_name "XObject"
|
281
|
+
entry("Subtype").of_name "Image"
|
282
|
+
entry("Width").of_num @width
|
283
|
+
entry("Height").of_num @height
|
284
|
+
entry("ColorSpace").of_name "DeviceRGB"
|
285
|
+
entry("BitsPerComponent").of_num 8
|
286
|
+
entry("SMask").of_ref @mask_obj
|
287
|
+
entry("Length").of_ref length_obj
|
288
|
+
end
|
289
|
+
stream(&block) => stream_size
|
290
|
+
end
|
291
|
+
obj(length_obj) { num stream_size }
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
def dsl(&block)
|
296
|
+
Docile.dsl_eval(self, &block)
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
class DocumentWriter
|
301
|
+
attr_reader :pages, :images, :font_manager
|
302
|
+
|
303
|
+
def initialize(core, resources_obj:, pages_obj:)
|
304
|
+
@core = core
|
305
|
+
@pages = []
|
306
|
+
@pages_obj = pages_obj
|
307
|
+
@resources_obj = resources_obj
|
308
|
+
@images = {}
|
309
|
+
@font_manager = FontManager.new(default_font: :Helvetica)
|
310
|
+
end
|
311
|
+
|
312
|
+
def default_font(spec)
|
313
|
+
@font_manager.default_font = spec
|
314
|
+
end
|
315
|
+
|
316
|
+
def load_font(path)
|
317
|
+
@font_manager.load_font(path)
|
318
|
+
end
|
319
|
+
|
320
|
+
def image(n, width:, height:, &block)
|
321
|
+
ic = ImageContext.new(@core, width:, height:)
|
322
|
+
ic.dsl(&block)
|
323
|
+
@images[n] = ic.image_obj
|
324
|
+
end
|
325
|
+
|
326
|
+
def page(width:, height:, &block)
|
327
|
+
@core.dsl do
|
328
|
+
alloc_obj => length_obj
|
329
|
+
|
330
|
+
stream_size = nil
|
331
|
+
obj do
|
332
|
+
dict { entry("Length") { ref length_obj } }
|
333
|
+
content_stream do
|
334
|
+
PageContext.new(self, font_manager: @font_manager).dsl(&block)
|
335
|
+
end => stream_size
|
336
|
+
end => contents_obj
|
337
|
+
|
338
|
+
obj(length_obj) { num stream_size }
|
339
|
+
|
340
|
+
obj.of_dict do
|
341
|
+
entry("Type") { name "Page" }
|
342
|
+
entry("Parent") { ref @pages_obj }
|
343
|
+
entry("MediaBox").of_array { num 0; num 0; num width; num height }
|
344
|
+
entry("Resources") { ref @resources_obj }
|
345
|
+
entry("Contents") { ref contents_obj }
|
346
|
+
end => page_obj
|
347
|
+
@pages << page_obj
|
348
|
+
end
|
349
|
+
end
|
350
|
+
|
351
|
+
def dsl(&block)
|
352
|
+
Docile.dsl_eval(self, &block)
|
353
|
+
end
|
354
|
+
end
|
355
|
+
|
356
|
+
def self.generate_unicode_cmap(mapping)
|
357
|
+
<<~CMAP
|
358
|
+
/CIDInit /ProcSet findresource begin
|
359
|
+
12 dict begin
|
360
|
+
begincmap
|
361
|
+
/CIDSystemInfo 3 dict dup begin
|
362
|
+
/Registry (Adobe) def
|
363
|
+
/Ordering (UCS) def
|
364
|
+
/Supplement 0 def
|
365
|
+
end def
|
366
|
+
/CMapName /Adobe-Identity-UCS def
|
367
|
+
/CMapType 2 def
|
368
|
+
|
369
|
+
1 begincodespacerange
|
370
|
+
<00> <FF>
|
371
|
+
endcodespacerange
|
372
|
+
|
373
|
+
#{mapping.length} beginbfchar
|
374
|
+
#{mapping.map do |code, codepoint|
|
375
|
+
format("<%<code>02X><%<codepoint>s>", code:, codepoint: codepoint.chr(::Encoding::UTF_16BE).unpack1("H*"))
|
376
|
+
end.join("\n")}
|
377
|
+
endbfchar
|
378
|
+
|
379
|
+
endcmap
|
380
|
+
CMapName currentdict /CMap defineresource pop
|
381
|
+
end
|
382
|
+
end
|
383
|
+
CMAP
|
384
|
+
end
|
385
|
+
|
386
|
+
def self.write(io, &block)
|
387
|
+
Core.write(io) do
|
388
|
+
header
|
389
|
+
|
390
|
+
alloc_obj => resources_obj
|
391
|
+
alloc_obj => pages_obj
|
392
|
+
writer = Document::DocumentWriter.new(self, resources_obj:, pages_obj:)
|
393
|
+
writer.dsl(&block)
|
394
|
+
|
395
|
+
# @type var font_file_objs: Hash[::String | Symbol, ObjectRef]
|
396
|
+
# @type var widths_objs: Hash[::String | Symbol, ObjectRef]
|
397
|
+
# @type var tounicode_objs: Hash[::String | Symbol, ObjectRef]
|
398
|
+
font_file_objs = {}
|
399
|
+
widths_objs = {}
|
400
|
+
tounicode_objs = {}
|
401
|
+
writer.font_manager.loaded_fonts.each do |n, f|
|
402
|
+
# TODO: How can Steep use is_a? on _Font?
|
403
|
+
# rubocop:disable Style/CaseEquality
|
404
|
+
next unless NonstandardFont === f
|
405
|
+
# rubocop:enable Style/CaseEquality
|
406
|
+
|
407
|
+
subset_data = f.encode_subset
|
408
|
+
obj do
|
409
|
+
dict do
|
410
|
+
entry("Length").of_num subset_data.bytesize
|
411
|
+
entry("Length1").of_num subset_data.bytesize # always required for TrueType
|
412
|
+
end
|
413
|
+
stream do |stream|
|
414
|
+
stream << subset_data
|
415
|
+
end
|
416
|
+
end => font_file_obj
|
417
|
+
font_file_objs[n] = font_file_obj
|
418
|
+
|
419
|
+
subset = TTFunk::File.new(subset_data)
|
420
|
+
obj.of_array do
|
421
|
+
(subset.os2.first_char_index..subset.os2.last_char_index).each do |code|
|
422
|
+
gid = subset.cmap.tables.first[code]
|
423
|
+
width_in_units = subset.horizontal_metrics.for(gid).advance_width
|
424
|
+
num (Float(width_in_units) * 1000 / subset.header.units_per_em).to_i
|
425
|
+
end
|
426
|
+
end => widths_obj
|
427
|
+
widths_objs[n] = widths_obj
|
428
|
+
|
429
|
+
alloc_obj => length_obj
|
430
|
+
stream_size = nil
|
431
|
+
obj do
|
432
|
+
dict { entry("Length").of_ref length_obj }
|
433
|
+
stream do |w|
|
434
|
+
w << generate_unicode_cmap(f.subset.to_unicode_map)
|
435
|
+
end => stream_size
|
436
|
+
end => tounicode_obj
|
437
|
+
obj(length_obj).of_num stream_size
|
438
|
+
tounicode_objs[n] = tounicode_obj
|
439
|
+
end
|
440
|
+
|
441
|
+
obj(resources_obj).of_dict do
|
442
|
+
entry("XObject").of_dict do
|
443
|
+
writer.images.each do |n, r|
|
444
|
+
entry(n).of_ref r
|
445
|
+
end
|
446
|
+
end
|
447
|
+
entry("ProcSet").of_array { name "PDF"; name "Text"; name "ImageB"; name "ImageC"; name "ImageI" }
|
448
|
+
entry("Font").of_dict do
|
449
|
+
writer.font_manager.loaded_fonts.each do |n, f|
|
450
|
+
entry(n.to_s).of_dict do
|
451
|
+
entry("Type") { name "Font" }
|
452
|
+
case f
|
453
|
+
when StandardFont
|
454
|
+
entry("Subtype") { name "Type1" }
|
455
|
+
entry("BaseFont") { name n.to_s }
|
456
|
+
when NonstandardFont
|
457
|
+
subset = TTFunk::File.new(f.encode_subset)
|
458
|
+
# https://github.com/prawnpdf/prawn/blob/aaea7f6beda092ba48001414125a576dcf891362/lib/prawn/fonts/ttf.rb#L446-L447
|
459
|
+
base_name = subset.name.postscript_name[0, 33].delete("\0")
|
460
|
+
entry("Subtype").of_name "TrueType"
|
461
|
+
entry("FirstChar").of_num subset.os2.first_char_index
|
462
|
+
entry("LastChar").of_num subset.os2.last_char_index
|
463
|
+
entry("ToUnicode").of_ref tounicode_objs[n]
|
464
|
+
entry("BaseFont").of_name base_name
|
465
|
+
entry("Widths").of_ref widths_objs[n]
|
466
|
+
entry("FontDescriptor").of_dict do
|
467
|
+
entry("Ascent").of_num subset.ascent
|
468
|
+
entry("Descent").of_num subset.descent
|
469
|
+
entry("CapHeight").of_num subset.os2.cap_height
|
470
|
+
entry("StemV").of_num 0
|
471
|
+
entry("ItalicAngle").of_num 0
|
472
|
+
entry("Flags").of_num 0b100
|
473
|
+
entry("FontBBox").of_array do
|
474
|
+
subset.bbox.each do |i|
|
475
|
+
num i
|
476
|
+
end
|
477
|
+
end
|
478
|
+
entry("FontName").of_name base_name
|
479
|
+
entry("XHeight").of_num subset.os2.x_height
|
480
|
+
entry("FontFile2").of_ref font_file_objs[n]
|
481
|
+
end
|
482
|
+
end
|
483
|
+
end
|
484
|
+
end
|
485
|
+
end
|
486
|
+
end => resources_obj
|
487
|
+
|
488
|
+
obj(pages_obj).of_dict do
|
489
|
+
entry("Type") { name "Pages" }
|
490
|
+
entry("Kids").of_array do
|
491
|
+
writer.pages.each do |page|
|
492
|
+
ref page
|
493
|
+
end
|
494
|
+
end
|
495
|
+
entry("Count") { num writer.pages.size }
|
496
|
+
end
|
497
|
+
obj.of_dict do
|
498
|
+
entry("Type") { name "Catalog" }
|
499
|
+
entry("Pages") { ref pages_obj }
|
500
|
+
end => root_obj
|
501
|
+
xref
|
502
|
+
trailer do
|
503
|
+
entry("Root") { ref root_obj }
|
504
|
+
end
|
505
|
+
end
|
506
|
+
end
|
507
|
+
end
|
508
|
+
end
|
data/lib/jot_pdf.rb
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
module JotPDF
|
2
|
+
module Core
|
3
|
+
class CrossReferenceTableEntry
|
4
|
+
attr_reader offset: Integer
|
5
|
+
attr_reader generation: Integer
|
6
|
+
attr_reader usage: Symbol
|
7
|
+
|
8
|
+
def initialize: (offset: Integer, generation: Integer, usage: Symbol) -> void
|
9
|
+
def self.default: () -> CrossReferenceTableEntry
|
10
|
+
end
|
11
|
+
|
12
|
+
class ObjectRef
|
13
|
+
attr_reader number: Integer
|
14
|
+
attr_reader generation: Integer
|
15
|
+
|
16
|
+
def initialize: (number: Integer, generation: Integer) -> void
|
17
|
+
end
|
18
|
+
|
19
|
+
class Writer
|
20
|
+
@objects: Array[CrossReferenceTableEntry]
|
21
|
+
@io: IO
|
22
|
+
@offset: Integer
|
23
|
+
@section: Array[Integer]
|
24
|
+
|
25
|
+
attr_reader objects: Array[CrossReferenceTableEntry]
|
26
|
+
attr_reader io: IO
|
27
|
+
attr_reader offset: Integer
|
28
|
+
|
29
|
+
def initialize: (IO) -> void
|
30
|
+
def new_object: () -> ObjectRef
|
31
|
+
def update_object_entry: (ObjectRef) -> void
|
32
|
+
def finish_section: () -> Array[Integer]
|
33
|
+
def <<: (_ToS) -> self
|
34
|
+
end
|
35
|
+
|
36
|
+
class WriteContext
|
37
|
+
@writer: Writer
|
38
|
+
|
39
|
+
def initialize: (Writer) -> void
|
40
|
+
def objects: () -> Array[CrossReferenceTableEntry]
|
41
|
+
def dsl: [T] () { () [self: self] -> T } -> T
|
42
|
+
end
|
43
|
+
|
44
|
+
class DictionaryWriteContext < WriteContext
|
45
|
+
def entry: (_ToS) { () [self: ObjectWriteContext] -> untyped } -> void | (_ToS) -> ObjectInterm[void]
|
46
|
+
end
|
47
|
+
|
48
|
+
class ContentStreamWriteContext < WriteContext
|
49
|
+
def op: (_ToS) ?{ () [self: ObjectWriteContext] -> untyped } -> void
|
50
|
+
end
|
51
|
+
|
52
|
+
class ObjectWriteContext < WriteContext
|
53
|
+
def null: () -> void
|
54
|
+
def bool: (_ToS) -> void
|
55
|
+
def name: (_ToS) -> void
|
56
|
+
def num: (_ToS) -> void
|
57
|
+
def str: (_ToS) -> void
|
58
|
+
def hexstr: (_ToS) -> void
|
59
|
+
def ref: (ObjectRef) -> void
|
60
|
+
def array: () { () [self: ObjectWriteContext] -> untyped } -> void
|
61
|
+
def dict: () { () [self: DictionaryWriteContext] -> untyped } -> void
|
62
|
+
def stream: () { (Writer) -> untyped } -> Integer
|
63
|
+
def content_stream: () { () [self: ContentStreamWriteContext] -> untyped } -> Integer
|
64
|
+
end
|
65
|
+
|
66
|
+
class ObjectInterm[T]
|
67
|
+
@writer: Writer
|
68
|
+
@finalizer: ^() -> T
|
69
|
+
|
70
|
+
def initialize: (writer: Writer, finalizer: ^() -> T) -> void
|
71
|
+
def of_null: () -> T
|
72
|
+
def of_bool: (_ToS) -> T
|
73
|
+
def of_name: (_ToS) -> T
|
74
|
+
def of_num: (_ToS) -> T
|
75
|
+
def of_str: (_ToS) -> T
|
76
|
+
def of_hexstr: (_ToS) -> T
|
77
|
+
def of_ref: (ObjectRef) -> T
|
78
|
+
def of_dict: () { () [self: DictionaryWriteContext] -> untyped } -> T
|
79
|
+
def of_array: () { () [self: ObjectWriteContext] -> untyped } -> T
|
80
|
+
end
|
81
|
+
|
82
|
+
class DocumentContext < WriteContext
|
83
|
+
@prev_xref_offset: Integer?
|
84
|
+
@xref_offset: Integer?
|
85
|
+
|
86
|
+
def header: (?::String version) -> void
|
87
|
+
def alloc_obj: () -> ObjectRef
|
88
|
+
def obj: (?ObjectRef object_ref) { () [self: ObjectWriteContext] -> untyped } -> ObjectRef | (?ObjectRef object_ref) -> ObjectInterm[ObjectRef]
|
89
|
+
def xref: () -> void
|
90
|
+
def trailer: () { () [self: DictionaryWriteContext] -> untyped } -> void
|
91
|
+
def dsl: [T] () { () [self: self] -> T } -> T
|
92
|
+
end
|
93
|
+
|
94
|
+
def self.write: (IO) { () [self: DocumentContext] -> void } -> void
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
module JotPDF
|
2
|
+
module Document
|
3
|
+
interface _Font
|
4
|
+
def name: () -> (::String | ::Symbol)
|
5
|
+
def unicode_to_code: (::Integer) -> ::Integer
|
6
|
+
def use: (::String) -> ::String
|
7
|
+
end
|
8
|
+
|
9
|
+
class StandardFont
|
10
|
+
@name: ::Symbol
|
11
|
+
attr_reader name: ::Symbol
|
12
|
+
|
13
|
+
def initialize: (::Symbol) -> void
|
14
|
+
|
15
|
+
def unicode_to_code: (::Integer) -> ::Integer
|
16
|
+
def use: (::String) -> ::String
|
17
|
+
end
|
18
|
+
|
19
|
+
class NonstandardFont
|
20
|
+
@subset: TTFunk::Subset
|
21
|
+
attr_reader subset: TTFunk::Subset
|
22
|
+
|
23
|
+
def initialize: (TTFunk::File) -> void
|
24
|
+
|
25
|
+
def name: () -> ::String
|
26
|
+
def unicode_to_code: (::Integer) -> ::Integer
|
27
|
+
def use: (::String) -> ::String
|
28
|
+
|
29
|
+
def encode_subset: () -> ::String
|
30
|
+
end
|
31
|
+
|
32
|
+
class FontManager
|
33
|
+
@fonts: Hash[::String | ::Symbol, _Font]
|
34
|
+
@default_font: ::String | ::Symbol
|
35
|
+
attr_writer default_font: ::String | ::Symbol
|
36
|
+
|
37
|
+
def initialize: (default_font: ::String | ::Symbol) -> void
|
38
|
+
def load_font: (::String) -> _Font
|
39
|
+
def require: (?(::String | ::Symbol | nil)) -> _Font
|
40
|
+
def loaded_fonts: () -> Hash[::String | ::Symbol, _Font]
|
41
|
+
end
|
42
|
+
|
43
|
+
class PageContext
|
44
|
+
@ctx: JotPDF::Core::ContentStreamWriteContext
|
45
|
+
@font_manager: FontManager
|
46
|
+
|
47
|
+
def initialize: (JotPDF::Core::ContentStreamWriteContext, font_manager: FontManager) -> void
|
48
|
+
def color: (::Integer) -> void | (r: ::Float, g: ::Float, b: ::Float) -> void
|
49
|
+
def stroke_color: (::Integer) -> void | (r: ::Float, g: ::Float, b: ::Float) -> void
|
50
|
+
def stroke_width: (::Float) -> void
|
51
|
+
def rect: (x: ::Float, y: ::Float, width: ::Float, height: ::Float) -> void
|
52
|
+
def path: (*[::Float, ::Float]) -> void
|
53
|
+
def stroke: () -> void
|
54
|
+
def fill: () -> void
|
55
|
+
def text: (?::String?, ?x: ::Float, ?y: ::Float, ?font: ::String | ::Symbol, ?size: ::Float, ?line_height: ::Float) ?{ () [self: TextContext] -> void } -> void
|
56
|
+
def image: (::String, x: ::Float, y: ::Float, width: ::Float, height: ::Float) -> void
|
57
|
+
|
58
|
+
def dsl: [T] () { () [self: self] -> T } -> T
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def colorspec: (::Integer) -> [::Float, ::Float, ::Float] | (r: ::Float?, g: ::Float?, b: ::Float?) -> [::Float, ::Float, ::Float]
|
63
|
+
end
|
64
|
+
|
65
|
+
class TextContext < PageContext
|
66
|
+
@base_x: ::Float
|
67
|
+
@base_y: ::Float
|
68
|
+
@line_height: ::Float?
|
69
|
+
@font: _Font
|
70
|
+
@size: ::Float
|
71
|
+
attr_reader base_x: ::Float
|
72
|
+
attr_reader base_y: ::Float
|
73
|
+
|
74
|
+
def initialize: (JotPDF::Core::ContentStreamWriteContext, font_manager: FontManager, ?x: ::Float, ?y: ::Float, ?font: ::String?, ?size: ::Float?, ?line_height: ::Float?) -> void
|
75
|
+
|
76
|
+
def font: (::String | ::Symbol | nil, ?size: ::Float | nil) -> void
|
77
|
+
def move: (x: ::Float, y: ::Float) -> void
|
78
|
+
def linebreak: (?factor: ::Float) -> void
|
79
|
+
def show: (::String) -> void
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def line_height: () -> (::Float)
|
84
|
+
end
|
85
|
+
|
86
|
+
class ImageContext
|
87
|
+
@core: JotPDF::Core::DocumentContext
|
88
|
+
@width: ::Float
|
89
|
+
@height: ::Float
|
90
|
+
@mask_obj: JotPDF::Core::ObjectRef
|
91
|
+
@image_obj: JotPDF::Core::ObjectRef
|
92
|
+
|
93
|
+
attr_reader image_obj: JotPDF::Core::ObjectRef
|
94
|
+
|
95
|
+
def initialize: (JotPDF::Core::DocumentContext, width: ::Float, height: ::Float) -> void
|
96
|
+
|
97
|
+
def alpha: () { (JotPDF::Core::Writer) -> void } -> void
|
98
|
+
def rgb: () { (JotPDF::Core::Writer) -> void } -> void
|
99
|
+
|
100
|
+
def dsl: [T] () { () [self: self] -> T } -> T
|
101
|
+
end
|
102
|
+
|
103
|
+
class DocumentWriter
|
104
|
+
@core: JotPDF::Core::DocumentContext
|
105
|
+
@pages: Array[JotPDF::Core::ObjectRef]
|
106
|
+
@pages_obj: JotPDF::Core::ObjectRef
|
107
|
+
@resources_obj: JotPDF::Core::ObjectRef
|
108
|
+
@images: Hash[::String, JotPDF::Core::ObjectRef]
|
109
|
+
@font_manager: FontManager
|
110
|
+
|
111
|
+
attr_reader pages: Array[JotPDF::Core::ObjectRef]
|
112
|
+
attr_reader images: Hash[::String, JotPDF::Core::ObjectRef]
|
113
|
+
attr_reader font_manager: FontManager
|
114
|
+
|
115
|
+
def initialize: (JotPDF::Core::DocumentContext, resources_obj: JotPDF::Core::ObjectRef, pages_obj: JotPDF::Core::ObjectRef ) -> void
|
116
|
+
|
117
|
+
def default_font: (::String | ::Symbol) -> void
|
118
|
+
def load_font: (::String) -> void
|
119
|
+
def image: (::String, width: ::Float, height: ::Float) { () [self: ImageContext] -> void } -> void
|
120
|
+
def page: (width: ::Float, height: ::Float) { () [self: PageContext] -> void } -> void
|
121
|
+
|
122
|
+
def dsl: [T] () { () [self: DocumentWriter] -> T } -> T
|
123
|
+
end
|
124
|
+
|
125
|
+
def self.generate_unicode_cmap: (Hash[::Integer, ::Integer]) -> ::String
|
126
|
+
|
127
|
+
def self.write: (IO) { () [self: DocumentWriter] -> void } -> void
|
128
|
+
end
|
129
|
+
end
|
data/sig/jot_pdf.rbs
ADDED
metadata
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: jot_pdf
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- coord_e
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2025-01-16 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: docile
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: ttfunk
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.0'
|
41
|
+
description:
|
42
|
+
email:
|
43
|
+
- me@coord-e.com
|
44
|
+
executables: []
|
45
|
+
extensions: []
|
46
|
+
extra_rdoc_files: []
|
47
|
+
files:
|
48
|
+
- ".rspec"
|
49
|
+
- ".rubocop.yml"
|
50
|
+
- CHANGELOG.md
|
51
|
+
- LICENSE.txt
|
52
|
+
- README.md
|
53
|
+
- Rakefile
|
54
|
+
- Steepfile
|
55
|
+
- lib/jot_pdf.rb
|
56
|
+
- lib/jot_pdf/core.rb
|
57
|
+
- lib/jot_pdf/document.rb
|
58
|
+
- lib/jot_pdf/version.rb
|
59
|
+
- sig/jot_pdf.rbs
|
60
|
+
- sig/jot_pdf/core.rbs
|
61
|
+
- sig/jot_pdf/document.rbs
|
62
|
+
homepage: https://github.com/coord-e/jot_pdf
|
63
|
+
licenses:
|
64
|
+
- MIT
|
65
|
+
metadata:
|
66
|
+
homepage_uri: https://github.com/coord-e/jot_pdf
|
67
|
+
source_code_uri: https://github.com/coord-e/jot_pdf
|
68
|
+
changelog_uri: https://github.com/coord-e/jot_pdf/blob/main/CHANGELOG.md
|
69
|
+
rubygems_mfa_required: 'true'
|
70
|
+
post_install_message:
|
71
|
+
rdoc_options: []
|
72
|
+
require_paths:
|
73
|
+
- lib
|
74
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
75
|
+
requirements:
|
76
|
+
- - ">="
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: 3.1.0
|
79
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - ">="
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '0'
|
84
|
+
requirements: []
|
85
|
+
rubygems_version: 3.5.11
|
86
|
+
signing_key:
|
87
|
+
specification_version: 4
|
88
|
+
summary: Streaming PDF writer DSL
|
89
|
+
test_files: []
|