gamba 2.0.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/CHANGELOG.md +80 -0
- data/COPYING +2 -0
- data/GPLv2 +339 -0
- data/GPLv3 +674 -0
- data/LICENSE +65 -0
- data/LICENSE-prawn-svg +21 -0
- data/README.md +148 -0
- data/data/fonts/Courier-Bold.afm +342 -0
- data/data/fonts/Courier-BoldOblique.afm +342 -0
- data/data/fonts/Courier-Oblique.afm +342 -0
- data/data/fonts/Courier.afm +342 -0
- data/data/fonts/Helvetica-Bold.afm +2827 -0
- data/data/fonts/Helvetica-BoldOblique.afm +2827 -0
- data/data/fonts/Helvetica-Oblique.afm +3051 -0
- data/data/fonts/Helvetica.afm +3051 -0
- data/data/fonts/MustRead.html +19 -0
- data/data/fonts/Symbol.afm +213 -0
- data/data/fonts/Times-Bold.afm +2588 -0
- data/data/fonts/Times-BoldItalic.afm +2384 -0
- data/data/fonts/Times-Italic.afm +2667 -0
- data/data/fonts/Times-Roman.afm +2419 -0
- data/data/fonts/ZapfDingbats.afm +225 -0
- data/lib/gamba/annotations.rb +21 -0
- data/lib/gamba/combine.rb +307 -0
- data/lib/gamba/document/bounding_box.rb +193 -0
- data/lib/gamba/document/column_box.rb +91 -0
- data/lib/gamba/document/internals.rb +264 -0
- data/lib/gamba/document/span.rb +25 -0
- data/lib/gamba/document.rb +268 -0
- data/lib/gamba/encoding.rb +13 -0
- data/lib/gamba/errors.rb +39 -0
- data/lib/gamba/font.rb +228 -0
- data/lib/gamba/fonts/afm.rb +171 -0
- data/lib/gamba/fonts/otf.rb +50 -0
- data/lib/gamba/fonts/ttf.rb +413 -0
- data/lib/gamba/graphic_state.rb +84 -0
- data/lib/gamba/graphics/color.rb +174 -0
- data/lib/gamba/graphics/patterns.rb +100 -0
- data/lib/gamba/graphics/style.rb +159 -0
- data/lib/gamba/graphics.rb +212 -0
- data/lib/gamba/grid.rb +216 -0
- data/lib/gamba/images/image.rb +271 -0
- data/lib/gamba/images/png_decoder.rb +65 -0
- data/lib/gamba/images.rb +115 -0
- data/lib/gamba/measurement_extensions.rb +40 -0
- data/lib/gamba/measurements.rb +62 -0
- data/lib/gamba/outline.rb +148 -0
- data/lib/gamba/page.rb +125 -0
- data/lib/gamba/page_geometry.rb +27 -0
- data/lib/gamba/pdf/decryption.rb +165 -0
- data/lib/gamba/pdf/outline_node.rb +53 -0
- data/lib/gamba/pdf/reader.rb +616 -0
- data/lib/gamba/pdf/reference.rb +31 -0
- data/lib/gamba/pdf/writer.rb +105 -0
- data/lib/gamba/pdf.rb +87 -0
- data/lib/gamba/pdf_a/srgb_profile.rb +61 -0
- data/lib/gamba/pdf_a.rb +132 -0
- data/lib/gamba/repeater.rb +79 -0
- data/lib/gamba/security.rb +111 -0
- data/lib/gamba/stamp.rb +76 -0
- data/lib/gamba/table/cell/kinds.rb +246 -0
- data/lib/gamba/table/cell.rb +331 -0
- data/lib/gamba/table/cells.rb +208 -0
- data/lib/gamba/table.rb +298 -0
- data/lib/gamba/text/box.rb +16 -0
- data/lib/gamba/text/formatted/arranger.rb +217 -0
- data/lib/gamba/text/formatted/box.rb +312 -0
- data/lib/gamba/text/formatted/fragment.rb +205 -0
- data/lib/gamba/text/formatted/line_wrap.rb +205 -0
- data/lib/gamba/text/formatted/parser.rb +150 -0
- data/lib/gamba/text/formatted/wrap.rb +103 -0
- data/lib/gamba/text.rb +310 -0
- data/lib/gamba/transformation_stack.rb +43 -0
- data/lib/gamba/true_type/cff_subset.rb +157 -0
- data/lib/gamba/true_type/file.rb +372 -0
- data/lib/gamba/true_type/open_type_layout.rb +189 -0
- data/lib/gamba/true_type/simple_subsets.rb +108 -0
- data/lib/gamba/true_type/subset.rb +206 -0
- data/lib/gamba/version.rb +10 -0
- data/lib/gamba/view.rb +28 -0
- data/lib/gamba.rb +124 -0
- data/lib/pdf/core.rb +4 -0
- data/lib/prawn/measurement_extensions.rb +4 -0
- data/lib/prawn/svg/attributes/clip_path.rb +46 -0
- data/lib/prawn/svg/attributes/mask.rb +16 -0
- data/lib/prawn/svg/attributes/opacity.rb +30 -0
- data/lib/prawn/svg/attributes/space.rb +10 -0
- data/lib/prawn/svg/attributes/stroke.rb +50 -0
- data/lib/prawn/svg/attributes/transform.rb +10 -0
- data/lib/prawn/svg/attributes.rb +6 -0
- data/lib/prawn/svg/calculators/arc_to_bezier_curve.rb +57 -0
- data/lib/prawn/svg/calculators/aspect_ratio.rb +60 -0
- data/lib/prawn/svg/calculators/document_sizing.rb +103 -0
- data/lib/prawn/svg/calculators/path_length.rb +168 -0
- data/lib/prawn/svg/calculators/pixels.rb +57 -0
- data/lib/prawn/svg/color.rb +240 -0
- data/lib/prawn/svg/css/font_face_parser.rb +72 -0
- data/lib/prawn/svg/css/font_family_parser.rb +39 -0
- data/lib/prawn/svg/css/font_parser.rb +46 -0
- data/lib/prawn/svg/css/selector_parser.rb +180 -0
- data/lib/prawn/svg/css/stylesheets.rb +233 -0
- data/lib/prawn/svg/css/values_parser.rb +68 -0
- data/lib/prawn/svg/document.rb +169 -0
- data/lib/prawn/svg/elements/anchor.rb +10 -0
- data/lib/prawn/svg/elements/base.rb +315 -0
- data/lib/prawn/svg/elements/bbox_scaling.rb +78 -0
- data/lib/prawn/svg/elements/call_duplicator.rb +37 -0
- data/lib/prawn/svg/elements/circle.rb +19 -0
- data/lib/prawn/svg/elements/clip_path.rb +26 -0
- data/lib/prawn/svg/elements/container.rb +10 -0
- data/lib/prawn/svg/elements/direct_render_base.rb +28 -0
- data/lib/prawn/svg/elements/ellipse.rb +20 -0
- data/lib/prawn/svg/elements/gradient.rb +183 -0
- data/lib/prawn/svg/elements/ignored.rb +5 -0
- data/lib/prawn/svg/elements/image.rb +178 -0
- data/lib/prawn/svg/elements/line.rb +27 -0
- data/lib/prawn/svg/elements/marker.rb +72 -0
- data/lib/prawn/svg/elements/mask.rb +65 -0
- data/lib/prawn/svg/elements/path.rb +294 -0
- data/lib/prawn/svg/elements/pattern.rb +150 -0
- data/lib/prawn/svg/elements/polygon.rb +25 -0
- data/lib/prawn/svg/elements/polyline.rb +23 -0
- data/lib/prawn/svg/elements/rect.rb +64 -0
- data/lib/prawn/svg/elements/root.rb +22 -0
- data/lib/prawn/svg/elements/switch.rb +92 -0
- data/lib/prawn/svg/elements/text.rb +99 -0
- data/lib/prawn/svg/elements/text_component.rb +352 -0
- data/lib/prawn/svg/elements/text_node.rb +308 -0
- data/lib/prawn/svg/elements/text_path.rb +153 -0
- data/lib/prawn/svg/elements/use.rb +142 -0
- data/lib/prawn/svg/elements/viewport.rb +29 -0
- data/lib/prawn/svg/elements.rb +46 -0
- data/lib/prawn/svg/extension.rb +25 -0
- data/lib/prawn/svg/font.rb +20 -0
- data/lib/prawn/svg/font_metrics.rb +144 -0
- data/lib/prawn/svg/font_registry.rb +315 -0
- data/lib/prawn/svg/funciri.rb +14 -0
- data/lib/prawn/svg/gradient_renderer.rb +332 -0
- data/lib/prawn/svg/gradients.rb +53 -0
- data/lib/prawn/svg/interface.rb +72 -0
- data/lib/prawn/svg/length.rb +43 -0
- data/lib/prawn/svg/link_renderer.rb +40 -0
- data/lib/prawn/svg/loaders/data.rb +17 -0
- data/lib/prawn/svg/loaders/file.rb +111 -0
- data/lib/prawn/svg/loaders/web.rb +24 -0
- data/lib/prawn/svg/paint.rb +67 -0
- data/lib/prawn/svg/pathable.rb +153 -0
- data/lib/prawn/svg/pattern_renderer.rb +112 -0
- data/lib/prawn/svg/pdfmatrix.rb +39 -0
- data/lib/prawn/svg/percentage.rb +24 -0
- data/lib/prawn/svg/properties.rb +329 -0
- data/lib/prawn/svg/renderer.rb +249 -0
- data/lib/prawn/svg/state.rb +24 -0
- data/lib/prawn/svg/transform_parser.rb +78 -0
- data/lib/prawn/svg/ttc.rb +35 -0
- data/lib/prawn/svg/ttf.rb +95 -0
- data/lib/prawn/svg/url_loader.rb +46 -0
- data/lib/prawn/svg/version.rb +5 -0
- data/lib/prawn/table.rb +4 -0
- data/lib/prawn-svg.rb +52 -0
- data/lib/prawn.rb +3 -0
- metadata +263 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gamba
|
|
4
|
+
module TrueType
|
|
5
|
+
# A new TrueType font holding only the glyphs a document uses, numbered in the order it first used them
|
|
6
|
+
# (glyph 0, .notdef, stays first). Composite glyphs bring their components along.
|
|
7
|
+
class Subset
|
|
8
|
+
# Tables ISO 32000-1 §9.9 requires in an embedded TrueType font, plus OS/2 and post for strict readers, and
|
|
9
|
+
# gasp, which tells rasterizers like Poppler at which sizes to hint and smooth glyphs.
|
|
10
|
+
COPIED_TABLES = %w[cvt\ fpgm prep OS/2 gasp].freeze
|
|
11
|
+
# Composite glyph flags (OpenType 'glyf' table).
|
|
12
|
+
ARGUMENTS_ARE_WORDS = 0x0001
|
|
13
|
+
HAS_SCALE = 0x0008
|
|
14
|
+
MORE_COMPONENTS = 0x0020
|
|
15
|
+
HAS_X_AND_Y_SCALE = 0x0040
|
|
16
|
+
HAS_TWO_BY_TWO = 0x0080
|
|
17
|
+
CHECKSUM_MAGIC = 0xB1B0AFBA
|
|
18
|
+
|
|
19
|
+
def initialize(file)
|
|
20
|
+
@file = file
|
|
21
|
+
@new_glyph_by_original = {0 => 0}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def glyph_for(original_glyph)
|
|
25
|
+
@new_glyph_by_original[original_glyph] ||= @new_glyph_by_original.size
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def original_glyphs
|
|
29
|
+
@new_glyph_by_original.keys
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Numbers the components of composite glyphs, level by level, in the order of their composites' indices.
|
|
33
|
+
def add_components(originals)
|
|
34
|
+
level = originals
|
|
35
|
+
loop do
|
|
36
|
+
components = level.flat_map { component_glyphs(@file.glyph_data(_1)) }
|
|
37
|
+
break if components.empty?
|
|
38
|
+
|
|
39
|
+
components.each { glyph_for(_1) }
|
|
40
|
+
level = components
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def bytes(extra_tables = {})
|
|
45
|
+
glyphs = glyph_data_including_components
|
|
46
|
+
offsets = [0]
|
|
47
|
+
glyf = glyphs.each_with_object(+"".b) do |data, table|
|
|
48
|
+
table << data << ("\x00" * ((4 - (data.bytesize % 4)) % 4))
|
|
49
|
+
offsets << table.bytesize
|
|
50
|
+
end
|
|
51
|
+
glyph_count = glyphs.size
|
|
52
|
+
widths = @file.advance_widths
|
|
53
|
+
left_side_bearings = original_glyphs.map { left_side_bearing(_1) }
|
|
54
|
+
|
|
55
|
+
bounds = glyphs.filter_map { |glyph| glyph.unpack("s>5") if glyph.bytesize >= 10 }
|
|
56
|
+
advances = original_glyphs.map { widths[_1] }
|
|
57
|
+
tables = {
|
|
58
|
+
"head" => head_table(bounds),
|
|
59
|
+
"hhea" => horizontal_header(bounds, advances, left_side_bearings, glyph_count),
|
|
60
|
+
"maxp" => maximum_profile(bounds, glyphs, glyph_count),
|
|
61
|
+
"hmtx" => horizontal_metrics(original_glyphs, widths, left_side_bearings),
|
|
62
|
+
"loca" => offsets.pack("N*"),
|
|
63
|
+
"glyf" => glyf,
|
|
64
|
+
"post" => post_table
|
|
65
|
+
}
|
|
66
|
+
COPIED_TABLES.each { |tag| tables[tag] = @file.table(tag) if @file.table?(tag) }
|
|
67
|
+
Subset.assemble(tables.merge(extra_tables))
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Version 3, no glyph names: the 32-byte header of the font's own post table, zeros where it has none or a
|
|
71
|
+
# shorter one, which the file accepts since the fields it reads stop at byte 16.
|
|
72
|
+
def post_table
|
|
73
|
+
post = (@file.table("post") || "".b).byteslice(0, 32).ljust(32, "\x00".b)
|
|
74
|
+
post[0, 4] = "\x00\x03\x00\x00".b
|
|
75
|
+
post
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# An sfnt file of these tables, with its directory and checksums (OpenType specification, "Font file").
|
|
79
|
+
def self.assemble(tables, sfnt_version: 0x00010000)
|
|
80
|
+
tags = tables.keys.sort
|
|
81
|
+
table_count = tags.size
|
|
82
|
+
search_range = (2**Math.log2(table_count).floor) * 16
|
|
83
|
+
directory = [sfnt_version, table_count, search_range, Math.log2(search_range / 16).to_i, (table_count * 16) - search_range].pack("Nn4")
|
|
84
|
+
body = +"".b
|
|
85
|
+
offset = 12 + (table_count * 16)
|
|
86
|
+
tags.each do |tag|
|
|
87
|
+
data = tables[tag]
|
|
88
|
+
directory << [tag, Subset.checksum(data), offset + body.bytesize, data.bytesize].pack("a4N3")
|
|
89
|
+
body << data << ("\x00" * ((4 - (data.bytesize % 4)) % 4))
|
|
90
|
+
end
|
|
91
|
+
font = directory << body
|
|
92
|
+
head_offset = 12 + (table_count * 16) + tags.take_while { _1 != "head" }.sum { |tag| (tables[tag].bytesize + 3) & ~3 }
|
|
93
|
+
font[head_offset + 8, 4] = [(CHECKSUM_MAGIC - Subset.checksum(font)) & 0xFFFFFFFF].pack("N")
|
|
94
|
+
font
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def self.checksum(data)
|
|
98
|
+
padded = data + ("\x00" * ((4 - (data.bytesize % 4)) % 4))
|
|
99
|
+
padded.unpack("N*").sum & 0xFFFFFFFF
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
private
|
|
103
|
+
|
|
104
|
+
def glyph_data_including_components
|
|
105
|
+
glyphs = []
|
|
106
|
+
index = 0
|
|
107
|
+
while index < original_glyphs.size
|
|
108
|
+
glyphs << remap_components(@file.glyph_data(original_glyphs[index]))
|
|
109
|
+
index += 1
|
|
110
|
+
end
|
|
111
|
+
glyphs
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def component_glyphs(data)
|
|
115
|
+
return [] if data.bytesize < 10 || data.unpack1("s>") >= 0
|
|
116
|
+
|
|
117
|
+
glyphs = []
|
|
118
|
+
position = 10
|
|
119
|
+
loop do
|
|
120
|
+
break if position + 4 > data.bytesize
|
|
121
|
+
|
|
122
|
+
flags, original = data.unpack("@#{position} n2")
|
|
123
|
+
glyphs << original if original < @file.glyph_count
|
|
124
|
+
position += 4 + ((flags & ARGUMENTS_ARE_WORDS).zero? ? 2 : 4)
|
|
125
|
+
position += 2 if (flags & HAS_SCALE).nonzero?
|
|
126
|
+
position += 4 if (flags & HAS_X_AND_Y_SCALE).nonzero?
|
|
127
|
+
position += 8 if (flags & HAS_TWO_BY_TWO).nonzero?
|
|
128
|
+
break if (flags & MORE_COMPONENTS).zero?
|
|
129
|
+
end
|
|
130
|
+
glyphs
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def remap_components(data)
|
|
134
|
+
return data if data.bytesize < 10 || data.unpack1("s>") >= 0
|
|
135
|
+
|
|
136
|
+
data = data.dup
|
|
137
|
+
position = 10
|
|
138
|
+
loop do
|
|
139
|
+
raise Error, "Truncated composite glyph" if position + 4 > data.bytesize
|
|
140
|
+
|
|
141
|
+
flags, original = data.unpack("@#{position} n2")
|
|
142
|
+
raise Error, "Composite glyph component #{original} beyond the font's #{@file.glyph_count} glyphs" if original >= @file.glyph_count
|
|
143
|
+
|
|
144
|
+
data[position + 2, 2] = [glyph_for(original)].pack("n")
|
|
145
|
+
position += 4 + ((flags & ARGUMENTS_ARE_WORDS).zero? ? 2 : 4)
|
|
146
|
+
position += 2 if (flags & HAS_SCALE).nonzero?
|
|
147
|
+
position += 4 if (flags & HAS_X_AND_Y_SCALE).nonzero?
|
|
148
|
+
position += 8 if (flags & HAS_TWO_BY_TWO).nonzero?
|
|
149
|
+
break if (flags & MORE_COMPONENTS).zero?
|
|
150
|
+
end
|
|
151
|
+
data
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Each glyph's advance width then left side bearing, packed: one array, no pairs to flatten.
|
|
155
|
+
def horizontal_metrics(original_glyphs, widths, left_side_bearings)
|
|
156
|
+
metrics = []
|
|
157
|
+
original_glyphs.each_with_index { |glyph, index| metrics.push(widths[glyph], left_side_bearings[index]) }
|
|
158
|
+
metrics.pack("n*")
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def left_side_bearing(glyph)
|
|
162
|
+
metric_count = @file.table("hhea").unpack1("@34 n")
|
|
163
|
+
hmtx = @file.table("hmtx")
|
|
164
|
+
raw = (glyph < metric_count) ? hmtx.unpack1("@#{(glyph * 4) + 2} n") : hmtx.unpack1("@#{(metric_count * 4) + ((glyph - metric_count) * 2)} n")
|
|
165
|
+
raw.to_i
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# The subset's own extremes, as the OpenType specification defines them: readers like Poppler size their
|
|
169
|
+
# glyph caches from the font's bounding box.
|
|
170
|
+
def head_table(bounds)
|
|
171
|
+
head = @file.table("head").dup
|
|
172
|
+
head[8, 4] = "\x00\x00\x00\x00".b
|
|
173
|
+
head[36, 8] = [bounds.map { _1[1] }.min || 0, bounds.map { _1[2] }.min || 0, bounds.map { _1[3] }.max || 0, bounds.map { _1[4] }.max || 0].pack("s>4")
|
|
174
|
+
head[50, 2] = [1].pack("n")
|
|
175
|
+
head
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def horizontal_header(bounds, advances, left_side_bearings, glyph_count)
|
|
179
|
+
hhea = @file.table("hhea").dup
|
|
180
|
+
right_side_bearings = bounds.zip(advances).filter_map { |bound, advance| advance - (bound[3] - bound[1]) - bound[1] if bound && advance }
|
|
181
|
+
extents = bounds.map { _1[3] }
|
|
182
|
+
hhea[10, 8] = [advances.compact.max || 0, left_side_bearings.min || 0, right_side_bearings.min || 0, extents.max || 0].pack("n s>3")
|
|
183
|
+
hhea[34, 2] = [glyph_count].pack("n")
|
|
184
|
+
hhea
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Largest simple glyph, in points and contours; composite figures stay those of the whole font.
|
|
188
|
+
def maximum_profile(bounds, glyphs, glyph_count)
|
|
189
|
+
maxp = @file.table("maxp").dup
|
|
190
|
+
maxp[4, 2] = [glyph_count].pack("n")
|
|
191
|
+
return maxp if maxp.bytesize < 10
|
|
192
|
+
|
|
193
|
+
simple = glyphs.select { |glyph| glyph.bytesize >= 10 && glyph.unpack1("s>") >= 0 }
|
|
194
|
+
contours = simple.map { _1.unpack1("s>") }
|
|
195
|
+
points = simple.zip(contours).map! do |glyph, count|
|
|
196
|
+
next 0 if count.zero?
|
|
197
|
+
raise Error, "Truncated simple glyph" if glyph.bytesize < 10 + (count * 2)
|
|
198
|
+
|
|
199
|
+
glyph.unpack1("@#{10 + ((count - 1) * 2)} n") + 1
|
|
200
|
+
end
|
|
201
|
+
maxp[6, 4] = [points.max || 0, contours.max || 0].pack("n2")
|
|
202
|
+
maxp
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
data/lib/gamba/view.rb
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Prawn
|
|
4
|
+
# Mixed into a class, turns it into a document: Prawn's methods are called on an implicit Prawn::Document.
|
|
5
|
+
module View
|
|
6
|
+
def document
|
|
7
|
+
@document ||= Document.new
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def method_missing(method_name, ...)
|
|
11
|
+
return super unless document.respond_to?(method_name)
|
|
12
|
+
|
|
13
|
+
document.public_send(method_name, ...)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def respond_to_missing?(method_name, include_all = false)
|
|
17
|
+
document.respond_to?(method_name) || super
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def update(&)
|
|
21
|
+
instance_eval(&)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def save_as(filename)
|
|
25
|
+
document.render_file(filename)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
data/lib/gamba.rb
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
if Gem.loaded_specs.key?("prawn")
|
|
4
|
+
raise LoadError, "Gamba replaces the prawn gem, which is in your bundle too: remove prawn (and prawn-table, " \
|
|
5
|
+
"prawn-svg) from your Gemfile"
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
require_relative "gamba/version"
|
|
9
|
+
|
|
10
|
+
module Prawn
|
|
11
|
+
BASEDIR = File.expand_path("..", __dir__)
|
|
12
|
+
DATADIR = File.join(BASEDIR, "data")
|
|
13
|
+
FLOAT_PRECISION = 1.0e-9
|
|
14
|
+
|
|
15
|
+
class << self
|
|
16
|
+
attr_accessor :debug
|
|
17
|
+
|
|
18
|
+
def verify_options(accepted, actual)
|
|
19
|
+
return unless debug || $DEBUG
|
|
20
|
+
|
|
21
|
+
unknown = actual.keys - Array(accepted)
|
|
22
|
+
raise Errors::UnknownOption, "\nDetected unknown option(s): #{unknown.inspect}\nAccepted options are: #{Array(accepted).inspect}" unless unknown.empty?
|
|
23
|
+
|
|
24
|
+
yield if block_given?
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
require_relative "gamba/errors"
|
|
30
|
+
require_relative "gamba/pdf"
|
|
31
|
+
require_relative "gamba/pdf/reference"
|
|
32
|
+
require_relative "gamba/pdf/writer"
|
|
33
|
+
require_relative "gamba/pdf/outline_node"
|
|
34
|
+
require_relative "gamba/page_geometry"
|
|
35
|
+
require_relative "gamba/encoding"
|
|
36
|
+
require_relative "gamba/graphic_state"
|
|
37
|
+
require_relative "gamba/page"
|
|
38
|
+
require_relative "gamba/transformation_stack"
|
|
39
|
+
require_relative "gamba/graphics/color"
|
|
40
|
+
require_relative "gamba/graphics/style"
|
|
41
|
+
require_relative "gamba/graphics/patterns"
|
|
42
|
+
require_relative "gamba/graphics"
|
|
43
|
+
require_relative "gamba/text"
|
|
44
|
+
require_relative "gamba/text/formatted/parser"
|
|
45
|
+
require_relative "gamba/text/formatted/fragment"
|
|
46
|
+
require_relative "gamba/text/formatted/arranger"
|
|
47
|
+
require_relative "gamba/text/formatted/line_wrap"
|
|
48
|
+
require_relative "gamba/text/formatted/wrap"
|
|
49
|
+
require_relative "gamba/text/formatted/box"
|
|
50
|
+
require_relative "gamba/text/box"
|
|
51
|
+
require_relative "gamba/annotations"
|
|
52
|
+
require_relative "gamba/stamp"
|
|
53
|
+
require_relative "gamba/measurements"
|
|
54
|
+
require_relative "gamba/view"
|
|
55
|
+
require_relative "gamba/images"
|
|
56
|
+
require_relative "gamba/images/png_decoder"
|
|
57
|
+
require_relative "gamba/images/image"
|
|
58
|
+
require_relative "gamba/repeater"
|
|
59
|
+
require_relative "gamba/grid"
|
|
60
|
+
require_relative "gamba/outline"
|
|
61
|
+
require_relative "gamba/table"
|
|
62
|
+
require_relative "gamba/table/cells"
|
|
63
|
+
require_relative "gamba/table/cell"
|
|
64
|
+
require_relative "gamba/table/cell/kinds"
|
|
65
|
+
require_relative "gamba/security"
|
|
66
|
+
require_relative "gamba/pdf_a"
|
|
67
|
+
require_relative "gamba/pdf_a/srgb_profile"
|
|
68
|
+
require_relative "gamba/document/internals"
|
|
69
|
+
require_relative "gamba/document/bounding_box"
|
|
70
|
+
require_relative "gamba/document/span"
|
|
71
|
+
require_relative "gamba/document/column_box"
|
|
72
|
+
require_relative "gamba/document"
|
|
73
|
+
require_relative "gamba/font"
|
|
74
|
+
require_relative "gamba/fonts/afm"
|
|
75
|
+
|
|
76
|
+
# TrueType fonts load on first use: a document of standard fonts never needs them.
|
|
77
|
+
module Gamba
|
|
78
|
+
module TrueType
|
|
79
|
+
autoload :Error, ::File.expand_path("gamba/true_type/file", __dir__)
|
|
80
|
+
autoload :File, ::File.expand_path("gamba/true_type/file", __dir__)
|
|
81
|
+
autoload :Subset, ::File.expand_path("gamba/true_type/subset", __dir__)
|
|
82
|
+
autoload :SimpleSubsets, ::File.expand_path("gamba/true_type/simple_subsets", __dir__)
|
|
83
|
+
autoload :CffSubset, ::File.expand_path("gamba/true_type/cff_subset", __dir__)
|
|
84
|
+
autoload :OpenTypeLayout, ::File.expand_path("gamba/true_type/open_type_layout", __dir__)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
module Prawn
|
|
89
|
+
module Fonts
|
|
90
|
+
%i[TTF TTC DFont].each { autoload _1, ::File.expand_path("gamba/fonts/ttf", __dir__) }
|
|
91
|
+
autoload :OTF, ::File.expand_path("gamba/fonts/otf", __dir__)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
class Font
|
|
95
|
+
%i[TTF TTC DFont].each { autoload _1, ::File.expand_path("gamba/fonts/ttf", __dir__) }
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
module Gamba
|
|
100
|
+
autoload :Combine, ::File.expand_path("gamba/combine", __dir__)
|
|
101
|
+
|
|
102
|
+
# The pages of other PDF files, one after the other in a new one:
|
|
103
|
+
# Gamba.combine([label, label], page_size: [288, 432]). See Gamba::Combine#initialize for the options.
|
|
104
|
+
def self.combine(sources, page_size: nil, orientation: nil)
|
|
105
|
+
Combine.new(page_size:, orientation:).call(sources)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Loads what Gamba otherwise loads on first use, SVG support included: call it before a server forks its
|
|
109
|
+
# workers, so they share that memory instead of each loading its own copy.
|
|
110
|
+
def self.eager_load!
|
|
111
|
+
[TrueType::File, TrueType::Subset, Prawn::Fonts::TTF, Combine, PDF::Core::Reader]
|
|
112
|
+
require "prawn-svg"
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
module PDF
|
|
117
|
+
module Core
|
|
118
|
+
autoload :Reader, ::File.expand_path("gamba/pdf/reader", __dir__)
|
|
119
|
+
autoload :Decryption, ::File.expand_path("gamba/pdf/decryption", __dir__)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
Prawn.image_handler.register(Prawn::Images::JPG)
|
|
124
|
+
Prawn.image_handler.register(Prawn::Images::PNG)
|
data/lib/pdf/core.rb
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module Prawn::SVG::Attributes::ClipPath
|
|
2
|
+
def parse_clip_path_attribute_and_call
|
|
3
|
+
return unless (clip_path = properties.clip_path)
|
|
4
|
+
return if clip_path == "none"
|
|
5
|
+
|
|
6
|
+
clip_path_element = extract_element_from_url_id_reference(clip_path, "clipPath")
|
|
7
|
+
|
|
8
|
+
if clip_path_element.nil?
|
|
9
|
+
document.warnings << "Could not resolve clip-path URI to a clipPath element"
|
|
10
|
+
else
|
|
11
|
+
clip_calls = clip_path_element.build_clip_calls(self)
|
|
12
|
+
return if clip_calls.nil?
|
|
13
|
+
|
|
14
|
+
add_call_and_enter "save_graphics_state"
|
|
15
|
+
|
|
16
|
+
if clip_path_contains_text?(clip_calls)
|
|
17
|
+
# Text in clip paths is implemented using a PDF soft mask. The text
|
|
18
|
+
# renders in white fill inside the mask to define the visible region.
|
|
19
|
+
# This correctly unions multiple text elements, unlike PDF text
|
|
20
|
+
# rendering mode 7 which intersects separate text objects.
|
|
21
|
+
@calls << ["soft_mask", [], {}, clip_calls]
|
|
22
|
+
else
|
|
23
|
+
@calls.concat clip_calls
|
|
24
|
+
|
|
25
|
+
# SVG's clip-rule applies per-element (determining each shape's interior),
|
|
26
|
+
# then elements are unioned. PDF's W* applies even-odd to the entire combined
|
|
27
|
+
# path, which incorrectly creates holes between separate shapes. Only use W*
|
|
28
|
+
# when there's a single child element, where self-intersection matters.
|
|
29
|
+
child_elements = clip_path_element.svg_child_elements
|
|
30
|
+
if child_elements.length == 1 && clip_path_element.computed_properties.clip_rule == "evenodd"
|
|
31
|
+
add_call "clip", clip_rule: :even_odd
|
|
32
|
+
else
|
|
33
|
+
add_call "clip"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def clip_path_contains_text?(calls)
|
|
42
|
+
calls.any? do |call, _arguments, _kwarguments, children|
|
|
43
|
+
call == "svg:render" || clip_path_contains_text?(children)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module Prawn::SVG::Attributes::Mask
|
|
2
|
+
def parse_mask_attribute_and_call
|
|
3
|
+
return unless (mask = properties.mask)
|
|
4
|
+
return if mask == "none"
|
|
5
|
+
|
|
6
|
+
mask_element = extract_element_from_url_id_reference(mask, "mask")
|
|
7
|
+
|
|
8
|
+
if mask_element.nil?
|
|
9
|
+
document.warnings << "Could not resolve mask URI to a mask element"
|
|
10
|
+
else
|
|
11
|
+
add_call_and_enter "save_graphics_state"
|
|
12
|
+
mask_calls = mask_element.build_mask_calls(self)
|
|
13
|
+
@calls << ["soft_mask", [], {}, mask_calls]
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module Prawn::SVG::Attributes::Opacity
|
|
2
|
+
def parse_opacity_attributes_and_call
|
|
3
|
+
# The opacity property is not inherited, but the opacity applies to all children underneath the element.
|
|
4
|
+
#
|
|
5
|
+
# Having an opacity property on a parent, and again on a child, multiplies the parent and child's opacities
|
|
6
|
+
# when drawing the children.
|
|
7
|
+
#
|
|
8
|
+
# The fill-opacity and stroke-opacity properties are inherited, but children which have a different value
|
|
9
|
+
# are displayed at that opacity rather than multiplying the parent's fill/stroke opacity with the child's.
|
|
10
|
+
#
|
|
11
|
+
# opacity and fill/stroke opacity can both be applied to the same element, and they multiply together.
|
|
12
|
+
|
|
13
|
+
opacity = computed_properties.opacity&.to_f&.clamp(0, 1)
|
|
14
|
+
fill_opacity = computed_properties.fill_opacity.to_f.clamp(0, 1)
|
|
15
|
+
stroke_opacity = computed_properties.stroke_opacity.to_f.clamp(0, 1)
|
|
16
|
+
|
|
17
|
+
state.opacity *= opacity if opacity
|
|
18
|
+
|
|
19
|
+
fill_opacity = (fill_opacity || 1) * state.opacity
|
|
20
|
+
stroke_opacity = (stroke_opacity || 1) * state.opacity
|
|
21
|
+
|
|
22
|
+
fill_opacity = stroke_opacity = 0 if computed_properties.visibility != "visible"
|
|
23
|
+
|
|
24
|
+
if state.last_fill_opacity != fill_opacity || state.last_stroke_opacity != stroke_opacity
|
|
25
|
+
state.last_fill_opacity = fill_opacity
|
|
26
|
+
state.last_stroke_opacity = stroke_opacity
|
|
27
|
+
add_call_and_enter "transparent", fill_opacity, stroke_opacity
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
module Prawn::SVG::Attributes::Stroke
|
|
2
|
+
CAP_STYLE_TRANSLATIONS = {"butt" => :butt, "round" => :round, "square" => :projecting_square}.freeze
|
|
3
|
+
JOIN_STYLE_TRANSLATIONS = {"miter" => :miter, "round" => :round, "bevel" => :bevel}.freeze
|
|
4
|
+
|
|
5
|
+
def parse_stroke_attributes_and_call
|
|
6
|
+
if (width_string = properties.stroke_width)
|
|
7
|
+
width = pixels(width_string)
|
|
8
|
+
state.stroke_width = width
|
|
9
|
+
add_call("line_width", width)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
if (linecap = properties.stroke_linecap) && linecap != "inherit"
|
|
13
|
+
add_call("cap_style", CAP_STYLE_TRANSLATIONS.fetch(linecap, :butt))
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
if (linejoin = properties.stroke_linejoin) && linejoin != "inherit"
|
|
17
|
+
add_call("join_style", JOIN_STYLE_TRANSLATIONS.fetch(linejoin, :miter))
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
if (miterlimit = properties.stroke_miterlimit) && miterlimit != "inherit"
|
|
21
|
+
add_call("miter_limit", miterlimit)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
if (dasharray = properties.stroke_dasharray)
|
|
25
|
+
case dasharray
|
|
26
|
+
when "inherit"
|
|
27
|
+
# don't do anything
|
|
28
|
+
when "none"
|
|
29
|
+
add_call("undash")
|
|
30
|
+
when Array
|
|
31
|
+
dasharray *= 2 if dasharray.length.odd?
|
|
32
|
+
values = dasharray.map { |value| pixels(value) }
|
|
33
|
+
|
|
34
|
+
if values.sum.zero?
|
|
35
|
+
add_call("undash")
|
|
36
|
+
else
|
|
37
|
+
options = {}
|
|
38
|
+
offset = computed_properties.stroke_dashoffset
|
|
39
|
+
if offset && offset != 0 && offset != "inherit"
|
|
40
|
+
phase = pixels(offset)
|
|
41
|
+
options[:phase] = phase unless phase.zero?
|
|
42
|
+
end
|
|
43
|
+
add_call("dash", values, **options)
|
|
44
|
+
end
|
|
45
|
+
else
|
|
46
|
+
raise "Unknown dasharray value: #{dasharray.inspect}"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
module Prawn::SVG::Attributes::Transform
|
|
2
|
+
def parse_transform_attribute_and_call
|
|
3
|
+
# Some elements do not support transforms
|
|
4
|
+
return unless transformable?
|
|
5
|
+
return unless (transform = attributes["transform"])
|
|
6
|
+
|
|
7
|
+
matrix = matrix_for_pdf(parse_transform_attribute(transform))
|
|
8
|
+
add_call_and_enter "transformation_matrix", *matrix unless matrix == [1, 0, 0, 1, 0, 0]
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
module Prawn::SVG::Calculators
|
|
2
|
+
module ArcToBezierCurve
|
|
3
|
+
protected
|
|
4
|
+
|
|
5
|
+
# Convert the elliptical arc to a cubic bézier curve using this algorithm:
|
|
6
|
+
# http://www.spaceroots.org/documents/ellipse/elliptical-arc.pdf
|
|
7
|
+
#
|
|
8
|
+
# eta_1 and eta_2 are the eccentric anomaly angles (the parametric angles on the
|
|
9
|
+
# unit circle that map to points on the ellipse). theta is the ellipse rotation angle.
|
|
10
|
+
def calculate_bezier_curve_points_for_arc(cx, cy, a, b, eta_1, eta_2, theta)
|
|
11
|
+
e = lambda do |eta|
|
|
12
|
+
[
|
|
13
|
+
cx + (a * Math.cos(theta) * Math.cos(eta)) - (b * Math.sin(theta) * Math.sin(eta)),
|
|
14
|
+
cy + (a * Math.sin(theta) * Math.cos(eta)) + (b * Math.cos(theta) * Math.sin(eta))
|
|
15
|
+
]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
ep = lambda do |eta|
|
|
19
|
+
[
|
|
20
|
+
(-a * Math.cos(theta) * Math.sin(eta)) - (b * Math.sin(theta) * Math.cos(eta)),
|
|
21
|
+
(-a * Math.sin(theta) * Math.sin(eta)) + (b * Math.cos(theta) * Math.cos(eta))
|
|
22
|
+
]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
iterations = 1
|
|
26
|
+
d_eta = eta_2 - eta_1
|
|
27
|
+
|
|
28
|
+
while iterations < 1024
|
|
29
|
+
break if d_eta.abs <= Math::PI / 2.0
|
|
30
|
+
|
|
31
|
+
iterations *= 2
|
|
32
|
+
d_eta = (eta_2 - eta_1) / iterations
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
(0...iterations).collect do |iteration|
|
|
36
|
+
seg_eta_a = eta_1 + (iteration * d_eta)
|
|
37
|
+
seg_eta_b = eta_1 + ((iteration + 1) * d_eta)
|
|
38
|
+
seg_d_eta = seg_eta_b - seg_eta_a
|
|
39
|
+
|
|
40
|
+
alpha = Math.sin(seg_d_eta) * ((Math.sqrt(4 + (3 * (Math.tan(seg_d_eta / 2)**2))) - 1) / 3)
|
|
41
|
+
|
|
42
|
+
x1, y1 = e[seg_eta_a]
|
|
43
|
+
x2, y2 = e[seg_eta_b]
|
|
44
|
+
|
|
45
|
+
ep_eta1_x, ep_eta1_y = ep[seg_eta_a]
|
|
46
|
+
q1_x = x1 + (alpha * ep_eta1_x)
|
|
47
|
+
q1_y = y1 + (alpha * ep_eta1_y)
|
|
48
|
+
|
|
49
|
+
ep_eta2_x, ep_eta2_y = ep[seg_eta_b]
|
|
50
|
+
q2_x = x2 - (alpha * ep_eta2_x)
|
|
51
|
+
q2_y = y2 - (alpha * ep_eta2_y)
|
|
52
|
+
|
|
53
|
+
{p2: [x2, y2], q1: [q1_x, q1_y], q2: [q2_x, q2_y]}
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
module Prawn::SVG::Calculators
|
|
2
|
+
class AspectRatio
|
|
3
|
+
attr_reader :align, :defer
|
|
4
|
+
attr_reader :width, :height, :x, :y
|
|
5
|
+
|
|
6
|
+
def initialize(value, container_dimensions, object_dimensions)
|
|
7
|
+
values = (value || "xMidYMid meet").split
|
|
8
|
+
@x = @y = 0
|
|
9
|
+
|
|
10
|
+
if values.first == "defer"
|
|
11
|
+
@defer = true
|
|
12
|
+
values.shift
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
@align, @meet_or_slice = values
|
|
16
|
+
|
|
17
|
+
w_container, h_container = container_dimensions
|
|
18
|
+
w_object, h_object = object_dimensions
|
|
19
|
+
|
|
20
|
+
container_ratio = w_container / h_container.to_f
|
|
21
|
+
object_ratio = w_object / h_object.to_f
|
|
22
|
+
|
|
23
|
+
if @align == "none"
|
|
24
|
+
@width, @height = container_dimensions
|
|
25
|
+
else
|
|
26
|
+
matches = @align.to_s.strip.match(/\Ax(Min|Mid|Max)Y(Min|Mid|Max)\z/i) || [nil, "Mid", "Mid"]
|
|
27
|
+
|
|
28
|
+
if (container_ratio > object_ratio) == slice?
|
|
29
|
+
@width = w_container
|
|
30
|
+
@height = w_container / object_ratio
|
|
31
|
+
@y = case matches[2].downcase
|
|
32
|
+
when "min" then 0
|
|
33
|
+
when "mid" then (h_container - (w_container / object_ratio)) / 2
|
|
34
|
+
when "max" then h_container - (w_container / object_ratio)
|
|
35
|
+
end
|
|
36
|
+
else
|
|
37
|
+
@width = h_container * object_ratio
|
|
38
|
+
@height = h_container
|
|
39
|
+
@x = case matches[1].downcase
|
|
40
|
+
when "min" then 0
|
|
41
|
+
when "mid" then (w_container - (h_container * object_ratio)) / 2
|
|
42
|
+
when "max" then w_container - (h_container * object_ratio)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def slice?
|
|
49
|
+
@meet_or_slice == "slice"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def meet?
|
|
53
|
+
@meet_or_slice != "slice"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def inspect
|
|
57
|
+
"[AspectRatio: #{@width},#{@height} offset #{@x},#{@y}]"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|