stationery 0.1.0 → 0.2.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 +4 -4
- data/CHANGELOG.md +94 -1
- data/README.md +358 -25
- data/exe/stationery +6 -0
- data/lib/generators/stationery/fonts_generator.rb +24 -0
- data/lib/stationery/builder.rb +25 -3
- data/lib/stationery/canvas/debug.rb +26 -0
- data/lib/stationery/canvas/path.rb +22 -12
- data/lib/stationery/canvas/text.rb +8 -5
- data/lib/stationery/canvas.rb +25 -8
- data/lib/stationery/cli/fonts.rb +85 -0
- data/lib/stationery/cli/render.rb +137 -0
- data/lib/stationery/cli.rb +74 -0
- data/lib/stationery/document.rb +53 -9
- data/lib/stationery/elements/lists.rb +58 -0
- data/lib/stationery/elements/rich.rb +27 -0
- data/lib/stationery/elements.rb +88 -12
- data/lib/stationery/errors.rb +13 -1
- data/lib/stationery/fonts/bundled.rb +25 -0
- data/lib/stationery/fonts/catalog.rb +101 -0
- data/lib/stationery/fonts/cff.rb +140 -0
- data/lib/stationery/fonts/cff_subset.rb +145 -0
- data/lib/stationery/fonts/data/Inter-Bold.ttf +0 -0
- data/lib/stationery/fonts/data/Inter-BoldItalic.ttf +0 -0
- data/lib/stationery/fonts/data/Inter-Italic.ttf +0 -0
- data/lib/stationery/fonts/data/Inter-Regular.ttf +0 -0
- data/lib/stationery/fonts/data/OFL.txt +92 -0
- data/lib/stationery/fonts/data/README.md +14 -0
- data/lib/stationery/fonts/embedding/base.rb +56 -0
- data/lib/stationery/fonts/embedding/cff.rb +35 -0
- data/lib/stationery/fonts/embedding/true_type.rb +35 -0
- data/lib/stationery/fonts/fallback.rb +58 -0
- data/lib/stationery/fonts/family.rb +11 -0
- data/lib/stationery/fonts/font.rb +52 -88
- data/lib/stationery/fonts/font_book.rb +46 -8
- data/lib/stationery/fonts/glyph_run.rb +38 -0
- data/lib/stationery/fonts/gpos/class_def.rb +25 -0
- data/lib/stationery/fonts/gpos/coverage.rb +20 -0
- data/lib/stationery/fonts/gpos/pair_pos.rb +83 -0
- data/lib/stationery/fonts/gpos.rb +62 -0
- data/lib/stationery/fonts/installer.rb +121 -0
- data/lib/stationery/fonts/kern_table.rb +52 -0
- data/lib/stationery/fonts/kerning.rb +17 -0
- data/lib/stationery/fonts/packs.rb +45 -0
- data/lib/stationery/fonts/sfnt_metrics.rb +51 -0
- data/lib/stationery/fonts/to_unicode.rb +36 -0
- data/lib/stationery/fonts/true_type.rb +32 -49
- data/lib/stationery/html/document.rb +11 -0
- data/lib/stationery/html/tokenizer.rb +74 -0
- data/lib/stationery/html/tree_builder/blocks.rb +137 -0
- data/lib/stationery/html/tree_builder/collector.rb +50 -0
- data/lib/stationery/html/tree_builder.rb +72 -0
- data/lib/stationery/html/whitespace.rb +43 -0
- data/lib/stationery/layout/box.rb +85 -16
- data/lib/stationery/layout/flow.rb +48 -15
- data/lib/stationery/layout/image.rb +1 -0
- data/lib/stationery/layout/list_item.rb +89 -0
- data/lib/stationery/layout/mark.rb +58 -0
- data/lib/stationery/layout/node.rb +26 -2
- data/lib/stationery/layout/paginator.rb +35 -6
- data/lib/stationery/layout/positioned.rb +3 -1
- data/lib/stationery/layout/row.rb +34 -4
- data/lib/stationery/layout/svg.rb +45 -0
- data/lib/stationery/layout/table/cell.rb +33 -6
- data/lib/stationery/layout/table/grid.rb +93 -0
- data/lib/stationery/layout/table/row_splitter.rb +36 -0
- data/lib/stationery/layout/table/selection.rb +3 -2
- data/lib/stationery/layout/table.rb +69 -23
- data/lib/stationery/layout/table_of_contents/entry.rb +56 -0
- data/lib/stationery/layout/table_of_contents.rb +43 -0
- data/lib/stationery/layout/text.rb +5 -4
- data/lib/stationery/layout/wrap.rb +78 -0
- data/lib/stationery/list_markers.rb +47 -0
- data/lib/stationery/markdown/block_parser/containers.rb +94 -0
- data/lib/stationery/markdown/block_parser/tables.rb +54 -0
- data/lib/stationery/markdown/block_parser.rb +141 -0
- data/lib/stationery/markdown/document.rb +50 -0
- data/lib/stationery/markdown/inline_parser/emphasis.rb +80 -0
- data/lib/stationery/markdown/inline_parser/links.rb +70 -0
- data/lib/stationery/markdown/inline_parser/nodes.rb +39 -0
- data/lib/stationery/markdown/inline_parser.rb +89 -0
- data/lib/stationery/minitest.rb +33 -0
- data/lib/stationery/outline.rb +37 -0
- data/lib/stationery/page.rb +18 -4
- data/lib/stationery/page_templates.rb +33 -10
- data/lib/stationery/pdf/assembler.rb +18 -7
- data/lib/stationery/pdf/outline_writer.rb +72 -0
- data/lib/stationery/pdf/serializer.rb +5 -1
- data/lib/stationery/pdf/writer.rb +4 -0
- data/lib/stationery/preview.rb +64 -0
- data/lib/stationery/rails/previews_controller.rb +61 -0
- data/lib/stationery/rails.rb +2 -0
- data/lib/stationery/railtie.rb +60 -0
- data/lib/stationery/regions.rb +67 -0
- data/lib/stationery/rich/nodes.rb +55 -0
- data/lib/stationery/rich/renderer/inlines.rb +53 -0
- data/lib/stationery/rich/renderer.rb +121 -0
- data/lib/stationery/rich/styles.rb +34 -0
- data/lib/stationery/rspec.rb +6 -0
- data/lib/stationery/structure.rb +78 -0
- data/lib/stationery/svg/arc.rb +91 -0
- data/lib/stationery/svg/document.rb +73 -0
- data/lib/stationery/svg/parser.rb +38 -0
- data/lib/stationery/svg/path_data.rb +137 -0
- data/lib/stationery/svg/shapes.rb +54 -0
- data/lib/stationery/svg/style.rb +68 -0
- data/lib/stationery/svg/transform.rb +43 -0
- data/lib/stationery/testing/inspector.rb +98 -0
- data/lib/stationery/testing/matchers.rb +123 -0
- data/lib/stationery/text/entities/html4.rb +49 -0
- data/lib/stationery/text/entities.rb +6 -1
- data/lib/stationery/text/line.rb +8 -2
- data/lib/stationery/text/paragraph.rb +32 -11
- data/lib/stationery/text/runs_builder.rb +9 -1
- data/lib/stationery/text/style.rb +2 -2
- data/lib/stationery/text/wrapper.rb +3 -2
- data/lib/stationery/version.rb +1 -1
- data/lib/stationery/warnings.rb +65 -0
- data/lib/stationery.rb +51 -0
- metadata +85 -4
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "inspector"
|
|
4
|
+
|
|
5
|
+
module Stationery
|
|
6
|
+
module Testing
|
|
7
|
+
# Framework-independent PDF matchers: each answers matches?(subject),
|
|
8
|
+
# failure_message, failure_message_when_negated and description, so RSpec
|
|
9
|
+
# uses them directly and Minitest assertions wrap them.
|
|
10
|
+
module Matchers
|
|
11
|
+
EXCERPT = 500
|
|
12
|
+
|
|
13
|
+
class Base
|
|
14
|
+
def initialize(expected = nil)
|
|
15
|
+
@expected = expected
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def matches?(subject)
|
|
19
|
+
@inspector = subject.is_a?(Inspector) ? subject : Inspector.new(subject)
|
|
20
|
+
match?(@inspector)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def failure_message = "expected PDF to #{description}, #{actual}"
|
|
24
|
+
def failure_message_when_negated = "expected PDF not to #{description}, #{actual}"
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def show(expected) = expected.is_a?(Regexp) ? expected.inspect : expected.to_s.inspect
|
|
29
|
+
|
|
30
|
+
def contains?(haystack, expected)
|
|
31
|
+
expected.is_a?(Regexp) ? expected.match?(haystack) : haystack.include?(expected.to_s)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def excerpt(text)
|
|
35
|
+
text.length > EXCERPT ? "#{text[0, EXCERPT]}…" : text
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
class HaveText < Base
|
|
40
|
+
def description = "have text #{show(@expected)}"
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def match?(pdf) = contains?(pdf.text, @expected)
|
|
45
|
+
def actual = "got text:\n#{excerpt(@inspector.text)}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
class HaveTextOnPage < Base
|
|
49
|
+
def initialize(page, expected)
|
|
50
|
+
super(expected)
|
|
51
|
+
@page = page
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def description = "have text #{show(@expected)} on page #{@page}"
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def page_text = @inspector.page_texts.fetch(@page - 1, nil)
|
|
59
|
+
def match?(_pdf) = !page_text.nil? && contains?(page_text, @expected)
|
|
60
|
+
|
|
61
|
+
def actual
|
|
62
|
+
return "but it has #{@inspector.page_count} page(s)" if page_text.nil?
|
|
63
|
+
|
|
64
|
+
"got text on page #{@page}:\n#{excerpt(page_text)}"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
class HavePageCount < Base
|
|
69
|
+
def description = "have #{@expected} page(s)"
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def match?(pdf) = pdf.page_count == @expected
|
|
74
|
+
def actual = "got #{@inspector.page_count}"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
class HaveLink < Base
|
|
78
|
+
def description = "have a link to #{show(@expected)}"
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def match?(pdf) = pdf.links.any? { |url| @expected.is_a?(Regexp) ? @expected.match?(url) : url == @expected }
|
|
83
|
+
def actual = "got links #{@inspector.links.inspect}"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
class HaveImageCount < Base
|
|
87
|
+
def description = "have #{@expected} image(s)"
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def match?(pdf) = pdf.image_count == @expected
|
|
92
|
+
def actual = "got #{@inspector.image_count}"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
class HaveBookmark < Base
|
|
96
|
+
def description = "have a bookmark #{show(@expected)}"
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
def match?(pdf) = pdf.bookmarks.include?(@expected)
|
|
101
|
+
def actual = "got bookmarks #{@inspector.bookmarks.inspect}"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
class HaveNoWarnings < Base
|
|
105
|
+
def description = "have no warnings"
|
|
106
|
+
|
|
107
|
+
private
|
|
108
|
+
|
|
109
|
+
def match?(pdf) = pdf.warnings.empty?
|
|
110
|
+
def messages = @inspector.warnings.map(&:message)
|
|
111
|
+
def actual = messages.empty? ? "got none" : "got:\n#{messages.map { |m| " - #{m}" }.join("\n")}"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def have_pdf_text(expected) = HaveText.new(expected)
|
|
115
|
+
def have_pdf_text_on_page(page, expected) = HaveTextOnPage.new(page, expected)
|
|
116
|
+
def have_page_count(expected) = HavePageCount.new(expected)
|
|
117
|
+
def have_pdf_link(expected) = HaveLink.new(expected)
|
|
118
|
+
def have_image_count(expected) = HaveImageCount.new(expected)
|
|
119
|
+
def have_bookmark(title) = HaveBookmark.new(title)
|
|
120
|
+
def have_no_warnings = HaveNoWarnings.new
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stationery
|
|
4
|
+
module Text
|
|
5
|
+
module Entities
|
|
6
|
+
# The 252 named character references of HTML 4.
|
|
7
|
+
HTML4 = { # rubocop:disable Metrics/CollectionLiteralLength
|
|
8
|
+
"AElig" => "Æ", "Aacute" => "Á", "Acirc" => "Â", "Agrave" => "À", "Alpha" => "Α", "Aring" => "Å",
|
|
9
|
+
"Atilde" => "Ã", "Auml" => "Ä", "Beta" => "Β", "Ccedil" => "Ç", "Chi" => "Χ", "Dagger" => "‡", "Delta" => "Δ",
|
|
10
|
+
"ETH" => "Ð", "Eacute" => "É", "Ecirc" => "Ê", "Egrave" => "È", "Epsilon" => "Ε", "Eta" => "Η", "Euml" => "Ë",
|
|
11
|
+
"Gamma" => "Γ", "Iacute" => "Í", "Icirc" => "Î", "Igrave" => "Ì", "Iota" => "Ι", "Iuml" => "Ï", "Kappa" => "Κ",
|
|
12
|
+
"Lambda" => "Λ", "Mu" => "Μ", "Ntilde" => "Ñ", "Nu" => "Ν", "OElig" => "Œ", "Oacute" => "Ó", "Ocirc" => "Ô",
|
|
13
|
+
"Ograve" => "Ò", "Omega" => "Ω", "Omicron" => "Ο", "Oslash" => "Ø", "Otilde" => "Õ", "Ouml" => "Ö",
|
|
14
|
+
"Phi" => "Φ", "Pi" => "Π", "Prime" => "″", "Psi" => "Ψ", "Rho" => "Ρ", "Scaron" => "Š", "Sigma" => "Σ",
|
|
15
|
+
"THORN" => "Þ", "Tau" => "Τ", "Theta" => "Θ", "Uacute" => "Ú", "Ucirc" => "Û", "Ugrave" => "Ù",
|
|
16
|
+
"Upsilon" => "Υ", "Uuml" => "Ü", "Xi" => "Ξ", "Yacute" => "Ý", "Yuml" => "Ÿ", "Zeta" => "Ζ", "aacute" => "á",
|
|
17
|
+
"acirc" => "â", "acute" => "´", "aelig" => "æ", "agrave" => "à", "alefsym" => "ℵ", "alpha" => "α", "amp" => "&",
|
|
18
|
+
"and" => "∧", "ang" => "∠", "aring" => "å", "asymp" => "≈", "atilde" => "ã", "auml" => "ä", "bdquo" => "„",
|
|
19
|
+
"beta" => "β", "brvbar" => "¦", "bull" => "•", "cap" => "∩", "ccedil" => "ç", "cedil" => "¸", "cent" => "¢",
|
|
20
|
+
"chi" => "χ", "circ" => "ˆ", "clubs" => "♣", "cong" => "≅", "copy" => "©", "crarr" => "↵", "cup" => "∪",
|
|
21
|
+
"curren" => "¤", "dArr" => "⇓", "dagger" => "†", "darr" => "↓", "deg" => "°", "delta" => "δ", "diams" => "♦",
|
|
22
|
+
"divide" => "÷", "eacute" => "é", "ecirc" => "ê", "egrave" => "è", "empty" => "∅", "emsp" => "\u2003",
|
|
23
|
+
"ensp" => "\u2002", "epsilon" => "ε", "equiv" => "≡", "eta" => "η", "eth" => "ð", "euml" => "ë", "euro" => "€",
|
|
24
|
+
"exist" => "∃", "fnof" => "ƒ", "forall" => "∀", "frac12" => "½", "frac14" => "¼", "frac34" => "¾",
|
|
25
|
+
"frasl" => "⁄", "gamma" => "γ", "ge" => "≥", "gt" => ">", "hArr" => "⇔", "harr" => "↔", "hearts" => "♥",
|
|
26
|
+
"hellip" => "…", "iacute" => "í", "icirc" => "î", "iexcl" => "¡", "igrave" => "ì", "image" => "ℑ",
|
|
27
|
+
"infin" => "∞", "int" => "∫", "iota" => "ι", "iquest" => "¿", "isin" => "∈", "iuml" => "ï", "kappa" => "κ",
|
|
28
|
+
"lArr" => "⇐", "lambda" => "λ", "lang" => "〈", "laquo" => "«", "larr" => "←", "lceil" => "⌈", "ldquo" => "“",
|
|
29
|
+
"le" => "≤", "lfloor" => "⌊", "lowast" => "∗", "loz" => "◊", "lrm" => "\u200E", "lsaquo" => "‹", "lsquo" => "‘",
|
|
30
|
+
"lt" => "<", "macr" => "¯", "mdash" => "—", "micro" => "µ", "middot" => "·", "minus" => "−", "mu" => "μ",
|
|
31
|
+
"nabla" => "∇", "nbsp" => "\u00A0", "ndash" => "–", "ne" => "≠", "ni" => "∋", "not" => "¬", "notin" => "∉",
|
|
32
|
+
"nsub" => "⊄", "ntilde" => "ñ", "nu" => "ν", "oacute" => "ó", "ocirc" => "ô", "oelig" => "œ", "ograve" => "ò",
|
|
33
|
+
"oline" => "‾", "omega" => "ω", "omicron" => "ο", "oplus" => "⊕", "or" => "∨", "ordf" => "ª", "ordm" => "º",
|
|
34
|
+
"oslash" => "ø", "otilde" => "õ", "otimes" => "⊗", "ouml" => "ö", "para" => "¶", "part" => "∂", "permil" => "‰",
|
|
35
|
+
"perp" => "⊥", "phi" => "φ", "pi" => "π", "piv" => "ϖ", "plusmn" => "±", "pound" => "£", "prime" => "′",
|
|
36
|
+
"prod" => "∏", "prop" => "∝", "psi" => "ψ", "quot" => "\"", "rArr" => "⇒", "radic" => "√", "rang" => "〉",
|
|
37
|
+
"raquo" => "»", "rarr" => "→", "rceil" => "⌉", "rdquo" => "”", "real" => "ℜ", "reg" => "®", "rfloor" => "⌋",
|
|
38
|
+
"rho" => "ρ", "rlm" => "\u200F", "rsaquo" => "›", "rsquo" => "’", "sbquo" => "‚", "scaron" => "š",
|
|
39
|
+
"sdot" => "⋅", "sect" => "§", "shy" => "\u00AD", "sigma" => "σ", "sigmaf" => "ς", "sim" => "∼", "spades" => "♠",
|
|
40
|
+
"sub" => "⊂", "sube" => "⊆", "sum" => "∑", "sup" => "⊃", "sup1" => "¹", "sup2" => "²", "sup3" => "³",
|
|
41
|
+
"supe" => "⊇", "szlig" => "ß", "tau" => "τ", "there4" => "∴", "theta" => "θ", "thetasym" => "ϑ",
|
|
42
|
+
"thinsp" => "\u2009", "thorn" => "þ", "tilde" => "˜", "times" => "×", "trade" => "™", "uArr" => "⇑",
|
|
43
|
+
"uacute" => "ú", "uarr" => "↑", "ucirc" => "û", "ugrave" => "ù", "uml" => "¨", "upsih" => "ϒ", "upsilon" => "υ",
|
|
44
|
+
"uuml" => "ü", "weierp" => "℘", "xi" => "ξ", "yacute" => "ý", "yen" => "¥", "yuml" => "ÿ", "zeta" => "ζ",
|
|
45
|
+
"zwj" => "\u200D", "zwnj" => "\u200C"
|
|
46
|
+
}.freeze
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -14,10 +14,15 @@ module Stationery
|
|
|
14
14
|
name = Regexp.last_match(1)
|
|
15
15
|
if name.start_with?("#x") then [name[2..].to_i(16)].pack("U")
|
|
16
16
|
elsif name.start_with?("#") then [name[1..].to_i].pack("U")
|
|
17
|
-
else NAMED.fetch(name, entity)
|
|
17
|
+
else NAMED.fetch(name) { html4.fetch(name, entity) }
|
|
18
18
|
end
|
|
19
19
|
end
|
|
20
20
|
end
|
|
21
|
+
|
|
22
|
+
def html4
|
|
23
|
+
require_relative "entities/html4" unless defined?(HTML4)
|
|
24
|
+
HTML4
|
|
25
|
+
end
|
|
21
26
|
end
|
|
22
27
|
end
|
|
23
28
|
end
|
data/lib/stationery/text/line.rb
CHANGED
|
@@ -7,12 +7,14 @@ module Stationery
|
|
|
7
7
|
Fragment = Data.define(:text, :style, :font, :face, :width, :x)
|
|
8
8
|
|
|
9
9
|
# One laid-out line. Vertical metrics come from its tallest fragment (or
|
|
10
|
-
# from the paragraph's base style when the line is empty).
|
|
10
|
+
# from the paragraph's base style when the line is empty). A soft-wrapped
|
|
11
|
+
# line is justifiable; the last line and one ended by a newline are not.
|
|
11
12
|
class Line
|
|
12
13
|
attr_reader :fragments, :width, :ascent, :descent, :height
|
|
13
14
|
|
|
14
|
-
def initialize(fragments, fallback)
|
|
15
|
+
def initialize(fragments, fallback, justifiable: false)
|
|
15
16
|
@fragments = fragments
|
|
17
|
+
@justifiable = justifiable
|
|
16
18
|
@width = fragments.sum(&:width)
|
|
17
19
|
metrics = fragments.empty? ? [fallback] : fragments.map { |f| [f.font, f.style.render_size] }
|
|
18
20
|
@ascent = metrics.map { |font, size| font.ascender(size) }.max
|
|
@@ -21,6 +23,10 @@ module Stationery
|
|
|
21
23
|
end
|
|
22
24
|
|
|
23
25
|
def text = fragments.map(&:text).join
|
|
26
|
+
def justifiable? = @justifiable
|
|
27
|
+
|
|
28
|
+
# U+0020 spaces only: justification stretches those, never tabs.
|
|
29
|
+
def space_count = fragments.sum { |fragment| fragment.text.count(" ") }
|
|
24
30
|
end
|
|
25
31
|
end
|
|
26
32
|
end
|
|
@@ -10,13 +10,13 @@ module Stationery
|
|
|
10
10
|
attr_reader :runs, :lines, :width, :align, :leading
|
|
11
11
|
|
|
12
12
|
def initialize(runs, book:, width:, align: :left, leading: 0, lines: nil, fallback_style: nil)
|
|
13
|
-
@runs = runs
|
|
13
|
+
@runs = lines ? runs : book.fallback(runs)
|
|
14
14
|
@book = book
|
|
15
15
|
@width = width
|
|
16
16
|
@align = align
|
|
17
17
|
@leading = leading
|
|
18
18
|
@fallback_style = fallback_style || runs.first&.style
|
|
19
|
-
@lines = lines || Wrapper.new(book).wrap(runs, width, fallback_style: @fallback_style)
|
|
19
|
+
@lines = lines || Wrapper.new(book).wrap(@runs, width, fallback_style: @fallback_style)
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def height
|
|
@@ -47,7 +47,12 @@ module Stationery
|
|
|
47
47
|
top = y
|
|
48
48
|
@lines.each do |line|
|
|
49
49
|
offset = x + Geometry.align_offset(@align, @width, line.width)
|
|
50
|
-
|
|
50
|
+
extra = word_spacing(line)
|
|
51
|
+
spaces = 0
|
|
52
|
+
line.fragments.each do |fragment|
|
|
53
|
+
draw_fragment(canvas, fragment, offset + (extra * spaces), top, line, extra)
|
|
54
|
+
spaces += fragment.text.count(" ")
|
|
55
|
+
end
|
|
51
56
|
top += line.height + @leading
|
|
52
57
|
end
|
|
53
58
|
end
|
|
@@ -82,16 +87,32 @@ module Stationery
|
|
|
82
87
|
@runs.map { |run| Run.new(run.text, run.style.with(size: [run.style.size * factor, MIN_SHRINK_SIZE].max)) }
|
|
83
88
|
end
|
|
84
89
|
|
|
85
|
-
|
|
90
|
+
# Extra points per space that stretch a justifiable line to the width.
|
|
91
|
+
def word_spacing(line)
|
|
92
|
+
return 0 unless @align == :justify && line.justifiable? && line.space_count.positive?
|
|
93
|
+
|
|
94
|
+
[(@width - line.width) / line.space_count, 0].max
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def draw_fragment(canvas, fragment, offset, top, line, word_spacing)
|
|
86
98
|
style = fragment.style
|
|
99
|
+
count_missing(fragment)
|
|
87
100
|
x = offset + fragment.x
|
|
88
|
-
canvas.text(fragment.text, x:, y: top + line.ascent, font: fragment.font, size: style.render_size,
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
canvas.link(x, top,
|
|
101
|
+
width = canvas.text(fragment.text, x:, y: top + line.ascent, font: fragment.font, size: style.render_size,
|
|
102
|
+
color: style.color, letter_spacing: style.letter_spacing, rise: style.rise,
|
|
103
|
+
opacity: style.opacity, underline: style.underline,
|
|
104
|
+
strikethrough: style.strikethrough, kerning: style.kerning,
|
|
105
|
+
synthetic_bold: fragment.face.synthetic_bold,
|
|
106
|
+
synthetic_oblique: fragment.face.synthetic_oblique, word_spacing:)
|
|
107
|
+
canvas.link(x, top, width, line.height, style.link) if style.link
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def count_missing(fragment)
|
|
111
|
+
fragment.text.each_char do |char|
|
|
112
|
+
next if Fonts::Fallback.carried?(char) || fragment.font.glyph?(char)
|
|
113
|
+
|
|
114
|
+
@book.warnings.missing_glyph(char, fragment.style.family)
|
|
115
|
+
end
|
|
95
116
|
end
|
|
96
117
|
end
|
|
97
118
|
end
|
|
@@ -19,9 +19,17 @@ module Stationery
|
|
|
19
19
|
|
|
20
20
|
# A block taking an argument is called with the builder (so it keeps its
|
|
21
21
|
# own self and instance variables); one without is evaluated on it.
|
|
22
|
+
# Once a block has taken the builder as an argument, nested blocks are
|
|
23
|
+
# called as closures too, so `t.link(url) { t.color(tone, "go") }` can
|
|
24
|
+
# still reach the caller's methods.
|
|
22
25
|
def capture(&block)
|
|
23
26
|
before = @runs.size
|
|
24
|
-
result = block.arity == 1
|
|
27
|
+
result = if block.arity == 1 || @yielding
|
|
28
|
+
@yielding = true
|
|
29
|
+
block.arity.zero? ? yield : yield(self)
|
|
30
|
+
else
|
|
31
|
+
instance_exec(&block)
|
|
32
|
+
end
|
|
25
33
|
plain(result) if result.is_a?(String) && @runs.size == before
|
|
26
34
|
end
|
|
27
35
|
|
|
@@ -6,9 +6,9 @@ module Stationery
|
|
|
6
6
|
|
|
7
7
|
# How a run of text looks. Immutable; derive variants with #with.
|
|
8
8
|
Style = Data.define(:family, :size, :weight, :style, :color, :letter_spacing,
|
|
9
|
-
:underline, :strikethrough, :script, :link, :opacity) do
|
|
9
|
+
:underline, :strikethrough, :script, :link, :opacity, :kerning) do
|
|
10
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)
|
|
11
|
+
underline: false, strikethrough: false, script: nil, link: nil, opacity: nil, kerning: true)
|
|
12
12
|
super
|
|
13
13
|
end
|
|
14
14
|
|
|
@@ -91,7 +91,7 @@ module Stationery
|
|
|
91
91
|
|
|
92
92
|
def finish(wrapped: false)
|
|
93
93
|
segments = wrapped ? @current : @current + @pending_space
|
|
94
|
-
@lines << Line.new(fragments(trim_trailing(segments)), fallback_metrics)
|
|
94
|
+
@lines << Line.new(fragments(trim_trailing(segments)), fallback_metrics, justifiable: wrapped)
|
|
95
95
|
@current = []
|
|
96
96
|
@pending_space = []
|
|
97
97
|
end
|
|
@@ -124,7 +124,8 @@ module Stationery
|
|
|
124
124
|
end
|
|
125
125
|
|
|
126
126
|
def measure(text, style)
|
|
127
|
-
@book.resolve(style).first
|
|
127
|
+
font = @book.resolve(style).first
|
|
128
|
+
font.width_of(text, style.render_size, letter_spacing: style.letter_spacing, kerning: style.kerning)
|
|
128
129
|
end
|
|
129
130
|
end
|
|
130
131
|
end
|
data/lib/stationery/version.rb
CHANGED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stationery
|
|
4
|
+
# Everything a render noticed but did not raise on. One collector per
|
|
5
|
+
# render; equal warnings are kept once and missing glyphs are counted per
|
|
6
|
+
# character and family.
|
|
7
|
+
class Warnings
|
|
8
|
+
include Enumerable
|
|
9
|
+
|
|
10
|
+
Overflow = Layout::Overflow
|
|
11
|
+
|
|
12
|
+
MissingGlyph = Data.define(:char, :family, :count) do
|
|
13
|
+
def message
|
|
14
|
+
times = count == 1 ? "once" : "#{count} times"
|
|
15
|
+
format('missing glyph "%<char>s" (U+%<code>04X) in %<family>s, drawn %<times>s as .notdef',
|
|
16
|
+
char:, code: char.ord, family:, times:)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
UnknownFamily = Data.define(:requested, :used) do
|
|
21
|
+
def message = %(font family "#{requested}" is not registered, using "#{used}")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
UnsupportedSvg = Data.define(:elements, :source) do
|
|
25
|
+
def message = %(SVG "#{source}" uses unsupported elements: #{elements.join(", ")})
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
SkippedImage = Data.define(:source, :reason) do
|
|
29
|
+
def message = %(image "#{source}" skipped: #{reason})
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
UnresolvedLink = Data.define(:name, :page) do
|
|
33
|
+
def message = %(link to "#{name}" on page #{page} has no matching anchor)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
DuplicateAnchor = Data.define(:name, :page) do
|
|
37
|
+
def message = %(anchor "#{name}" on page #{page} is already defined)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def initialize
|
|
41
|
+
@items = []
|
|
42
|
+
@glyphs = Hash.new(0)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def <<(warning)
|
|
46
|
+
@items << warning unless @items.include?(warning)
|
|
47
|
+
self
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def missing_glyph(char, family)
|
|
51
|
+
@glyphs[[char, family]] += 1
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def each(&)
|
|
55
|
+
return enum_for(:each) unless block_given?
|
|
56
|
+
|
|
57
|
+
@items.each(&)
|
|
58
|
+
@glyphs.each { |(char, family), count| yield MissingGlyph.new(char:, family:, count:) }
|
|
59
|
+
self
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def size = @items.size + @glyphs.size
|
|
63
|
+
def empty? = size.zero?
|
|
64
|
+
end
|
|
65
|
+
end
|
data/lib/stationery.rb
CHANGED
|
@@ -6,13 +6,31 @@ require_relative "stationery/pdf/types"
|
|
|
6
6
|
require_relative "stationery/pdf/serializer"
|
|
7
7
|
require_relative "stationery/pdf/stream"
|
|
8
8
|
require_relative "stationery/pdf/writer"
|
|
9
|
+
require_relative "stationery/fonts/sfnt_metrics"
|
|
9
10
|
require_relative "stationery/fonts/true_type"
|
|
11
|
+
require_relative "stationery/fonts/cff"
|
|
12
|
+
require_relative "stationery/fonts/cff_subset"
|
|
10
13
|
require_relative "stationery/fonts/cmap"
|
|
11
14
|
require_relative "stationery/fonts/name_table"
|
|
15
|
+
require_relative "stationery/fonts/kern_table"
|
|
16
|
+
require_relative "stationery/fonts/gpos"
|
|
17
|
+
require_relative "stationery/fonts/gpos/coverage"
|
|
18
|
+
require_relative "stationery/fonts/gpos/class_def"
|
|
19
|
+
require_relative "stationery/fonts/gpos/pair_pos"
|
|
20
|
+
require_relative "stationery/fonts/kerning"
|
|
12
21
|
require_relative "stationery/fonts/subset"
|
|
13
22
|
require_relative "stationery/fonts/registry"
|
|
23
|
+
require_relative "stationery/fonts/glyph_run"
|
|
24
|
+
require_relative "stationery/fonts/to_unicode"
|
|
25
|
+
require_relative "stationery/fonts/embedding/base"
|
|
26
|
+
require_relative "stationery/fonts/embedding/true_type"
|
|
27
|
+
require_relative "stationery/fonts/embedding/cff"
|
|
14
28
|
require_relative "stationery/fonts/font"
|
|
15
29
|
require_relative "stationery/fonts/family"
|
|
30
|
+
require_relative "stationery/fonts/bundled"
|
|
31
|
+
require_relative "stationery/fonts/catalog"
|
|
32
|
+
require_relative "stationery/fonts/packs"
|
|
33
|
+
require_relative "stationery/fonts/installer"
|
|
16
34
|
require_relative "stationery/images/images"
|
|
17
35
|
require_relative "stationery/images/cache"
|
|
18
36
|
require_relative "stationery/images/scanlines"
|
|
@@ -24,9 +42,12 @@ require_relative "stationery/page"
|
|
|
24
42
|
require_relative "stationery/resources"
|
|
25
43
|
require_relative "stationery/canvas/path"
|
|
26
44
|
require_relative "stationery/canvas/text"
|
|
45
|
+
require_relative "stationery/canvas/debug"
|
|
27
46
|
require_relative "stationery/canvas"
|
|
47
|
+
require_relative "stationery/pdf/outline_writer"
|
|
28
48
|
require_relative "stationery/pdf/assembler"
|
|
29
49
|
require_relative "stationery/text/style"
|
|
50
|
+
require_relative "stationery/fonts/fallback"
|
|
30
51
|
require_relative "stationery/fonts/font_book"
|
|
31
52
|
require_relative "stationery/text/entities"
|
|
32
53
|
require_relative "stationery/text/markup"
|
|
@@ -35,24 +56,54 @@ require_relative "stationery/text/line"
|
|
|
35
56
|
require_relative "stationery/text/wrapper"
|
|
36
57
|
require_relative "stationery/text/paragraph"
|
|
37
58
|
require_relative "stationery/layout/node"
|
|
59
|
+
require_relative "stationery/warnings"
|
|
38
60
|
require_relative "stationery/layout/flow"
|
|
39
61
|
require_relative "stationery/layout/box"
|
|
40
62
|
require_relative "stationery/layout/row"
|
|
41
63
|
require_relative "stationery/layout/text"
|
|
42
64
|
require_relative "stationery/layout/image"
|
|
43
65
|
require_relative "stationery/layout/leaves"
|
|
66
|
+
require_relative "stationery/layout/mark"
|
|
67
|
+
require_relative "stationery/layout/table_of_contents"
|
|
68
|
+
require_relative "stationery/layout/table_of_contents/entry"
|
|
44
69
|
require_relative "stationery/layout/table"
|
|
45
70
|
require_relative "stationery/layout/table/widths"
|
|
46
71
|
require_relative "stationery/layout/table/cell"
|
|
72
|
+
require_relative "stationery/layout/table/grid"
|
|
47
73
|
require_relative "stationery/layout/table/selection"
|
|
74
|
+
require_relative "stationery/layout/table/row_splitter"
|
|
48
75
|
require_relative "stationery/layout/paginator"
|
|
76
|
+
require_relative "stationery/svg/path_data"
|
|
77
|
+
require_relative "stationery/svg/arc"
|
|
78
|
+
require_relative "stationery/svg/parser"
|
|
79
|
+
require_relative "stationery/svg/transform"
|
|
80
|
+
require_relative "stationery/svg/style"
|
|
81
|
+
require_relative "stationery/svg/shapes"
|
|
82
|
+
require_relative "stationery/svg/document"
|
|
83
|
+
require_relative "stationery/layout/svg"
|
|
84
|
+
require_relative "stationery/layout/wrap"
|
|
49
85
|
require_relative "stationery/layout/positioned"
|
|
86
|
+
require_relative "stationery/layout/list_item"
|
|
87
|
+
require_relative "stationery/list_markers"
|
|
88
|
+
require_relative "stationery/outline"
|
|
50
89
|
require_relative "stationery/builder"
|
|
51
90
|
require_relative "stationery/elements"
|
|
91
|
+
require_relative "stationery/elements/lists"
|
|
92
|
+
require_relative "stationery/elements/rich"
|
|
52
93
|
require_relative "stationery/component"
|
|
94
|
+
require_relative "stationery/regions"
|
|
53
95
|
require_relative "stationery/page_templates"
|
|
96
|
+
require_relative "stationery/structure"
|
|
54
97
|
require_relative "stationery/document"
|
|
55
98
|
|
|
56
99
|
# Pure-Ruby PDF documents built from Phlex-style components.
|
|
57
100
|
module Stationery
|
|
101
|
+
@font_paths = []
|
|
102
|
+
|
|
103
|
+
class << self
|
|
104
|
+
# Directories searched for font files; the Railtie appends the app's.
|
|
105
|
+
attr_reader :font_paths
|
|
106
|
+
end
|
|
107
|
+
# Names of the font families shipped inside the gem.
|
|
108
|
+
def self.bundled_fonts = Fonts::Bundled.names
|
|
58
109
|
end
|