giga-fast-kit 0.0.1
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/giga-fast-kit.gemspec +12 -0
- data/prawn-2.5.0/COPYING +2 -0
- data/prawn-2.5.0/GPLv2 +339 -0
- data/prawn-2.5.0/GPLv3 +674 -0
- data/prawn-2.5.0/LICENSE +56 -0
- data/prawn-2.5.0/data/fonts/Courier-Bold.afm +342 -0
- data/prawn-2.5.0/data/fonts/Courier-BoldOblique.afm +342 -0
- data/prawn-2.5.0/data/fonts/Courier-Oblique.afm +342 -0
- data/prawn-2.5.0/data/fonts/Courier.afm +342 -0
- data/prawn-2.5.0/data/fonts/Helvetica-Bold.afm +2827 -0
- data/prawn-2.5.0/data/fonts/Helvetica-BoldOblique.afm +2827 -0
- data/prawn-2.5.0/data/fonts/Helvetica-Oblique.afm +3051 -0
- data/prawn-2.5.0/data/fonts/Helvetica.afm +3051 -0
- data/prawn-2.5.0/data/fonts/MustRead.html +19 -0
- data/prawn-2.5.0/data/fonts/Symbol.afm +213 -0
- data/prawn-2.5.0/data/fonts/Times-Bold.afm +2588 -0
- data/prawn-2.5.0/data/fonts/Times-BoldItalic.afm +2384 -0
- data/prawn-2.5.0/data/fonts/Times-Italic.afm +2667 -0
- data/prawn-2.5.0/data/fonts/Times-Roman.afm +2419 -0
- data/prawn-2.5.0/data/fonts/ZapfDingbats.afm +225 -0
- data/prawn-2.5.0/lib/prawn/document/bounding_box.rb +625 -0
- data/prawn-2.5.0/lib/prawn/document/column_box.rb +177 -0
- data/prawn-2.5.0/lib/prawn/document/internals.rb +76 -0
- data/prawn-2.5.0/lib/prawn/document/span.rb +65 -0
- data/prawn-2.5.0/lib/prawn/document.rb +846 -0
- data/prawn-2.5.0/lib/prawn/encoding.rb +85 -0
- data/prawn-2.5.0/lib/prawn/errors.rb +73 -0
- data/prawn-2.5.0/lib/prawn/font.rb +567 -0
- data/prawn-2.5.0/lib/prawn/font_metric_cache.rb +50 -0
- data/prawn-2.5.0/lib/prawn/fonts/afm.rb +332 -0
- data/prawn-2.5.0/lib/prawn/fonts/dfont.rb +42 -0
- data/prawn-2.5.0/lib/prawn/fonts/otf.rb +14 -0
- data/prawn-2.5.0/lib/prawn/fonts/to_unicode_cmap.rb +151 -0
- data/prawn-2.5.0/lib/prawn/fonts/ttc.rb +41 -0
- data/prawn-2.5.0/lib/prawn/fonts/ttf.rb +615 -0
- data/prawn-2.5.0/lib/prawn/fonts.rb +14 -0
- data/prawn-2.5.0/lib/prawn/graphics/blend_mode.rb +62 -0
- data/prawn-2.5.0/lib/prawn/graphics/cap_style.rb +44 -0
- data/prawn-2.5.0/lib/prawn/graphics/color.rb +257 -0
- data/prawn-2.5.0/lib/prawn/graphics/dash.rb +123 -0
- data/prawn-2.5.0/lib/prawn/graphics/join_style.rb +60 -0
- data/prawn-2.5.0/lib/prawn/graphics/patterns.rb +389 -0
- data/prawn-2.5.0/lib/prawn/graphics/transformation.rb +172 -0
- data/prawn-2.5.0/lib/prawn/graphics/transparency.rb +79 -0
- data/prawn-2.5.0/lib/prawn/graphics.rb +791 -0
- data/prawn-2.5.0/lib/prawn/grid.rb +449 -0
- data/prawn-2.5.0/lib/prawn/image_handler.rb +61 -0
- data/prawn-2.5.0/lib/prawn/images/image.rb +48 -0
- data/prawn-2.5.0/lib/prawn/images/jpg.rb +121 -0
- data/prawn-2.5.0/lib/prawn/images/png.rb +383 -0
- data/prawn-2.5.0/lib/prawn/images.rb +195 -0
- data/prawn-2.5.0/lib/prawn/measurement_extensions.rb +82 -0
- data/prawn-2.5.0/lib/prawn/measurements.rb +129 -0
- data/prawn-2.5.0/lib/prawn/outline.rb +316 -0
- data/prawn-2.5.0/lib/prawn/repeater.rb +140 -0
- data/prawn-2.5.0/lib/prawn/security/arcfour.rb +54 -0
- data/prawn-2.5.0/lib/prawn/security.rb +308 -0
- data/prawn-2.5.0/lib/prawn/soft_mask.rb +105 -0
- data/prawn-2.5.0/lib/prawn/stamp.rb +147 -0
- data/prawn-2.5.0/lib/prawn/text/box.rb +192 -0
- data/prawn-2.5.0/lib/prawn/text/formatted/arranger.rb +396 -0
- data/prawn-2.5.0/lib/prawn/text/formatted/box.rb +725 -0
- data/prawn-2.5.0/lib/prawn/text/formatted/fragment.rb +373 -0
- data/prawn-2.5.0/lib/prawn/text/formatted/line_wrap.rb +335 -0
- data/prawn-2.5.0/lib/prawn/text/formatted/parser.rb +284 -0
- data/prawn-2.5.0/lib/prawn/text/formatted/wrap.rb +166 -0
- data/prawn-2.5.0/lib/prawn/text/formatted.rb +82 -0
- data/prawn-2.5.0/lib/prawn/text.rb +701 -0
- data/prawn-2.5.0/lib/prawn/transformation_stack.rb +63 -0
- data/prawn-2.5.0/lib/prawn/utilities.rb +34 -0
- data/prawn-2.5.0/lib/prawn/version.rb +6 -0
- data/prawn-2.5.0/lib/prawn/view.rb +116 -0
- data/prawn-2.5.0/lib/prawn.rb +83 -0
- metadata +114 -0
|
@@ -0,0 +1,615 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# prawn/font/ttf.rb : Implements AFM font support for Prawn
|
|
4
|
+
#
|
|
5
|
+
# Copyright May 2008, Gregory Brown / James Healy / Jamis Buck
|
|
6
|
+
# All Rights Reserved.
|
|
7
|
+
#
|
|
8
|
+
# This is free software. Please see the LICENSE and COPYING files for details.
|
|
9
|
+
|
|
10
|
+
require 'ttfunk'
|
|
11
|
+
require 'ttfunk/subset_collection'
|
|
12
|
+
require_relative 'to_unicode_cmap'
|
|
13
|
+
|
|
14
|
+
module Prawn
|
|
15
|
+
module Fonts
|
|
16
|
+
# TrueType font.
|
|
17
|
+
#
|
|
18
|
+
# @note You shouldn't use this class directly.
|
|
19
|
+
class TTF < Font
|
|
20
|
+
# TrueType font error.
|
|
21
|
+
class Error < StandardError
|
|
22
|
+
# @private
|
|
23
|
+
DEFAULT_MESSAGE = 'TTF font error'
|
|
24
|
+
|
|
25
|
+
# @private
|
|
26
|
+
MESSAGE_WITH_FONT = 'TTF font error in font %<font>s'
|
|
27
|
+
|
|
28
|
+
def initialize(message = DEFAULT_MESSAGE, font: nil)
|
|
29
|
+
if font && message == DEFAULT_MESSAGE
|
|
30
|
+
super(format(MESSAGE_WITH_FONT, font: font))
|
|
31
|
+
else
|
|
32
|
+
super(message)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Signals absence of a Unicode character map in the font.
|
|
38
|
+
class NoUnicodeCMap < Error
|
|
39
|
+
# @private
|
|
40
|
+
DEFAULT_MESSAGE = 'No unicode cmap found in font'
|
|
41
|
+
|
|
42
|
+
# @private
|
|
43
|
+
MESSAGE_WITH_FONT = 'No unicode cmap found in font %<font>s'
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Signals absense of a PostScript font name.
|
|
47
|
+
class NoPostscriptName < Error
|
|
48
|
+
# @private
|
|
49
|
+
DEFAULT_MESSAGE = 'Can not detect a postscript name'
|
|
50
|
+
|
|
51
|
+
# @private
|
|
52
|
+
MESSAGE_WITH_FONT = 'Can not detect a postscript name in font %<font>s'
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# TTFunk font.
|
|
56
|
+
# @return [TTFunk::File]
|
|
57
|
+
attr_reader :ttf
|
|
58
|
+
attr_reader :subsets
|
|
59
|
+
|
|
60
|
+
# Does this font support Unicode?
|
|
61
|
+
#
|
|
62
|
+
# @return [true]
|
|
63
|
+
def unicode?
|
|
64
|
+
true
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# An adapter for subset collection to represent a full font.
|
|
68
|
+
#
|
|
69
|
+
# @private
|
|
70
|
+
class FullFontSubsetsCollection
|
|
71
|
+
FULL_FONT = Object.new.tap do |obj|
|
|
72
|
+
obj.singleton_class.define_method(:inspect) do
|
|
73
|
+
super().insert(-2, ' FULL_FONT')
|
|
74
|
+
end
|
|
75
|
+
end.freeze
|
|
76
|
+
|
|
77
|
+
def initialize(original)
|
|
78
|
+
@original = original
|
|
79
|
+
|
|
80
|
+
(@cmap ||= original.cmap.unicode.first) || raise(NoUnicodeCMap.new(font: name))
|
|
81
|
+
|
|
82
|
+
@code_space_size =
|
|
83
|
+
case cmap.code_map.keys.max
|
|
84
|
+
when 0..0xff then 1
|
|
85
|
+
when 0x100..0xffff then 2
|
|
86
|
+
when 0x10000..0xffffff then 3
|
|
87
|
+
else
|
|
88
|
+
4
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Codespaces are not sequentional, they're ranges in
|
|
92
|
+
# a multi-dimentional space. Each byte is considered separately. So we
|
|
93
|
+
# have to maximally extend the lower two bytes in order to allow for
|
|
94
|
+
# continuos Unicode mapping.
|
|
95
|
+
# We only keep the highest byte because Unicode only goes to 1FFFFF
|
|
96
|
+
# and fonts usually cover even less of the space. We don't want to
|
|
97
|
+
# list all those unmapped charac codes here.
|
|
98
|
+
@code_space_max = cmap.code_map.keys.max | ('ff' * (code_space_size - 1)).to_i(16)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Encode characters.
|
|
102
|
+
#
|
|
103
|
+
# @return [Array<Array(FULL_FONT, String)>]
|
|
104
|
+
def encode(characters)
|
|
105
|
+
[
|
|
106
|
+
[
|
|
107
|
+
FULL_FONT,
|
|
108
|
+
characters.map { |c|
|
|
109
|
+
check_bounds!(c)
|
|
110
|
+
[cmap[c]].pack('n')
|
|
111
|
+
}.join(''),
|
|
112
|
+
],
|
|
113
|
+
]
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
private
|
|
117
|
+
|
|
118
|
+
attr_reader :cmap
|
|
119
|
+
attr_reader :code_space_size
|
|
120
|
+
attr_reader :code_space_max
|
|
121
|
+
|
|
122
|
+
def check_bounds!(num)
|
|
123
|
+
if num > code_space_max
|
|
124
|
+
raise Error, "CID (#{num}) exceedes code space size"
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# @param document [Prawn::Document]
|
|
130
|
+
# @param name [String] font file path
|
|
131
|
+
# @param options [Hash]
|
|
132
|
+
# @option options :family [String]
|
|
133
|
+
# @option options :style [Symbol]
|
|
134
|
+
def initialize(document, name, options = {})
|
|
135
|
+
super
|
|
136
|
+
|
|
137
|
+
@ttf = read_ttf_file
|
|
138
|
+
@subsets =
|
|
139
|
+
if full_font_embedding
|
|
140
|
+
FullFontSubsetsCollection.new(@ttf)
|
|
141
|
+
else
|
|
142
|
+
TTFunk::SubsetCollection.new(@ttf)
|
|
143
|
+
end
|
|
144
|
+
@italic_angle = nil
|
|
145
|
+
|
|
146
|
+
@attributes = {}
|
|
147
|
+
@bounding_boxes = {}
|
|
148
|
+
@char_widths = {}
|
|
149
|
+
@has_kerning_data = @ttf.kerning.exists? && @ttf.kerning.tables.any?
|
|
150
|
+
|
|
151
|
+
@ascender = Integer(@ttf.ascent * scale_factor)
|
|
152
|
+
@descender = Integer(@ttf.descent * scale_factor)
|
|
153
|
+
@line_gap = Integer(@ttf.line_gap * scale_factor)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Compute width of a string at the specified size, optionally with kerning
|
|
157
|
+
# applied.
|
|
158
|
+
#
|
|
159
|
+
# @param string [String] *must* be encoded as UTF-8
|
|
160
|
+
# @param options [Hash{Symbol => any}]
|
|
161
|
+
# @option options :size [Number]
|
|
162
|
+
# @option options :kerning [Boolean] (false)
|
|
163
|
+
# @return [Number]
|
|
164
|
+
def compute_width_of(string, options = {})
|
|
165
|
+
scale = (options[:size] || size) / 1000.0
|
|
166
|
+
if options[:kerning]
|
|
167
|
+
kern(string).reduce(0) { |s, r|
|
|
168
|
+
if r.is_a?(Numeric)
|
|
169
|
+
s - r
|
|
170
|
+
else
|
|
171
|
+
r.reduce(s) { |a, e| a + character_width_by_code(e) }
|
|
172
|
+
end
|
|
173
|
+
} * scale
|
|
174
|
+
else
|
|
175
|
+
string.codepoints.reduce(0) { |s, r|
|
|
176
|
+
s + character_width_by_code(r)
|
|
177
|
+
} * scale
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# The font bbox.
|
|
182
|
+
#
|
|
183
|
+
# @return [Array(Number, Number, Number, Number)]
|
|
184
|
+
def bbox
|
|
185
|
+
@bbox ||= @ttf.bbox.map { |i| Integer(i * scale_factor) }
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Does this font contain kerning data.
|
|
189
|
+
#
|
|
190
|
+
# @return [Boolean]
|
|
191
|
+
def has_kerning_data? # rubocop: disable Naming/PredicateName
|
|
192
|
+
@has_kerning_data
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Perform any changes to the string that need to happen before it is
|
|
196
|
+
# rendered to the canvas. Returns an array of subset "chunks", where the
|
|
197
|
+
# even-numbered indices are the font subset number, and the following
|
|
198
|
+
# entry element is either a string or an array (for kerned text).
|
|
199
|
+
#
|
|
200
|
+
# @param text [String] must be in UTF-8 encoding
|
|
201
|
+
# @param options [Hash{Symbol => any}]
|
|
202
|
+
# @option options :kerning [Boolean]
|
|
203
|
+
# @return [Array<Array(0, (String, Array)>]
|
|
204
|
+
def encode_text(text, options = {})
|
|
205
|
+
text = text.chomp
|
|
206
|
+
|
|
207
|
+
if options[:kerning]
|
|
208
|
+
last_subset = nil
|
|
209
|
+
kern(text).reduce([]) do |result, element|
|
|
210
|
+
if element.is_a?(Numeric)
|
|
211
|
+
unless result.last[1].is_a?(Array)
|
|
212
|
+
result.last[1] = [result.last[1]]
|
|
213
|
+
end
|
|
214
|
+
result.last[1] << element
|
|
215
|
+
result
|
|
216
|
+
else
|
|
217
|
+
encoded = @subsets.encode(element)
|
|
218
|
+
|
|
219
|
+
if encoded.first[0] == last_subset
|
|
220
|
+
result.last[1] << encoded.first[1]
|
|
221
|
+
encoded.shift
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
if encoded.any?
|
|
225
|
+
last_subset = encoded.last[0]
|
|
226
|
+
result + encoded
|
|
227
|
+
else
|
|
228
|
+
result
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
else
|
|
233
|
+
@subsets.encode(text.unpack('U*'))
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Base name of the font.
|
|
238
|
+
#
|
|
239
|
+
# @return [String]
|
|
240
|
+
def basename
|
|
241
|
+
@basename ||= @ttf.name.postscript_name
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# @devnote not sure how to compute this for true-type fonts...
|
|
245
|
+
#
|
|
246
|
+
# @private
|
|
247
|
+
# @return [Number]
|
|
248
|
+
def stem_v
|
|
249
|
+
0
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# @private
|
|
253
|
+
# @return [Number]
|
|
254
|
+
def italic_angle
|
|
255
|
+
return @italic_angle if @italic_angle
|
|
256
|
+
|
|
257
|
+
if @ttf.postscript.exists?
|
|
258
|
+
raw = @ttf.postscript.italic_angle
|
|
259
|
+
hi = raw >> 16
|
|
260
|
+
low = raw & 0xFF
|
|
261
|
+
hi = -((hi ^ 0xFFFF) + 1) if hi & 0x8000 != 0
|
|
262
|
+
@italic_angle = Float("#{hi}.#{low}")
|
|
263
|
+
else
|
|
264
|
+
@italic_angle = 0
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
@italic_angle
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# @private
|
|
271
|
+
# @return [Number]
|
|
272
|
+
def cap_height
|
|
273
|
+
@cap_height ||=
|
|
274
|
+
begin
|
|
275
|
+
height = (@ttf.os2.exists? && @ttf.os2.cap_height) || 0
|
|
276
|
+
height.zero? ? @ascender : height
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# @private
|
|
281
|
+
# @return [number]
|
|
282
|
+
def x_height
|
|
283
|
+
# FIXME: seems like if os2 table doesn't exist, we could
|
|
284
|
+
# just find the height of the lower-case 'x' glyph?
|
|
285
|
+
(@ttf.os2.exists? && @ttf.os2.x_height) || 0
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# @private
|
|
289
|
+
# @return [Number]
|
|
290
|
+
def family_class
|
|
291
|
+
@family_class ||= ((@ttf.os2.exists? && @ttf.os2.family_class) || 0) >> 8
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# @private
|
|
295
|
+
# @return [Boolean]
|
|
296
|
+
def serif?
|
|
297
|
+
@serif ||= [1, 2, 3, 4, 5, 7].include?(family_class)
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# @private
|
|
301
|
+
# @return [Boolean]
|
|
302
|
+
def script?
|
|
303
|
+
@script ||= family_class == 10
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# @private
|
|
307
|
+
# @return [Integer]
|
|
308
|
+
def pdf_flags
|
|
309
|
+
@pdf_flags ||=
|
|
310
|
+
begin
|
|
311
|
+
flags = 0
|
|
312
|
+
flags |= 0x0001 if @ttf.postscript.fixed_pitch?
|
|
313
|
+
flags |= 0x0002 if serif?
|
|
314
|
+
flags |= 0x0008 if script?
|
|
315
|
+
flags |= 0x0040 if italic_angle != 0
|
|
316
|
+
# Assume the font contains at least some non-latin characters
|
|
317
|
+
flags | 0x0004
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# Normlize text to a compatible encoding.
|
|
322
|
+
#
|
|
323
|
+
# @param text [String]
|
|
324
|
+
# @return [String]
|
|
325
|
+
def normalize_encoding(text)
|
|
326
|
+
text.encode(::Encoding::UTF_8)
|
|
327
|
+
rescue StandardError
|
|
328
|
+
raise Prawn::Errors::IncompatibleStringEncoding,
|
|
329
|
+
"Encoding #{text.encoding} can not be transparently converted to UTF-8. " \
|
|
330
|
+
'Please ensure the encoding of the string you are attempting to use is set correctly'
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# Encode text to UTF-8.
|
|
334
|
+
#
|
|
335
|
+
# @param text [String]
|
|
336
|
+
# @return [String]
|
|
337
|
+
def to_utf8(text)
|
|
338
|
+
text.encode('UTF-8')
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
# Does this font has a glyph for the character?
|
|
342
|
+
#
|
|
343
|
+
# @param char [String]
|
|
344
|
+
# @return [Boolean]
|
|
345
|
+
def glyph_present?(char)
|
|
346
|
+
code = char.codepoints.first
|
|
347
|
+
cmap[code].positive?
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
# Returns the number of characters in `str` (a UTF-8-encoded string).
|
|
351
|
+
#
|
|
352
|
+
# @param str [String]
|
|
353
|
+
# @return [Integer]
|
|
354
|
+
def character_count(str)
|
|
355
|
+
str.length
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
private
|
|
359
|
+
|
|
360
|
+
def cmap
|
|
361
|
+
(@cmap ||= @ttf.cmap.unicode.first) || raise(NoUnicodeCMap.new(font: name))
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
# +string+ must be UTF8-encoded.
|
|
365
|
+
#
|
|
366
|
+
# Returns an array. If an element is a numeric, it represents the
|
|
367
|
+
# kern amount to inject at that position. Otherwise, the element
|
|
368
|
+
# is an array of UTF-16 characters.
|
|
369
|
+
def kern(string)
|
|
370
|
+
a = []
|
|
371
|
+
|
|
372
|
+
string.each_codepoint do |r|
|
|
373
|
+
if a.empty?
|
|
374
|
+
a << [r]
|
|
375
|
+
elsif (kern = kern_pairs_table[[cmap[a.last.last], cmap[r]]])
|
|
376
|
+
kern *= scale_factor
|
|
377
|
+
a << -kern << [r]
|
|
378
|
+
else
|
|
379
|
+
a.last << r
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
a
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def kern_pairs_table
|
|
387
|
+
@kern_pairs_table ||=
|
|
388
|
+
if has_kerning_data?
|
|
389
|
+
@ttf.kerning.tables.first.pairs
|
|
390
|
+
else
|
|
391
|
+
{}
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
def hmtx
|
|
396
|
+
@hmtx ||= @ttf.horizontal_metrics
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
def character_width_by_code(code)
|
|
400
|
+
return 0 unless cmap[code]
|
|
401
|
+
|
|
402
|
+
# Some TTF fonts have nonzero widths for \n (UTF-8 / ASCII code: 10).
|
|
403
|
+
# Patch around this as we'll never be drawing a newline with a width.
|
|
404
|
+
return 0.0 if code == 10
|
|
405
|
+
|
|
406
|
+
@char_widths[code] ||= Integer(hmtx.widths[cmap[code]] * scale_factor)
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
def scale_factor
|
|
410
|
+
@scale_factor ||= 1000.0 / @ttf.header.units_per_em
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def register(subset)
|
|
414
|
+
temp_name = @ttf.name.postscript_name.delete("\0").to_sym
|
|
415
|
+
ref = @document.ref!(Type: :Font, BaseFont: temp_name)
|
|
416
|
+
|
|
417
|
+
# Embed the font metrics in the document after everything has been
|
|
418
|
+
# drawn, just before the document is emitted.
|
|
419
|
+
@document.renderer.before_render { |_doc| embed(ref, subset) }
|
|
420
|
+
|
|
421
|
+
ref
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
def embed(reference, subset)
|
|
425
|
+
if full_font_embedding
|
|
426
|
+
embed_full_font(reference)
|
|
427
|
+
else
|
|
428
|
+
embed_subset(reference, subset)
|
|
429
|
+
end
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
def embed_subset(reference, subset)
|
|
433
|
+
font = TTFunk::File.new(@subsets[subset].encode)
|
|
434
|
+
unicode_mapping = @subsets[subset].to_unicode_map
|
|
435
|
+
embed_simple_font(reference, font, unicode_mapping)
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
def embed_simple_font(reference, font, unicode_mapping)
|
|
439
|
+
if font_type(font) == :unknown
|
|
440
|
+
raise Error, %(Simple font embedding is not uspported for font "#{font.name}.")
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
true_type = font_type(font) == :true_type
|
|
444
|
+
open_type = font_type(font) == :open_type
|
|
445
|
+
|
|
446
|
+
# empirically, it looks like Adobe Reader will not display fonts
|
|
447
|
+
# if their font name is more than 33 bytes long. Strange. But true.
|
|
448
|
+
basename = font.name.postscript_name[0, 33].delete("\0")
|
|
449
|
+
|
|
450
|
+
raise NoPostscriptName.new(font: font) if basename.nil?
|
|
451
|
+
|
|
452
|
+
fontfile = @document.ref!({})
|
|
453
|
+
fontfile.data[:Length1] = font.contents.size
|
|
454
|
+
fontfile.stream << font.contents.string
|
|
455
|
+
fontfile.stream.compress! if @document.compression_enabled?
|
|
456
|
+
|
|
457
|
+
descriptor = @document.ref!(
|
|
458
|
+
Type: :FontDescriptor,
|
|
459
|
+
FontName: basename.to_sym,
|
|
460
|
+
FontBBox: bbox,
|
|
461
|
+
Flags: pdf_flags,
|
|
462
|
+
StemV: stem_v,
|
|
463
|
+
ItalicAngle: italic_angle,
|
|
464
|
+
Ascent: @ascender,
|
|
465
|
+
Descent: @descender,
|
|
466
|
+
CapHeight: cap_height,
|
|
467
|
+
XHeight: x_height,
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
first_char, last_char = unicode_mapping.keys.minmax
|
|
471
|
+
hmtx = font.horizontal_metrics
|
|
472
|
+
widths =
|
|
473
|
+
(first_char..last_char).map { |code|
|
|
474
|
+
if unicode_mapping.key?(code)
|
|
475
|
+
gid = font.cmap.tables.first.code_map[code]
|
|
476
|
+
Integer(hmtx.widths[gid] * scale_factor)
|
|
477
|
+
else
|
|
478
|
+
# These characters are not in the document so we don't ever use
|
|
479
|
+
# these values but we need to encode them so let's use as little
|
|
480
|
+
# sapce as possible.
|
|
481
|
+
0
|
|
482
|
+
end
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
# It would be nice to have Encoding set for the macroman subsets,
|
|
486
|
+
# and only do a ToUnicode cmap for non-encoded unicode subsets.
|
|
487
|
+
# However, apparently Adobe Reader won't render MacRoman encoded
|
|
488
|
+
# subsets if original font contains unicode characters. (It has to
|
|
489
|
+
# be some flag or something that ttfunk is simply copying over...
|
|
490
|
+
# but I can't figure out which flag that is.)
|
|
491
|
+
#
|
|
492
|
+
# For now, it's simplest to just create a unicode cmap for every font.
|
|
493
|
+
# It offends my inner purist, but it'll do.
|
|
494
|
+
|
|
495
|
+
to_unicode = @document.ref!({})
|
|
496
|
+
to_unicode << ToUnicodeCMap.new(unicode_mapping).generate
|
|
497
|
+
to_unicode.stream.compress! if @document.compression_enabled?
|
|
498
|
+
|
|
499
|
+
reference.data.update(
|
|
500
|
+
BaseFont: basename.to_sym,
|
|
501
|
+
FontDescriptor: descriptor,
|
|
502
|
+
FirstChar: first_char,
|
|
503
|
+
LastChar: last_char,
|
|
504
|
+
Widths: @document.ref!(widths),
|
|
505
|
+
ToUnicode: to_unicode,
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
if true_type
|
|
509
|
+
reference.data.update(Subtype: :TrueType)
|
|
510
|
+
descriptor.data.update(FontFile2: fontfile)
|
|
511
|
+
elsif open_type
|
|
512
|
+
@document.renderer.min_version(1.6)
|
|
513
|
+
reference.data.update(Subtype: :Type1)
|
|
514
|
+
descriptor.data.update(FontFile3: fontfile)
|
|
515
|
+
fontfile.data.update(Subtype: :OpenType)
|
|
516
|
+
end
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
def embed_full_font(reference)
|
|
520
|
+
embed_composite_font(reference, @ttf)
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
def embed_composite_font(reference, font)
|
|
524
|
+
if font_type(font) == :unknown
|
|
525
|
+
raise Error, %(Composite font embedding is not uspported for font "#{font.name}.")
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
true_type = font_type(font) == :true_type
|
|
529
|
+
open_type = font_type(font) == :open_type
|
|
530
|
+
|
|
531
|
+
fontfile = @document.ref!({})
|
|
532
|
+
fontfile.data[:Length1] = font.contents.size if true_type
|
|
533
|
+
fontfile.data[:Subtype] = :CIDFontType0C if open_type
|
|
534
|
+
fontfile.stream << font.contents.string
|
|
535
|
+
fontfile.stream.compress! if @document.compression_enabled?
|
|
536
|
+
|
|
537
|
+
# empirically, it looks like Adobe Reader will not display fonts
|
|
538
|
+
# if their font name is more than 33 bytes long. Strange. But true.
|
|
539
|
+
basename = font.name.postscript_name[0, 33].delete("\0")
|
|
540
|
+
|
|
541
|
+
descriptor = @document.ref!(
|
|
542
|
+
Type: :FontDescriptor,
|
|
543
|
+
FontName: basename.to_sym,
|
|
544
|
+
FontBBox: bbox,
|
|
545
|
+
Flags: pdf_flags,
|
|
546
|
+
StemV: stem_v,
|
|
547
|
+
ItalicAngle: italic_angle,
|
|
548
|
+
Ascent: @ascender,
|
|
549
|
+
Descent: @descender,
|
|
550
|
+
CapHeight: cap_height,
|
|
551
|
+
XHeight: x_height,
|
|
552
|
+
)
|
|
553
|
+
descriptor.data[:FontFile2] = fontfile if true_type
|
|
554
|
+
descriptor.data[:FontFile3] = fontfile if open_type
|
|
555
|
+
|
|
556
|
+
to_unicode = @document.ref!({})
|
|
557
|
+
to_unicode << ToUnicodeCMap.new(
|
|
558
|
+
font.cmap.unicode.first
|
|
559
|
+
.code_map
|
|
560
|
+
.reject { |cid, gid| gid.zero? || (0xd800..0xdfff).cover?(cid) }
|
|
561
|
+
.invert
|
|
562
|
+
.sort.to_h,
|
|
563
|
+
2, # Identity-H is a 2-byte encoding
|
|
564
|
+
).generate
|
|
565
|
+
to_unicode.stream.compress! if @document.compression_enabled?
|
|
566
|
+
|
|
567
|
+
widths =
|
|
568
|
+
font.horizontal_metrics.widths.map { |w| (w * scale_factor).round }
|
|
569
|
+
|
|
570
|
+
child_font = @document.ref!(
|
|
571
|
+
Type: :Font,
|
|
572
|
+
BaseFont: basename.to_sym,
|
|
573
|
+
CIDSystemInfo: {
|
|
574
|
+
Registry: 'Adobe',
|
|
575
|
+
Ordering: 'Identity',
|
|
576
|
+
Supplement: 0,
|
|
577
|
+
},
|
|
578
|
+
FontDescriptor: descriptor,
|
|
579
|
+
W: [0, widths],
|
|
580
|
+
)
|
|
581
|
+
if true_type
|
|
582
|
+
child_font.data.update(
|
|
583
|
+
Subtype: :CIDFontType2,
|
|
584
|
+
CIDToGIDMap: :Identity,
|
|
585
|
+
)
|
|
586
|
+
end
|
|
587
|
+
if open_type
|
|
588
|
+
child_font.data[:Subtype] = :CIDFontType0
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
reference.data.update(
|
|
592
|
+
Subtype: :Type0,
|
|
593
|
+
BaseFont: basename.to_sym,
|
|
594
|
+
Encoding: :'Identity-H',
|
|
595
|
+
DescendantFonts: [child_font],
|
|
596
|
+
ToUnicode: to_unicode,
|
|
597
|
+
)
|
|
598
|
+
end
|
|
599
|
+
|
|
600
|
+
def font_type(font)
|
|
601
|
+
if font.directory.tables.key?('glyf')
|
|
602
|
+
:true_type
|
|
603
|
+
elsif font.directory.tables.key?('CFF ')
|
|
604
|
+
:open_type
|
|
605
|
+
else
|
|
606
|
+
:unknown
|
|
607
|
+
end
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
def read_ttf_file
|
|
611
|
+
TTFunk::File.open(@name)
|
|
612
|
+
end
|
|
613
|
+
end
|
|
614
|
+
end
|
|
615
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Prawn
|
|
4
|
+
# Namespace for different fonts.
|
|
5
|
+
module Fonts
|
|
6
|
+
end
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
require_relative 'font'
|
|
10
|
+
require_relative 'fonts/afm'
|
|
11
|
+
require_relative 'fonts/ttf'
|
|
12
|
+
require_relative 'fonts/dfont'
|
|
13
|
+
require_relative 'fonts/otf'
|
|
14
|
+
require_relative 'fonts/ttc'
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Prawn
|
|
4
|
+
module Graphics
|
|
5
|
+
# The {Prawn::BlendMode} module is used to change the way two graphic
|
|
6
|
+
# objects are blended together.
|
|
7
|
+
module BlendMode
|
|
8
|
+
# @group Stable API
|
|
9
|
+
|
|
10
|
+
# Set blend mode. If a block is passed blend mode is restored afterwards.
|
|
11
|
+
#
|
|
12
|
+
# Passing an array of blend modes is allowed. PDF viewers should blend
|
|
13
|
+
# layers based on the first recognized blend mode.
|
|
14
|
+
#
|
|
15
|
+
# Valid blend modes since PDF 1.4 include `:Normal`, `:Multiply`, `:Screen`,
|
|
16
|
+
# `:Overlay`, `:Darken`, `:Lighten`, `:ColorDodge`, `:ColorBurn`,
|
|
17
|
+
# `:HardLight`, `:SoftLight`, `:Difference`, `:Exclusion`, `:Hue`,
|
|
18
|
+
# `:Saturation`, `:Color`, and `:Luminosity`.
|
|
19
|
+
#
|
|
20
|
+
# @example
|
|
21
|
+
# pdf.fill_color('0000ff')
|
|
22
|
+
# pdf.fill_rectangle([x, y + 25], 50, 50)
|
|
23
|
+
# pdf.blend_mode(:Multiply) do
|
|
24
|
+
# pdf.fill_color('ff0000')
|
|
25
|
+
# pdf.fill_circle([x, y], 25)
|
|
26
|
+
# end
|
|
27
|
+
#
|
|
28
|
+
# @param blend_mode [Symbol, Array<Symbol>]
|
|
29
|
+
# @yield
|
|
30
|
+
# @return [void]
|
|
31
|
+
def blend_mode(blend_mode = :Normal)
|
|
32
|
+
renderer.min_version(1.4)
|
|
33
|
+
|
|
34
|
+
save_graphics_state if block_given?
|
|
35
|
+
renderer.add_content("/#{blend_mode_dictionary_name(blend_mode)} gs")
|
|
36
|
+
if block_given?
|
|
37
|
+
yield
|
|
38
|
+
restore_graphics_state
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def blend_mode_dictionary_registry
|
|
45
|
+
@blend_mode_dictionary_registry ||= {}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def blend_mode_dictionary_name(blend_mode)
|
|
49
|
+
key = Array(blend_mode).join('')
|
|
50
|
+
dictionary_name = "BM#{key}"
|
|
51
|
+
|
|
52
|
+
dictionary = blend_mode_dictionary_registry[dictionary_name] ||= ref!(
|
|
53
|
+
Type: :ExtGState,
|
|
54
|
+
BM: blend_mode,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
page.ext_gstates[dictionary_name] = dictionary
|
|
58
|
+
dictionary_name
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|