stationery 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.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +12 -0
  3. data/LICENSE.txt +28 -0
  4. data/README.md +145 -0
  5. data/lib/stationery/builder.rb +61 -0
  6. data/lib/stationery/canvas/path.rb +82 -0
  7. data/lib/stationery/canvas/text.rb +55 -0
  8. data/lib/stationery/canvas.rb +121 -0
  9. data/lib/stationery/color.rb +62 -0
  10. data/lib/stationery/component.rb +54 -0
  11. data/lib/stationery/document.rb +95 -0
  12. data/lib/stationery/elements.rb +90 -0
  13. data/lib/stationery/errors.rb +11 -0
  14. data/lib/stationery/fonts/cmap.rb +75 -0
  15. data/lib/stationery/fonts/family.rb +51 -0
  16. data/lib/stationery/fonts/font.rb +149 -0
  17. data/lib/stationery/fonts/font_book.rb +36 -0
  18. data/lib/stationery/fonts/name_table.rb +39 -0
  19. data/lib/stationery/fonts/registry.rb +38 -0
  20. data/lib/stationery/fonts/subset.rb +139 -0
  21. data/lib/stationery/fonts/true_type.rb +174 -0
  22. data/lib/stationery/geometry.rb +56 -0
  23. data/lib/stationery/images/cache.rb +32 -0
  24. data/lib/stationery/images/images.rb +45 -0
  25. data/lib/stationery/images/jpeg.rb +53 -0
  26. data/lib/stationery/images/png.rb +119 -0
  27. data/lib/stationery/images/scanlines.rb +56 -0
  28. data/lib/stationery/layout/box.rb +134 -0
  29. data/lib/stationery/layout/flow.rb +121 -0
  30. data/lib/stationery/layout/image.rb +53 -0
  31. data/lib/stationery/layout/leaves.rb +58 -0
  32. data/lib/stationery/layout/node.rb +46 -0
  33. data/lib/stationery/layout/paginator.rb +41 -0
  34. data/lib/stationery/layout/positioned.rb +24 -0
  35. data/lib/stationery/layout/row.rb +71 -0
  36. data/lib/stationery/layout/table/cell.rb +93 -0
  37. data/lib/stationery/layout/table/selection.rb +52 -0
  38. data/lib/stationery/layout/table/widths.rb +53 -0
  39. data/lib/stationery/layout/table.rb +118 -0
  40. data/lib/stationery/layout/text.rb +62 -0
  41. data/lib/stationery/page.rb +47 -0
  42. data/lib/stationery/page_templates.rb +36 -0
  43. data/lib/stationery/pdf/assembler.rb +53 -0
  44. data/lib/stationery/pdf/serializer.rb +56 -0
  45. data/lib/stationery/pdf/stream.rb +27 -0
  46. data/lib/stationery/pdf/types.rb +15 -0
  47. data/lib/stationery/pdf/writer.rb +59 -0
  48. data/lib/stationery/rails.rb +20 -0
  49. data/lib/stationery/resources.rb +34 -0
  50. data/lib/stationery/text/entities.rb +23 -0
  51. data/lib/stationery/text/line.rb +26 -0
  52. data/lib/stationery/text/markup.rb +103 -0
  53. data/lib/stationery/text/paragraph.rb +98 -0
  54. data/lib/stationery/text/runs_builder.rb +52 -0
  55. data/lib/stationery/text/style.rb +46 -0
  56. data/lib/stationery/text/wrapper.rb +131 -0
  57. data/lib/stationery/version.rb +5 -0
  58. data/lib/stationery.rb +58 -0
  59. metadata +108 -0
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "strscan"
4
+
5
+ module Stationery
6
+ module Text
7
+ # Parses Prawn-style inline markup into runs:
8
+ #
9
+ # <b> <strong> <i> <em> <u> <strikethrough> <s> <del> <sub> <sup> <br>
10
+ # <color rgb="#hex"> <color c="" m="" y="" k="">
11
+ # <font size="n" name="Family"> <link href="…"> <a href="…">
12
+ #
13
+ # Unknown tags are dropped and their content kept. A "<" that does not open
14
+ # a well-formed tag is text, so "1 < 2" survives.
15
+ class Markup
16
+ TAG = %r{<(/?)([a-zA-Z][a-zA-Z0-9]*)((?:\s+[a-zA-Z_:-]+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>"']+))*)\s*/?>}
17
+ ATTRIBUTE = /([a-zA-Z_:-]+)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>"']+))/
18
+ STYLES = {
19
+ "b" => { weight: :bold }, "strong" => { weight: :bold },
20
+ "i" => { style: :italic }, "em" => { style: :italic },
21
+ "u" => { underline: true },
22
+ "strikethrough" => { strikethrough: true }, "s" => { strikethrough: true }, "del" => { strikethrough: true },
23
+ "sub" => { script: :sub }, "sup" => { script: :sup }
24
+ }.freeze
25
+
26
+ def self.parse(source, style)
27
+ new(source, style).runs
28
+ end
29
+
30
+ def initialize(source, style)
31
+ @scanner = StringScanner.new(source.to_s)
32
+ @stack = [[nil, style]]
33
+ @runs = []
34
+ end
35
+
36
+ def runs
37
+ until @scanner.eos?
38
+ if (text = @scanner.scan(/[^<]+/))
39
+ emit(Entities.decode(text))
40
+ elsif @scanner.check(TAG)
41
+ tag(@scanner.scan(TAG))
42
+ else
43
+ emit(@scanner.getch)
44
+ end
45
+ end
46
+ Run.merge(@runs)
47
+ end
48
+
49
+ private
50
+
51
+ def emit(text)
52
+ @runs << Run.new(text, @stack.last.last)
53
+ end
54
+
55
+ def tag(source)
56
+ closing, name, attributes = source.match(TAG).captures
57
+ name = name.downcase
58
+ return emit("\n") if name == "br"
59
+ return close(name) if closing == "/"
60
+
61
+ overrides = overrides_for(name, attributes(attributes))
62
+ @stack << [name, @stack.last.last.with(**overrides)] if overrides
63
+ end
64
+
65
+ def close(name)
66
+ index = @stack.rindex { |tag_name, _| tag_name == name }
67
+ @stack.slice!(index..) if index&.positive?
68
+ end
69
+
70
+ def overrides_for(name, attributes)
71
+ return STYLES[name] if STYLES.key?(name)
72
+
73
+ case name
74
+ when "color" then color(attributes)
75
+ when "font" then font(attributes)
76
+ when "link", "a" then attributes["href"] ? { link: attributes["href"] } : {}
77
+ end
78
+ end
79
+
80
+ def color(attributes)
81
+ return { color: attributes["rgb"] } if attributes["rgb"]
82
+ return {} unless %w[c m y k].all? { |key| attributes.key?(key) }
83
+
84
+ { color: %w[c m y k].map { |key| number(attributes[key]) } }
85
+ end
86
+
87
+ def font(attributes)
88
+ overrides = {}
89
+ overrides[:size] = number(attributes["size"]) if attributes["size"]
90
+ overrides[:family] = attributes["name"] if attributes["name"]
91
+ overrides
92
+ end
93
+
94
+ def attributes(source)
95
+ source.scan(ATTRIBUTE).to_h { |key, *values| [key.downcase, Entities.decode(values.compact.first)] }
96
+ end
97
+
98
+ def number(value)
99
+ value.include?(".") ? value.to_f : value.to_i
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stationery
4
+ module Text
5
+ # Wrapped lines of runs at a width, ready to measure, split across pages
6
+ # and draw.
7
+ class Paragraph
8
+ MIN_SHRINK_SIZE = 4
9
+
10
+ attr_reader :runs, :lines, :width, :align, :leading
11
+
12
+ def initialize(runs, book:, width:, align: :left, leading: 0, lines: nil, fallback_style: nil)
13
+ @runs = runs
14
+ @book = book
15
+ @width = width
16
+ @align = align
17
+ @leading = leading
18
+ @fallback_style = fallback_style || runs.first&.style
19
+ @lines = lines || Wrapper.new(book).wrap(runs, width, fallback_style: @fallback_style)
20
+ end
21
+
22
+ def height
23
+ return 0 if @lines.empty?
24
+
25
+ @lines.sum(&:height) + (@leading * (@lines.size - 1))
26
+ end
27
+
28
+ # [fits, rest] by whole lines; nil for an empty side.
29
+ def split(max_height)
30
+ count = fitting_lines(max_height)
31
+ return [self, nil] if count == @lines.size
32
+ return [nil, self] if count.zero?
33
+
34
+ [with_lines(@lines.first(count)), with_lines(@lines.drop(count))]
35
+ end
36
+
37
+ # A paragraph that fits `max_height`: truncated to whole lines, shrunk
38
+ # (never below 4pt) or left as is for :visible.
39
+ def fit(max_height, overflow:)
40
+ return self if overflow == :visible || height <= max_height
41
+ return split(max_height).first || with_lines([]) if overflow == :truncate
42
+
43
+ shrink(max_height)
44
+ end
45
+
46
+ def draw(canvas, x, y)
47
+ top = y
48
+ @lines.each do |line|
49
+ offset = x + Geometry.align_offset(@align, @width, line.width)
50
+ line.fragments.each { |fragment| draw_fragment(canvas, fragment, offset, top, line) }
51
+ top += line.height + @leading
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ def fitting_lines(max_height)
58
+ used = 0
59
+ @lines.take_while.with_index do |line, index|
60
+ used += line.height + (index.zero? ? 0 : @leading)
61
+ used <= max_height + 0.0001
62
+ end.size
63
+ end
64
+
65
+ def with_lines(lines)
66
+ self.class.new(@runs, book: @book, width: @width, align: @align, leading: @leading, lines:,
67
+ fallback_style: @fallback_style)
68
+ end
69
+
70
+ def shrink(max_height)
71
+ factor = 1.0
72
+ loop do
73
+ factor -= 0.05
74
+ scaled = scaled_runs(factor)
75
+ candidate = self.class.new(scaled, book: @book, width: @width, align: @align, leading: @leading * factor,
76
+ fallback_style: scaled.first&.style || @fallback_style)
77
+ return candidate if candidate.height <= max_height || scaled.all? { |run| run.style.size <= MIN_SHRINK_SIZE }
78
+ end
79
+ end
80
+
81
+ def scaled_runs(factor)
82
+ @runs.map { |run| Run.new(run.text, run.style.with(size: [run.style.size * factor, MIN_SHRINK_SIZE].max)) }
83
+ end
84
+
85
+ def draw_fragment(canvas, fragment, offset, top, line)
86
+ style = fragment.style
87
+ x = offset + fragment.x
88
+ canvas.text(fragment.text, x:, y: top + line.ascent, font: fragment.font, size: style.render_size,
89
+ color: style.color, letter_spacing: style.letter_spacing, rise: style.rise,
90
+ opacity: style.opacity, underline: style.underline,
91
+ strikethrough: style.strikethrough,
92
+ synthetic_bold: fragment.face.synthetic_bold,
93
+ synthetic_oblique: fragment.face.synthetic_oblique)
94
+ canvas.link(x, top, fragment.width, line.height, style.link) if style.link
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stationery
4
+ module Text
5
+ # Builds runs with Ruby instead of markup:
6
+ #
7
+ # text { plain "Total "; b "due"; link("https://…") { i "pay" } }
8
+ class RunsBuilder
9
+ def self.build(style, &)
10
+ new(style).tap { |builder| builder.capture(&) }.runs
11
+ end
12
+
13
+ def initialize(style)
14
+ @styles = [style]
15
+ @runs = []
16
+ end
17
+
18
+ def runs = Run.merge(@runs)
19
+
20
+ # A block taking an argument is called with the builder (so it keeps its
21
+ # own self and instance variables); one without is evaluated on it.
22
+ def capture(&block)
23
+ before = @runs.size
24
+ result = block.arity == 1 ? yield(self) : instance_exec(&block)
25
+ plain(result) if result.is_a?(String) && @runs.size == before
26
+ end
27
+
28
+ def plain(text) = @runs << Run.new(text.to_s, @styles.last)
29
+ def br = plain("\n")
30
+
31
+ def b(text = nil, &) = styled({ weight: :bold }, text, &)
32
+ def i(text = nil, &) = styled({ style: :italic }, text, &)
33
+ def u(text = nil, &) = styled({ underline: true }, text, &)
34
+ def strikethrough(text = nil, &) = styled({ strikethrough: true }, text, &)
35
+ def sub(text = nil, &) = styled({ script: :sub }, text, &)
36
+ def sup(text = nil, &) = styled({ script: :sup }, text, &)
37
+ def color(value, text = nil, &) = styled({ color: value }, text, &)
38
+ def size(value, text = nil, &) = styled({ size: value }, text, &)
39
+ def font(family, text = nil, &) = styled({ family: family.to_s }, text, &)
40
+ def link(url, text = nil, &) = styled({ link: url.to_s }, text, &)
41
+
42
+ private
43
+
44
+ def styled(overrides, text, &)
45
+ @styles << @styles.last.with(**overrides)
46
+ text ? plain(text) : capture(&)
47
+ ensure
48
+ @styles.pop
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stationery
4
+ module Text
5
+ SCRIPT_SCALE = 0.583
6
+
7
+ # How a run of text looks. Immutable; derive variants with #with.
8
+ Style = Data.define(:family, :size, :weight, :style, :color, :letter_spacing,
9
+ :underline, :strikethrough, :script, :link, :opacity) do
10
+ def initialize(family:, size: 10, weight: :regular, style: :normal, color: "#000000", letter_spacing: 0,
11
+ underline: false, strikethrough: false, script: nil, link: nil, opacity: nil)
12
+ super
13
+ end
14
+
15
+ # The size glyphs are drawn at: smaller for sub- and superscript.
16
+ def render_size
17
+ script ? size * SCRIPT_SCALE : size
18
+ end
19
+
20
+ # Baseline shift for sub- and superscript.
21
+ def rise
22
+ case script
23
+ when :sup then size * 0.33
24
+ when :sub then -size * 0.15
25
+ else 0
26
+ end
27
+ end
28
+ end
29
+
30
+ # A piece of text in one style.
31
+ Run = Data.define(:text, :style) do
32
+ # Joins neighbouring runs that share a style.
33
+ def self.merge(runs)
34
+ runs.each_with_object([]) do |run, merged|
35
+ next if run.text.empty?
36
+
37
+ if merged.last&.style == run.style
38
+ merged[-1] = Run.new(merged.last.text + run.text, run.style)
39
+ else
40
+ merged << run
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stationery
4
+ module Text
5
+ # Greedy line breaking over styled runs. Breaks at spaces (the space is
6
+ # dropped at the break) and after hyphens; a word wider than the line is
7
+ # broken between characters. A word that changes style midway ("<b>Tot</b>al")
8
+ # is still one word.
9
+ class Wrapper
10
+ TOKEN = /\n|[ \t]+|[^ \t\n-]*-+|[^ \t\n-]+/
11
+
12
+ Segment = Data.define(:text, :style)
13
+
14
+ def initialize(book)
15
+ @book = book
16
+ end
17
+
18
+ def wrap(runs, max_width, fallback_style: runs.first&.style)
19
+ @max = max_width
20
+ @lines = []
21
+ @current = []
22
+ @pending_space = []
23
+ @fallback = fallback_style
24
+ items(runs).each { |item| place(item) }
25
+ finish unless @current.empty? && @lines.any? && !@ended_with_newline
26
+ @lines
27
+ end
28
+
29
+ private
30
+
31
+ # [:word, segments] | [:space, segments] | [:newline]
32
+ def items(runs)
33
+ items = []
34
+ runs.each do |run|
35
+ run.text.scan(TOKEN) do |token|
36
+ if token == "\n" then items << [:newline]
37
+ elsif token.match?(/\A[ \t]/) then items << [:space, [Segment.new(token, run.style)]]
38
+ elsif items.last&.first == :word && !items.last.last.last.text.end_with?("-")
39
+ items.last.last << Segment.new(token, run.style)
40
+ else items << [:word, [Segment.new(token, run.style)]]
41
+ end
42
+ end
43
+ end
44
+ items
45
+ end
46
+
47
+ def place((kind, segments))
48
+ @ended_with_newline = kind == :newline
49
+ case kind
50
+ when :newline then finish
51
+ when :space then @pending_space += segments
52
+ else place_word(segments)
53
+ end
54
+ end
55
+
56
+ def place_word(word)
57
+ needed = width(@pending_space) + width(word)
58
+ if @current.empty? || line_width + needed <= @max + 0.0001
59
+ @current.concat(@pending_space, word)
60
+ else
61
+ finish(wrapped: true)
62
+ @current.concat(word)
63
+ end
64
+ @pending_space = []
65
+ break_long_word while line_width > @max + 0.0001 && characters(@current) > 1
66
+ end
67
+
68
+ # Moves characters that overflow the line onto following lines.
69
+ def break_long_word
70
+ fitting = []
71
+ @current.each do |segment|
72
+ segment.text.each_char do |char|
73
+ piece = Segment.new(char, segment.style)
74
+ return keep_overflow(fitting, piece) if fitting.any? && width(fitting + [piece]) > @max + 0.0001
75
+
76
+ fitting << piece
77
+ end
78
+ end
79
+ end
80
+
81
+ def keep_overflow(fitting, piece)
82
+ rest = remaining_after(fitting.size)
83
+ @current = fitting
84
+ finish(wrapped: true)
85
+ @current = [piece, *rest.drop(1)]
86
+ end
87
+
88
+ def remaining_after(count)
89
+ @current.flat_map { |segment| segment.text.chars.map { |char| Segment.new(char, segment.style) } }.drop(count)
90
+ end
91
+
92
+ def finish(wrapped: false)
93
+ segments = wrapped ? @current : @current + @pending_space
94
+ @lines << Line.new(fragments(trim_trailing(segments)), fallback_metrics)
95
+ @current = []
96
+ @pending_space = []
97
+ end
98
+
99
+ def trim_trailing(segments)
100
+ segments = segments.dup
101
+ segments.pop while segments.last && segments.last.text.strip.empty?
102
+ segments
103
+ end
104
+
105
+ def fragments(segments)
106
+ x = 0
107
+ segments.chunk_while { |a, b| a.style == b.style }.map do |group|
108
+ text = group.map(&:text).join
109
+ font, face = @book.resolve(group.first.style)
110
+ fragment_width = measure(text, group.first.style)
111
+ Fragment.new(text, group.first.style, font, face, fragment_width, x).tap { x += fragment_width }
112
+ end
113
+ end
114
+
115
+ def fallback_metrics
116
+ [@book.resolve(@fallback).first, @fallback.render_size]
117
+ end
118
+
119
+ def line_width = width(@current)
120
+ def characters(segments) = segments.sum { |segment| segment.text.length }
121
+
122
+ def width(segments)
123
+ segments.sum { |segment| measure(segment.text, segment.style) }
124
+ end
125
+
126
+ def measure(text, style)
127
+ @book.resolve(style).first.width_of(text, style.render_size, letter_spacing: style.letter_spacing)
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stationery
4
+ VERSION = "0.1.0"
5
+ end
data/lib/stationery.rb ADDED
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "stationery/version"
4
+ require_relative "stationery/errors"
5
+ require_relative "stationery/pdf/types"
6
+ require_relative "stationery/pdf/serializer"
7
+ require_relative "stationery/pdf/stream"
8
+ require_relative "stationery/pdf/writer"
9
+ require_relative "stationery/fonts/true_type"
10
+ require_relative "stationery/fonts/cmap"
11
+ require_relative "stationery/fonts/name_table"
12
+ require_relative "stationery/fonts/subset"
13
+ require_relative "stationery/fonts/registry"
14
+ require_relative "stationery/fonts/font"
15
+ require_relative "stationery/fonts/family"
16
+ require_relative "stationery/images/images"
17
+ require_relative "stationery/images/cache"
18
+ require_relative "stationery/images/scanlines"
19
+ require_relative "stationery/images/jpeg"
20
+ require_relative "stationery/images/png"
21
+ require_relative "stationery/color"
22
+ require_relative "stationery/geometry"
23
+ require_relative "stationery/page"
24
+ require_relative "stationery/resources"
25
+ require_relative "stationery/canvas/path"
26
+ require_relative "stationery/canvas/text"
27
+ require_relative "stationery/canvas"
28
+ require_relative "stationery/pdf/assembler"
29
+ require_relative "stationery/text/style"
30
+ require_relative "stationery/fonts/font_book"
31
+ require_relative "stationery/text/entities"
32
+ require_relative "stationery/text/markup"
33
+ require_relative "stationery/text/runs_builder"
34
+ require_relative "stationery/text/line"
35
+ require_relative "stationery/text/wrapper"
36
+ require_relative "stationery/text/paragraph"
37
+ require_relative "stationery/layout/node"
38
+ require_relative "stationery/layout/flow"
39
+ require_relative "stationery/layout/box"
40
+ require_relative "stationery/layout/row"
41
+ require_relative "stationery/layout/text"
42
+ require_relative "stationery/layout/image"
43
+ require_relative "stationery/layout/leaves"
44
+ require_relative "stationery/layout/table"
45
+ require_relative "stationery/layout/table/widths"
46
+ require_relative "stationery/layout/table/cell"
47
+ require_relative "stationery/layout/table/selection"
48
+ require_relative "stationery/layout/paginator"
49
+ require_relative "stationery/layout/positioned"
50
+ require_relative "stationery/builder"
51
+ require_relative "stationery/elements"
52
+ require_relative "stationery/component"
53
+ require_relative "stationery/page_templates"
54
+ require_relative "stationery/document"
55
+
56
+ # Pure-Ruby PDF documents built from Phlex-style components.
57
+ module Stationery
58
+ end
metadata ADDED
@@ -0,0 +1,108 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: stationery
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Mikael Henriksson
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: |
13
+ Stationery renders PDF documents from Phlex-style Ruby components. Describe
14
+ the page with rows, columns, boxes, tables, text and images; a box-layout
15
+ engine measures, places and paginates them, and a small PDF writer embeds
16
+ subsetted TrueType fonts and JPEG/PNG images. It has no runtime dependencies,
17
+ no native extensions and never starts another process — no Prawn, no
18
+ headless browser.
19
+ email:
20
+ - mikael@mhenrixon.com
21
+ executables: []
22
+ extensions: []
23
+ extra_rdoc_files: []
24
+ files:
25
+ - CHANGELOG.md
26
+ - LICENSE.txt
27
+ - README.md
28
+ - lib/stationery.rb
29
+ - lib/stationery/builder.rb
30
+ - lib/stationery/canvas.rb
31
+ - lib/stationery/canvas/path.rb
32
+ - lib/stationery/canvas/text.rb
33
+ - lib/stationery/color.rb
34
+ - lib/stationery/component.rb
35
+ - lib/stationery/document.rb
36
+ - lib/stationery/elements.rb
37
+ - lib/stationery/errors.rb
38
+ - lib/stationery/fonts/cmap.rb
39
+ - lib/stationery/fonts/family.rb
40
+ - lib/stationery/fonts/font.rb
41
+ - lib/stationery/fonts/font_book.rb
42
+ - lib/stationery/fonts/name_table.rb
43
+ - lib/stationery/fonts/registry.rb
44
+ - lib/stationery/fonts/subset.rb
45
+ - lib/stationery/fonts/true_type.rb
46
+ - lib/stationery/geometry.rb
47
+ - lib/stationery/images/cache.rb
48
+ - lib/stationery/images/images.rb
49
+ - lib/stationery/images/jpeg.rb
50
+ - lib/stationery/images/png.rb
51
+ - lib/stationery/images/scanlines.rb
52
+ - lib/stationery/layout/box.rb
53
+ - lib/stationery/layout/flow.rb
54
+ - lib/stationery/layout/image.rb
55
+ - lib/stationery/layout/leaves.rb
56
+ - lib/stationery/layout/node.rb
57
+ - lib/stationery/layout/paginator.rb
58
+ - lib/stationery/layout/positioned.rb
59
+ - lib/stationery/layout/row.rb
60
+ - lib/stationery/layout/table.rb
61
+ - lib/stationery/layout/table/cell.rb
62
+ - lib/stationery/layout/table/selection.rb
63
+ - lib/stationery/layout/table/widths.rb
64
+ - lib/stationery/layout/text.rb
65
+ - lib/stationery/page.rb
66
+ - lib/stationery/page_templates.rb
67
+ - lib/stationery/pdf/assembler.rb
68
+ - lib/stationery/pdf/serializer.rb
69
+ - lib/stationery/pdf/stream.rb
70
+ - lib/stationery/pdf/types.rb
71
+ - lib/stationery/pdf/writer.rb
72
+ - lib/stationery/rails.rb
73
+ - lib/stationery/resources.rb
74
+ - lib/stationery/text/entities.rb
75
+ - lib/stationery/text/line.rb
76
+ - lib/stationery/text/markup.rb
77
+ - lib/stationery/text/paragraph.rb
78
+ - lib/stationery/text/runs_builder.rb
79
+ - lib/stationery/text/style.rb
80
+ - lib/stationery/text/wrapper.rb
81
+ - lib/stationery/version.rb
82
+ homepage: https://github.com/zoolutions/stationery
83
+ licenses:
84
+ - MIT
85
+ metadata:
86
+ homepage_uri: https://github.com/zoolutions/stationery
87
+ source_code_uri: https://github.com/zoolutions/stationery/tree/main
88
+ changelog_uri: https://github.com/zoolutions/stationery/blob/main/CHANGELOG.md
89
+ rubygems_mfa_required: 'true'
90
+ rdoc_options: []
91
+ require_paths:
92
+ - lib
93
+ required_ruby_version: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: 3.4.0
98
+ required_rubygems_version: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ requirements: []
104
+ rubygems_version: 4.0.19
105
+ specification_version: 4
106
+ summary: 'Pure-Ruby, Phlex-style PDF documents: box layout, TrueType fonts, images.
107
+ No Prawn, no Chrome.'
108
+ test_files: []