bandoola 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 399aa72c7e648f1d22ac8e4c8a3dabd5bec6cb1cbdab69f198c07c678ead2e7e
4
+ data.tar.gz: c2911c8a52cc62d55df0e5ab93c41ac45bcb13647b35ea805640454b37b029d5
5
+ SHA512:
6
+ metadata.gz: 9a3601d9583eb626212aea6b50a929c810cf38a40d47072e02b323455227bb0e4ba83dab3d1fcf49252d663563d2990527fce2b8d8a443e9203d4588deadda16
7
+ data.tar.gz: 3da607cdeef5663505cc48f02b1849750da611369c495b5601f3d87d40b3ef460207367b7427cad9c14b6365b903215b43dcaf06966f6452494faa2ffdac2faa
data/.tool-versions ADDED
@@ -0,0 +1,2 @@
1
+ ruby 4.0.2
2
+ nodejs 24.14.0
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2026-06-16
4
+
5
+ - Initial release
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Johan Halse
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,146 @@
1
+ # Bandoola
2
+
3
+ Build PDF documents from plain Ruby, styling elements with a Tailwind-inspired
4
+ class vocabulary. No runtime dependencies.
5
+
6
+ ```ruby
7
+ class Invoice < Bandoola::View
8
+ def view_template
9
+ div(class: "p-8 bg-slate-100 rounded-lg") do
10
+ text(class: "text-3xl font-bold") { "Bandoola" }
11
+ text(class: "text-base text-slate-600 mt-2") { "PDFs, the Tailwind way." }
12
+ end
13
+ end
14
+ end
15
+
16
+ invoice = Invoice.new
17
+ invoice.render # build the PDF (also returns the bytes)
18
+ invoice.write("out.pdf")
19
+ ```
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ bundle add bandoola
25
+ ```
26
+
27
+ ## Elements
28
+
29
+ Inside `view_template` you build a tree of elements. Each takes a `class:` (a
30
+ space-separated string or an array of tokens) and, where it makes sense, a block.
31
+
32
+ - `div(class:) { … }` — a block-level box that lays out the elements inside it.
33
+ - `text(class:) { "…" }` — a run of text; the block returns the string. It wraps
34
+ to its box width, and embedded newlines start new lines.
35
+ - `img(src:, class:)` — an image; `src` is a path to an SVG or JPEG.
36
+
37
+ Elements flow like HTML blocks: each fills its container's width and stacks below
38
+ the previous one, unless a width class makes it share a row (and reflow).
39
+
40
+ ## Styling
41
+
42
+ The class names follow [Tailwind](https://tailwindcss.com/docs); see their docs
43
+ for the full scales. What Bandoola understands today:
44
+
45
+ | Concern | Classes |
46
+ | --- | --- |
47
+ | Padding / margin | `p-4`, `px-2`, `pt-1`, `m-4`, `mx-2`, … |
48
+ | Width / height | `w-32`, `w-1/2`, `w-full`, `h-16`, `h-1/2`, `h-full` |
49
+ | Border width | `border-1` … `border-16` |
50
+ | Border radius | `rounded`, `rounded-sm` … `rounded-3xl`, `rounded-full` |
51
+ | Background | `bg-cyan-100`, `bg-slate-800`, `bg-black`, `bg-white` |
52
+ | Border color | `border-blue-600` (needs a border width to show) |
53
+ | Text color | `text-rose-500`, `text-white` |
54
+ | Font family | `font-sans`, `font-serif`, `font-mono`, `font-<registered>` |
55
+ | Font weight | `font-light` … `font-black` |
56
+ | Text size | `text-xs` … `text-9xl` |
57
+
58
+ Sizes and spacing are points (1px ≈ 1pt). Notes:
59
+
60
+ - `div` has no fill or border by default — add `bg-*`, `border-*`, or `rounded-*`.
61
+ - Colors work on the full Tailwind palette plus `black`/`white`; a color with
62
+ no shade (`bg-cyan`) uses 500.
63
+ - A `div`'s `font-*` family and weight are inherited by the text inside it; text
64
+ size is set per `text` element.
65
+
66
+ ## Fonts
67
+
68
+ `font-sans`/`serif`/`mono` map to the standard PDF fonts (Helvetica, Times,
69
+ Courier), which cover Western European text. Register a TrueType font for
70
+ anything else:
71
+
72
+ ```ruby
73
+ class Doc < Bandoola::View
74
+ register_font :gowun, "fonts/GowunDodum-Regular.ttf"
75
+ # or per weight: register_font :gowun, regular: "…-Regular.ttf", bold: "…-Bold.ttf"
76
+
77
+ def view_template
78
+ text(class: "font-gowun") { "Räksmörgås · 안녕하세요" }
79
+ end
80
+ end
81
+ ```
82
+
83
+ Embedded fonts render any Unicode they have glyphs for (Identity-H encoding) and
84
+ are subset to just the glyphs you use — a few KB rather than the whole file. Opt
85
+ out with `compact_fonts false` if you'd rather keep the original glyph ids. You
86
+ can register several fonts in one document and use whichever fits each script.
87
+
88
+ ## Headers, footers & pages
89
+
90
+ Define `header` and `footer` alongside `view_template` for content that repeats
91
+ at the top and bottom of every page. When the body is taller than one page it
92
+ splits automatically, and `page_number` / `page_count` are available inside the
93
+ header and footer.
94
+
95
+ ```ruby
96
+ class Report < Bandoola::View
97
+ def header = text(class: "font-bold mb-4") { "Quarterly Report" }
98
+ def footer = text(class: "text-sm mt-4") { "Page #{page_number} of #{page_count}" }
99
+
100
+ def view_template
101
+ 50.times { |i| div(class: "p-2 mb-2 border-1") { text { "Row #{i + 1}" } } }
102
+ end
103
+ end
104
+ ```
105
+
106
+ Give the header an `mb-*` (and the footer an `mt-*`, or use `my-*`) for some air
107
+ between them and the body. Pages break between top-level elements — an element
108
+ isn't split across a page boundary.
109
+
110
+ ## Page size
111
+
112
+ The default page is A4. Override the geometry per view:
113
+
114
+ ```ruby
115
+ class Wide < Bandoola::View
116
+ def width = 1190
117
+ def height = 842
118
+ def margin = 48
119
+ end
120
+ ```
121
+
122
+ ## Command line
123
+
124
+ Render a file that defines a view straight to a PDF:
125
+
126
+ ```bash
127
+ bundle exec bandoola doc.rb > doc.pdf
128
+ ```
129
+
130
+ ## Development
131
+
132
+ `bin/setup` to install dependencies, `rake test` to run the tests, `bin/console`
133
+ for a prompt. Running `rake test` also regenerates the inspectable example PDFs
134
+ under `tmp/pdf_samples/`.
135
+
136
+ ## Status
137
+
138
+ Bandoola is pre-release and the API may shift. It is **not** a browser — there's
139
+ no cascade, no flexbox, line breaking is greedy (no hyphenation or justification),
140
+ and a class either does what's listed above or is ignored. I add things as I need
141
+ them, so coverage is deliberately partial; PRs and issues that fill a gap are
142
+ welcome.
143
+
144
+ ## License
145
+
146
+ Available as open source under the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "minitest/test_task"
3
+
4
+ Minitest::TestTask.create
5
+
6
+ require "rubocop/rake_task"
7
+
8
+ RuboCop::RakeTask.new
9
+
10
+ task default: %i[test rubocop]
data/exe/bandoola ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bandoola"
4
+
5
+ exit Bandoola::CLI.run(ARGV)
@@ -0,0 +1,44 @@
1
+ module Bandoola
2
+ # The `bandoola` command-line tool. It loads a Ruby file that defines a
3
+ # Bandoola::View subclass, renders it, and writes the PDF bytes to an IO
4
+ # (stdout by default), so a document can be produced with:
5
+ #
6
+ # bundle exec bandoola foo.rb > foo.pdf
7
+ #
8
+ # If the file defines more than one view, the last one defined is rendered.
9
+ class CLI
10
+ # Raised for problems the user can fix (bad arguments, missing file, no
11
+ # view); reported as a tidy message rather than a backtrace.
12
+ class Error < Bandoola::Error
13
+ end
14
+
15
+ # Run the tool. Returns a process exit status (0 on success, 1 on a
16
+ # user-facing error). +out+ receives the PDF; +err+ receives messages.
17
+ def self.run(argv, out: $stdout, err: $stderr)
18
+ path = argv.first
19
+ raise Error, "usage: bandoola FILE.rb" if path.nil? || path.empty?
20
+ raise Error, "no such file: #{path}" unless File.file?(path)
21
+
22
+ view_class = load_view(path)
23
+ out.binmode if out.respond_to?(:binmode)
24
+ out.write(view_class.new.render)
25
+ 0
26
+ rescue Error => e
27
+ err.puts "bandoola: #{e.message}"
28
+ 1
29
+ end
30
+
31
+ # Load +path+ and return the View subclass it defined. Comparing the
32
+ # registry before and after isolates the file's own view(s) from any
33
+ # others already loaded.
34
+ def self.load_view(path)
35
+ known = View.registry.dup
36
+ load File.expand_path(path)
37
+ defined_views = View.registry - known
38
+ raise Error, "#{path} defines no Bandoola::View subclass" if defined_views.empty?
39
+
40
+ defined_views.last
41
+ end
42
+ private_class_method :load_view
43
+ end
44
+ end
@@ -0,0 +1,204 @@
1
+ module Bandoola
2
+ # One of the 14 standard PDF fonts (no embedding needed): viewers ship the
3
+ # metrics, so a single Font object naming the BaseFont is enough. Text is
4
+ # single-byte WinAnsiEncoding, which covers Latin/Western European only.
5
+ class StandardFont
6
+ def initialize(base_font)
7
+ @base_font = base_font
8
+ end
9
+
10
+ # How many PDF objects this font occupies (for object numbering).
11
+ def object_count
12
+ 1
13
+ end
14
+
15
+ # The PDF object bodies, given the object number of the first.
16
+ def pdf_objects(_first)
17
+ ["<< /Type /Font /Subtype /Type1 /BaseFont /#{@base_font} /Encoding /WinAnsiEncoding >>"]
18
+ end
19
+
20
+ # Encode a string as a PDF literal for the Tj operator. Transcode UTF-8 to
21
+ # Windows-1252 (characters outside it become "?") and escape delimiters.
22
+ def encode(string)
23
+ bytes = string.encode("Windows-1252", invalid: :replace, undef: :replace, replace: "?").b
24
+ "(#{bytes.gsub(/[\\()]/) { |char| "\\#{char}" }})"
25
+ end
26
+
27
+ COURIER_WIDTH = 600 # Courier is monospaced
28
+ QUESTION_MARK = 0x3F # fallback for characters outside the metric table
29
+
30
+ # The width of +string+ at +size+ points, from the Adobe Core metrics.
31
+ def width_of(string, size)
32
+ string.codepoints.sum { |codepoint| char_width(codepoint) } * size / 1000.0
33
+ end
34
+
35
+ private
36
+
37
+ def char_width(codepoint)
38
+ return COURIER_WIDTH if @base_font.start_with?("Courier")
39
+
40
+ table = METRICS.fetch(@base_font)
41
+ table[codepoint] || table[QUESTION_MARK]
42
+ end
43
+ end
44
+
45
+ # A TrueType font embedded as a Type0 (composite) font with Identity-H
46
+ # encoding. Text is written as the font's own 2-byte glyph ids, which lifts
47
+ # the 256-character limit of simple fonts: any Unicode the font has a glyph
48
+ # for — accented Latin, Cyrillic, CJK — renders. It collects the glyphs used
49
+ # during painting so the /W widths and the /ToUnicode map cover exactly them.
50
+ #
51
+ # It occupies five objects: the Type0 font, the CIDFont descendant, the
52
+ # FontDescriptor, the FontFile2 program, and the ToUnicode CMap.
53
+ class EmbeddedFont
54
+ DEFAULT_WIDTH = 1000
55
+
56
+ CMAP_HEADER = <<~CMAP.freeze
57
+ /CIDInit /ProcSet findresource begin
58
+ 12 dict begin
59
+ begincmap
60
+ /CIDSystemInfo << /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def
61
+ /CMapName /Adobe-Identity-UCS def
62
+ /CMapType 2 def
63
+ 1 begincodespacerange
64
+ <0000> <FFFF>
65
+ endcodespacerange
66
+ CMAP
67
+
68
+ CMAP_FOOTER = <<~CMAP.freeze
69
+ endcmap
70
+ CMapName currentdict /CMap defineresource pop
71
+ end
72
+ end
73
+ CMAP
74
+
75
+ def self.load(path, compact: true)
76
+ new(TrueTypeFont.load(path), compact: compact)
77
+ end
78
+
79
+ def initialize(ttf, compact: true)
80
+ @ttf = ttf
81
+ @compact = compact
82
+ @codes = {} # content-stream code => Unicode code point
83
+ @order = [0] # compact mode: code => original glyph id (0 = .notdef)
84
+ @ids = { 0 => 0 } # compact mode: original glyph id => code
85
+ end
86
+
87
+ def object_count
88
+ 5
89
+ end
90
+
91
+ # The width of +string+ at +size+ points, summing the glyphs' advances.
92
+ def width_of(string, size)
93
+ string.codepoints.sum { |codepoint| @ttf.width(codepoint) } * size / 1000.0
94
+ end
95
+
96
+ # Encode a string as Identity-H: each character becomes a 2-byte code,
97
+ # written as a PDF hex string for the Tj operator. In compact mode the code
98
+ # is a freshly assigned dense id; otherwise it's the original glyph id.
99
+ # Records the codes used so /W and /ToUnicode can describe them.
100
+ def encode(string)
101
+ hex = string.each_char.map do |char|
102
+ codepoint = char.ord
103
+ code = code_for(@ttf.glyph(codepoint))
104
+ @codes[code] = codepoint
105
+ format("%04X", code)
106
+ end
107
+ "<#{hex.join}>"
108
+ end
109
+
110
+ def code_for(glyph)
111
+ return glyph unless @compact
112
+
113
+ @ids[glyph] ||= begin
114
+ @order << glyph
115
+ @order.size - 1
116
+ end
117
+ end
118
+
119
+ def pdf_objects(first)
120
+ descendant = first + 1
121
+ descriptor = first + 2
122
+ file = first + 3
123
+ unicode = first + 4
124
+ [type0_font(descendant, unicode), cid_font(descriptor), font_descriptor(file),
125
+ font_file, to_unicode]
126
+ end
127
+
128
+ private
129
+
130
+ def name
131
+ @ttf.postscript_name
132
+ end
133
+
134
+ def type0_font(descendant, unicode)
135
+ "<< /Type /Font /Subtype /Type0 /BaseFont /#{name} /Encoding /Identity-H " \
136
+ "/DescendantFonts [#{descendant} 0 R] /ToUnicode #{unicode} 0 R >>"
137
+ end
138
+
139
+ def cid_font(descriptor)
140
+ "<< /Type /Font /Subtype /CIDFontType2 /BaseFont /#{name} " \
141
+ "/CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >> " \
142
+ "/FontDescriptor #{descriptor} 0 R /CIDToGIDMap /Identity " \
143
+ "/DW #{DEFAULT_WIDTH} /W [#{widths}] >>"
144
+ end
145
+
146
+ # One "code [width]" entry per used code (CID equals the content-stream
147
+ # code under the Identity map). In compact mode the code maps back to the
148
+ # original glyph through @order.
149
+ def widths
150
+ @codes.keys.sort.map { |code| "#{code} [#{@ttf.glyph_width(original(code))}]" }.join(" ")
151
+ end
152
+
153
+ # The original glyph id behind a content-stream code.
154
+ def original(code)
155
+ @compact ? @order[code] : code
156
+ end
157
+
158
+ def font_descriptor(file)
159
+ bbox = @ttf.font_bbox.join(" ")
160
+ "<< /Type /FontDescriptor /FontName /#{name} /Flags #{@ttf.flags} " \
161
+ "/FontBBox [#{bbox}] /ItalicAngle #{@ttf.italic_angle} /Ascent #{@ttf.ascent} " \
162
+ "/Descent #{@ttf.descent} /CapHeight #{@ttf.cap_height} /StemV 80 " \
163
+ "/FontFile2 #{file} 0 R >>"
164
+ end
165
+
166
+ # The embedded font program: a subset built from exactly the glyphs used,
167
+ # so the file ships a few KB instead of the whole font. Compact mode also
168
+ # renumbers glyphs (shrinking loca/hmtx); sparse mode keeps glyph ids.
169
+ # Falls back to full embedding if the font can't be subset (e.g. CFF).
170
+ # Length1 is the size of the (uncompressed) TrueType data — equal to Length
171
+ # since no filter is applied.
172
+ def font_file
173
+ bytes = subset
174
+ "<< /Length #{bytes.bytesize} /Length1 #{bytes.bytesize} >>\nstream\n".b + bytes + "\nendstream".b
175
+ end
176
+
177
+ def subset
178
+ if @compact
179
+ TrueTypeSubset.compact(@ttf.data, @order)
180
+ else
181
+ TrueTypeSubset.sparse(@ttf.data, @codes.keys)
182
+ end
183
+ rescue StandardError
184
+ @ttf.data
185
+ end
186
+
187
+ # The ToUnicode CMap maps content-stream codes back to Unicode so the text
188
+ # stays searchable and copyable.
189
+ def to_unicode
190
+ body = +CMAP_HEADER
191
+ entries = @codes.map { |code, codepoint| "<#{format("%04X", code)}> <#{utf16(codepoint)}>" }
192
+ entries.each_slice(100) do |chunk|
193
+ body << "#{chunk.size} beginbfchar\n#{chunk.join("\n")}\nendbfchar\n"
194
+ end
195
+ body << CMAP_FOOTER
196
+ "<< /Length #{body.bytesize} >>\nstream\n#{body}endstream"
197
+ end
198
+
199
+ # A code point as UTF-16BE hex (a surrogate pair for non-BMP code points).
200
+ def utf16(codepoint)
201
+ [codepoint].pack("U").encode("UTF-16BE").unpack1("H*").upcase
202
+ end
203
+ end
204
+ end
@@ -0,0 +1,104 @@
1
+ module Bandoola
2
+ # A JPEG image, embedded into a PDF as an image XObject. PDF can carry JPEG
3
+ # data verbatim through the DCTDecode filter, so the file's bytes go into the
4
+ # stream unchanged; only the pixel dimensions and color space need reading.
5
+ #
6
+ # A standalone value object that knows nothing about View or layout: it
7
+ # supplies its intrinsic size to the layout pass and, at paint time,
8
+ # registers itself with the page Resources and emits the `Do` that draws it.
9
+ class Jpeg
10
+ # SOF (Start Of Frame) markers carry the dimensions. SOF0..SOF15 are
11
+ # 0xC0..0xCF, except DHT (C4), JPG (C8) and DAC (CC), which are not frames.
12
+ NON_FRAME = [0xC4, 0xC8, 0xCC].freeze
13
+
14
+ def self.load(src)
15
+ new(File.binread(src))
16
+ end
17
+
18
+ def initialize(data)
19
+ @data = data
20
+ @width, @height, @components = read_frame(data)
21
+ end
22
+
23
+ attr_reader :width, :height
24
+
25
+ # Pixel dimensions double as the intrinsic point size (72 DPI).
26
+ def intrinsic_width
27
+ width
28
+ end
29
+
30
+ def aspect
31
+ width.to_f / height
32
+ end
33
+
34
+ # Append the operators that draw this image into +box+ ([left, top, width,
35
+ # height]). PDF image space is the unit square, so the cm matrix scales it
36
+ # to the box; no y flip is needed (unlike vector paths). Registers the
37
+ # XObject by name.
38
+ def paint_into(buffer, resources, box)
39
+ left, top, width, height = box
40
+ name = resources.add_image(self)
41
+ bottom = top - height
42
+ buffer << "q\n#{fmt(width)} 0 0 #{fmt(height)} #{fmt(left)} #{fmt(bottom)} cm\n" \
43
+ "/#{name} Do\nQ\n"
44
+ end
45
+
46
+ # The image XObject body (dictionary + raw JPEG stream), as binary bytes.
47
+ def xobject
48
+ header = "<< /Type /XObject /Subtype /Image /Width #{width} /Height #{height} " \
49
+ "/BitsPerComponent 8 /ColorSpace /#{color_space} /Filter /DCTDecode " \
50
+ "/Length #{@data.bytesize} >>\nstream\n"
51
+ header.b + @data + "\nendstream".b
52
+ end
53
+
54
+ private
55
+
56
+ def color_space
57
+ @components == 1 ? "DeviceGray" : "DeviceRGB"
58
+ end
59
+
60
+ # Walk the JPEG marker segments to the first frame header and read its
61
+ # height, width and component count.
62
+ def read_frame(data)
63
+ offset = 2 # skip the SOI marker (FFD8)
64
+ while offset < data.bytesize - 1
65
+ unless data.getbyte(offset) == 0xFF
66
+ offset += 1
67
+ next
68
+ end
69
+
70
+ marker = data.getbyte(offset + 1)
71
+ offset += 2
72
+ next if standalone?(marker)
73
+
74
+ length = (data.getbyte(offset) << 8) | data.getbyte(offset + 1)
75
+ return frame_size(data, offset) if frame?(marker)
76
+
77
+ offset += length
78
+ end
79
+ raise Error, "could not find JPEG frame header"
80
+ end
81
+
82
+ # Markers that carry no length segment: padding (FF), RSTn/SOI/EOI
83
+ # (D0..D9) and TEM (01).
84
+ def standalone?(marker)
85
+ marker == 0xFF || marker.between?(0xD0, 0xD9) || marker == 0x01
86
+ end
87
+
88
+ def frame?(marker)
89
+ marker.between?(0xC0, 0xCF) && !NON_FRAME.include?(marker)
90
+ end
91
+
92
+ # Inside a frame segment: [length:2][precision:1][height:2][width:2][comps:1]
93
+ def frame_size(data, offset)
94
+ height = (data.getbyte(offset + 3) << 8) | data.getbyte(offset + 4)
95
+ width = (data.getbyte(offset + 5) << 8) | data.getbyte(offset + 6)
96
+ components = data.getbyte(offset + 7)
97
+ [width, height, components]
98
+ end
99
+
100
+ def fmt(value)
101
+ format("%.2f", value).sub(/\.?0+\z/, "")
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,113 @@
1
+ module Bandoola
2
+ class StandardFont
3
+ # Adobe Core font advance widths (1000-unit em) by WinAnsi/Latin-1 code
4
+ # point, for the standard PDF faces families map to. Used to measure and
5
+ # wrap text. Courier is monospace, handled in code.
6
+ METRICS = {
7
+ "Helvetica" => {
8
+ 32 => 278, 33 => 278, 34 => 355, 35 => 556, 36 => 556, 37 => 889, 38 => 667, 39 => 222,
9
+ 40 => 333, 41 => 333, 42 => 389, 43 => 584, 44 => 278, 45 => 333, 46 => 278, 47 => 278,
10
+ 48 => 556, 49 => 556, 50 => 556, 51 => 556, 52 => 556, 53 => 556, 54 => 556, 55 => 556,
11
+ 56 => 556, 57 => 556, 58 => 278, 59 => 278, 60 => 584, 61 => 584, 62 => 584, 63 => 556,
12
+ 64 => 1015, 65 => 667, 66 => 667, 67 => 722, 68 => 722, 69 => 667, 70 => 611, 71 => 778,
13
+ 72 => 722, 73 => 278, 74 => 500, 75 => 667, 76 => 556, 77 => 833, 78 => 722, 79 => 778,
14
+ 80 => 667, 81 => 778, 82 => 722, 83 => 667, 84 => 611, 85 => 722, 86 => 667, 87 => 944,
15
+ 88 => 667, 89 => 667, 90 => 611, 91 => 278, 92 => 278, 93 => 278, 94 => 469, 95 => 556,
16
+ 96 => 222, 97 => 556, 98 => 556, 99 => 500, 100 => 556, 101 => 556, 102 => 278, 103 => 556,
17
+ 104 => 556, 105 => 222, 106 => 222, 107 => 500, 108 => 222, 109 => 833, 110 => 556, 111 => 556,
18
+ 112 => 556, 113 => 556, 114 => 333, 115 => 500, 116 => 278, 117 => 556, 118 => 500, 119 => 722,
19
+ 120 => 500, 121 => 500, 122 => 500, 123 => 334, 124 => 260, 125 => 334, 126 => 584, 160 => 278,
20
+ 161 => 333, 162 => 556, 163 => 556, 164 => 556, 165 => 556, 166 => 260, 167 => 556, 168 => 333,
21
+ 169 => 737, 170 => 370, 171 => 556, 172 => 584, 173 => 333, 174 => 737, 175 => 333, 176 => 400,
22
+ 177 => 584, 178 => 333, 179 => 333, 180 => 333, 181 => 556, 182 => 537, 183 => 278, 184 => 333,
23
+ 185 => 333, 186 => 365, 187 => 556, 188 => 834, 189 => 834, 190 => 834, 191 => 611, 192 => 667,
24
+ 193 => 667, 194 => 667, 195 => 667, 196 => 667, 197 => 667, 198 => 1000, 199 => 722, 200 => 667,
25
+ 201 => 667, 202 => 667, 203 => 667, 204 => 278, 205 => 278, 206 => 278, 207 => 278, 208 => 722,
26
+ 209 => 722, 210 => 778, 211 => 778, 212 => 778, 213 => 778, 214 => 778, 215 => 584, 216 => 778,
27
+ 217 => 722, 218 => 722, 219 => 722, 220 => 722, 221 => 667, 222 => 667, 223 => 611, 224 => 556,
28
+ 225 => 556, 226 => 556, 227 => 556, 228 => 556, 229 => 556, 230 => 889, 231 => 500, 232 => 556,
29
+ 233 => 556, 234 => 556, 235 => 556, 236 => 278, 237 => 278, 238 => 278, 239 => 278, 240 => 556,
30
+ 241 => 556, 242 => 556, 243 => 556, 244 => 556, 245 => 556, 246 => 556, 247 => 584, 248 => 611,
31
+ 249 => 556, 250 => 556, 251 => 556, 252 => 556, 253 => 500, 254 => 556, 255 => 500
32
+ },
33
+ "Helvetica-Bold" => {
34
+ 32 => 278, 33 => 333, 34 => 474, 35 => 556, 36 => 556, 37 => 889, 38 => 722, 39 => 278,
35
+ 40 => 333, 41 => 333, 42 => 389, 43 => 584, 44 => 278, 45 => 333, 46 => 278, 47 => 278,
36
+ 48 => 556, 49 => 556, 50 => 556, 51 => 556, 52 => 556, 53 => 556, 54 => 556, 55 => 556,
37
+ 56 => 556, 57 => 556, 58 => 333, 59 => 333, 60 => 584, 61 => 584, 62 => 584, 63 => 611,
38
+ 64 => 975, 65 => 722, 66 => 722, 67 => 722, 68 => 722, 69 => 667, 70 => 611, 71 => 778,
39
+ 72 => 722, 73 => 278, 74 => 556, 75 => 722, 76 => 611, 77 => 833, 78 => 722, 79 => 778,
40
+ 80 => 667, 81 => 778, 82 => 722, 83 => 667, 84 => 611, 85 => 722, 86 => 667, 87 => 944,
41
+ 88 => 667, 89 => 667, 90 => 611, 91 => 333, 92 => 278, 93 => 333, 94 => 584, 95 => 556,
42
+ 96 => 278, 97 => 556, 98 => 611, 99 => 556, 100 => 611, 101 => 556, 102 => 333, 103 => 611,
43
+ 104 => 611, 105 => 278, 106 => 278, 107 => 556, 108 => 278, 109 => 889, 110 => 611, 111 => 611,
44
+ 112 => 611, 113 => 611, 114 => 389, 115 => 556, 116 => 333, 117 => 611, 118 => 556, 119 => 778,
45
+ 120 => 556, 121 => 556, 122 => 500, 123 => 389, 124 => 280, 125 => 389, 126 => 584, 160 => 278,
46
+ 161 => 333, 162 => 556, 163 => 556, 164 => 556, 165 => 556, 166 => 280, 167 => 556, 168 => 333,
47
+ 169 => 737, 170 => 370, 171 => 556, 172 => 584, 173 => 333, 174 => 737, 175 => 333, 176 => 400,
48
+ 177 => 584, 178 => 333, 179 => 333, 180 => 333, 181 => 611, 182 => 556, 183 => 278, 184 => 333,
49
+ 185 => 333, 186 => 365, 187 => 556, 188 => 834, 189 => 834, 190 => 834, 191 => 611, 192 => 722,
50
+ 193 => 722, 194 => 722, 195 => 722, 196 => 722, 197 => 722, 198 => 1000, 199 => 722, 200 => 667,
51
+ 201 => 667, 202 => 667, 203 => 667, 204 => 278, 205 => 278, 206 => 278, 207 => 278, 208 => 722,
52
+ 209 => 722, 210 => 778, 211 => 778, 212 => 778, 213 => 778, 214 => 778, 215 => 584, 216 => 778,
53
+ 217 => 722, 218 => 722, 219 => 722, 220 => 722, 221 => 667, 222 => 667, 223 => 611, 224 => 556,
54
+ 225 => 556, 226 => 556, 227 => 556, 228 => 556, 229 => 556, 230 => 889, 231 => 556, 232 => 556,
55
+ 233 => 556, 234 => 556, 235 => 556, 236 => 278, 237 => 278, 238 => 278, 239 => 278, 240 => 611,
56
+ 241 => 611, 242 => 611, 243 => 611, 244 => 611, 245 => 611, 246 => 611, 247 => 584, 248 => 611,
57
+ 249 => 611, 250 => 611, 251 => 611, 252 => 611, 253 => 556, 254 => 611, 255 => 556
58
+ },
59
+ "Times-Roman" => {
60
+ 32 => 250, 33 => 333, 34 => 408, 35 => 500, 36 => 500, 37 => 833, 38 => 778, 39 => 333,
61
+ 40 => 333, 41 => 333, 42 => 500, 43 => 564, 44 => 250, 45 => 333, 46 => 250, 47 => 278,
62
+ 48 => 500, 49 => 500, 50 => 500, 51 => 500, 52 => 500, 53 => 500, 54 => 500, 55 => 500,
63
+ 56 => 500, 57 => 500, 58 => 278, 59 => 278, 60 => 564, 61 => 564, 62 => 564, 63 => 444,
64
+ 64 => 921, 65 => 722, 66 => 667, 67 => 667, 68 => 722, 69 => 611, 70 => 556, 71 => 722,
65
+ 72 => 722, 73 => 333, 74 => 389, 75 => 722, 76 => 611, 77 => 889, 78 => 722, 79 => 722,
66
+ 80 => 556, 81 => 722, 82 => 667, 83 => 556, 84 => 611, 85 => 722, 86 => 722, 87 => 944,
67
+ 88 => 722, 89 => 722, 90 => 611, 91 => 333, 92 => 278, 93 => 333, 94 => 469, 95 => 500,
68
+ 96 => 333, 97 => 444, 98 => 500, 99 => 444, 100 => 500, 101 => 444, 102 => 333, 103 => 500,
69
+ 104 => 500, 105 => 278, 106 => 278, 107 => 500, 108 => 278, 109 => 778, 110 => 500, 111 => 500,
70
+ 112 => 500, 113 => 500, 114 => 333, 115 => 389, 116 => 278, 117 => 500, 118 => 500, 119 => 722,
71
+ 120 => 500, 121 => 500, 122 => 444, 123 => 480, 124 => 200, 125 => 480, 126 => 541, 160 => 250,
72
+ 161 => 333, 162 => 500, 163 => 500, 164 => 500, 165 => 500, 166 => 200, 167 => 500, 168 => 333,
73
+ 169 => 760, 170 => 276, 171 => 500, 172 => 564, 173 => 333, 174 => 760, 175 => 333, 176 => 400,
74
+ 177 => 564, 178 => 300, 179 => 300, 180 => 333, 181 => 500, 182 => 453, 183 => 250, 184 => 333,
75
+ 185 => 300, 186 => 310, 187 => 500, 188 => 750, 189 => 750, 190 => 750, 191 => 444, 192 => 722,
76
+ 193 => 722, 194 => 722, 195 => 722, 196 => 722, 197 => 722, 198 => 889, 199 => 667, 200 => 611,
77
+ 201 => 611, 202 => 611, 203 => 611, 204 => 333, 205 => 333, 206 => 333, 207 => 333, 208 => 722,
78
+ 209 => 722, 210 => 722, 211 => 722, 212 => 722, 213 => 722, 214 => 722, 215 => 564, 216 => 722,
79
+ 217 => 722, 218 => 722, 219 => 722, 220 => 722, 221 => 722, 222 => 556, 223 => 500, 224 => 444,
80
+ 225 => 444, 226 => 444, 227 => 444, 228 => 444, 229 => 444, 230 => 667, 231 => 444, 232 => 444,
81
+ 233 => 444, 234 => 444, 235 => 444, 236 => 278, 237 => 278, 238 => 278, 239 => 278, 240 => 500,
82
+ 241 => 500, 242 => 500, 243 => 500, 244 => 500, 245 => 500, 246 => 500, 247 => 564, 248 => 500,
83
+ 249 => 500, 250 => 500, 251 => 500, 252 => 500, 253 => 500, 254 => 500, 255 => 500
84
+ },
85
+ "Times-Bold" => {
86
+ 32 => 250, 33 => 333, 34 => 555, 35 => 500, 36 => 500, 37 => 1000, 38 => 833, 39 => 333,
87
+ 40 => 333, 41 => 333, 42 => 500, 43 => 570, 44 => 250, 45 => 333, 46 => 250, 47 => 278,
88
+ 48 => 500, 49 => 500, 50 => 500, 51 => 500, 52 => 500, 53 => 500, 54 => 500, 55 => 500,
89
+ 56 => 500, 57 => 500, 58 => 333, 59 => 333, 60 => 570, 61 => 570, 62 => 570, 63 => 500,
90
+ 64 => 930, 65 => 722, 66 => 667, 67 => 722, 68 => 722, 69 => 667, 70 => 611, 71 => 778,
91
+ 72 => 778, 73 => 389, 74 => 500, 75 => 778, 76 => 667, 77 => 944, 78 => 722, 79 => 778,
92
+ 80 => 611, 81 => 778, 82 => 722, 83 => 556, 84 => 667, 85 => 722, 86 => 722, 87 => 1000,
93
+ 88 => 722, 89 => 722, 90 => 667, 91 => 333, 92 => 278, 93 => 333, 94 => 581, 95 => 500,
94
+ 96 => 333, 97 => 500, 98 => 556, 99 => 444, 100 => 556, 101 => 444, 102 => 333, 103 => 500,
95
+ 104 => 556, 105 => 278, 106 => 333, 107 => 556, 108 => 278, 109 => 833, 110 => 556, 111 => 500,
96
+ 112 => 556, 113 => 556, 114 => 444, 115 => 389, 116 => 333, 117 => 556, 118 => 500, 119 => 722,
97
+ 120 => 500, 121 => 500, 122 => 444, 123 => 394, 124 => 220, 125 => 394, 126 => 520, 160 => 250,
98
+ 161 => 333, 162 => 500, 163 => 500, 164 => 500, 165 => 500, 166 => 220, 167 => 500, 168 => 333,
99
+ 169 => 747, 170 => 300, 171 => 500, 172 => 570, 173 => 333, 174 => 747, 175 => 333, 176 => 400,
100
+ 177 => 570, 178 => 300, 179 => 300, 180 => 333, 181 => 556, 182 => 540, 183 => 250, 184 => 333,
101
+ 185 => 300, 186 => 330, 187 => 500, 188 => 750, 189 => 750, 190 => 750, 191 => 500, 192 => 722,
102
+ 193 => 722, 194 => 722, 195 => 722, 196 => 722, 197 => 722, 198 => 1000, 199 => 722, 200 => 667,
103
+ 201 => 667, 202 => 667, 203 => 667, 204 => 389, 205 => 389, 206 => 389, 207 => 389, 208 => 722,
104
+ 209 => 722, 210 => 778, 211 => 778, 212 => 778, 213 => 778, 214 => 778, 215 => 570, 216 => 778,
105
+ 217 => 722, 218 => 722, 219 => 722, 220 => 722, 221 => 722, 222 => 611, 223 => 556, 224 => 500,
106
+ 225 => 500, 226 => 500, 227 => 500, 228 => 500, 229 => 500, 230 => 722, 231 => 444, 232 => 444,
107
+ 233 => 444, 234 => 444, 235 => 444, 236 => 278, 237 => 278, 238 => 278, 239 => 278, 240 => 500,
108
+ 241 => 556, 242 => 500, 243 => 500, 244 => 500, 245 => 500, 246 => 500, 247 => 570, 248 => 500,
109
+ 249 => 556, 250 => 556, 251 => 556, 252 => 556, 253 => 500, 254 => 556, 255 => 500
110
+ }
111
+ }.freeze
112
+ end
113
+ end