stationery 0.12.3 → 0.13.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 +3 -2
- data/README.md +66 -2
- data/lib/stationery/barcode/data_matrix.rb +2 -2
- data/lib/stationery/barcode/data_matrix_placement.rb +1 -1
- data/lib/stationery/barcode/ean13.rb +2 -2
- data/lib/stationery/barcode/qr.rb +4 -4
- data/lib/stationery/barcode/qr_matrix.rb +1 -1
- data/lib/stationery/barcode/reed_solomon.rb +7 -6
- data/lib/stationery/canvas/debug.rb +1 -1
- data/lib/stationery/canvas/interface.rb +1 -1
- data/lib/stationery/canvas/marking.rb +1 -1
- data/lib/stationery/cli.rb +7 -2
- data/lib/stationery/color.rb +5 -15
- data/lib/stationery/css/stylesheet.rb +1 -1
- data/lib/stationery/document.rb +32 -8
- data/lib/stationery/examples.rb +1 -1
- data/lib/stationery/fonts/bundled.rb +3 -3
- data/lib/stationery/fonts/catalog.rb +4 -3
- data/lib/stationery/fonts/cff.rb +6 -7
- data/lib/stationery/fonts/cff_subset.rb +1 -1
- data/lib/stationery/fonts/family.rb +5 -2
- data/lib/stationery/fonts/font.rb +4 -1
- data/lib/stationery/fonts/gsub.rb +12 -8
- data/lib/stationery/fonts/installer.rb +2 -2
- data/lib/stationery/fonts/ligatures.rb +5 -3
- data/lib/stationery/fonts/registry.rb +9 -19
- data/lib/stationery/fonts/true_type.rb +14 -17
- data/lib/stationery/forms/appearance.rb +1 -1
- data/lib/stationery/forms/field.rb +2 -2
- data/lib/stationery/html/tree_builder/blocks.rb +2 -2
- data/lib/stationery/html/tree_builder.rb +2 -2
- data/lib/stationery/hyphenation.rb +4 -6
- data/lib/stationery/images/cache.rb +10 -15
- data/lib/stationery/images/images.rb +2 -2
- data/lib/stationery/images/jpeg/bit_reader.rb +1 -1
- data/lib/stationery/images/jpeg/scan.rb +1 -1
- data/lib/stationery/images/jpeg.rb +7 -4
- data/lib/stationery/images/png.rb +4 -3
- data/lib/stationery/images/webp/prefix_code.rb +1 -1
- data/lib/stationery/images/webp.rb +4 -3
- data/lib/stationery/instrumentation.rb +24 -4
- data/lib/stationery/layout/box.rb +2 -2
- data/lib/stationery/layout/table/cell.rb +4 -5
- data/lib/stationery/layout/table.rb +2 -2
- data/lib/stationery/layout/table_of_contents/entry.rb +1 -1
- data/lib/stationery/lru.rb +31 -0
- data/lib/stationery/memo.rb +33 -0
- data/lib/stationery/monochrome.rb +2 -2
- data/lib/stationery/page.rb +2 -2
- data/lib/stationery/pdf/conformance.rb +10 -6
- data/lib/stationery/pdf/encryption/aes.rb +1 -1
- data/lib/stationery/pdf/encryption/standard_security.rb +2 -2
- data/lib/stationery/pdf/factur_x.rb +2 -2
- data/lib/stationery/pdf/serializer.rb +1 -1
- data/lib/stationery/pdf/writer.rb +1 -1
- data/lib/stationery/pdf/xmp.rb +1 -1
- data/lib/stationery/preview.rb +15 -5
- data/lib/stationery/railtie.rb +3 -0
- data/lib/stationery/raster/png.rb +2 -2
- data/lib/stationery/raster/render.rb +3 -3
- data/lib/stationery/rich/renderer/inlines.rb +4 -4
- data/lib/stationery/rich/styles.rb +2 -2
- data/lib/stationery/shareable.rb +41 -0
- data/lib/stationery/skill.rb +2 -2
- data/lib/stationery/store.rb +42 -0
- data/lib/stationery/svg/region.rb +1 -1
- data/lib/stationery/testing/inspector.rb +1 -1
- data/lib/stationery/testing/marked_text.rb +1 -1
- data/lib/stationery/testing/page_reader.rb +1 -1
- data/lib/stationery/text/markup.rb +2 -2
- data/lib/stationery/verify/allowlist.rb +1 -1
- data/lib/stationery/verify/engines/pdfium.rb +1 -1
- data/lib/stationery/verify/engines/pdfjs.rb +1 -1
- data/lib/stationery/verify/engines/pdfkit.rb +1 -1
- data/lib/stationery/version.rb +1 -1
- data/lib/stationery.rb +35 -1
- metadata +5 -1
|
@@ -20,7 +20,7 @@ module Stationery
|
|
|
20
20
|
DEFAULT_FILENAME = "factur-x.xml"
|
|
21
21
|
DESCRIPTION = "Factur-X Invoice"
|
|
22
22
|
ROOTS = ["<?xml", "<rsm:CrossIndustryInvoice"].freeze
|
|
23
|
-
SCHEMA =
|
|
23
|
+
SCHEMA = Stationery.shareable(
|
|
24
24
|
name: "Factur-X PDFA Extension Schema", uri: NAMESPACE, prefix: "fx",
|
|
25
25
|
properties: [
|
|
26
26
|
{ name: "DocumentFileName", type: "Text", category: "external",
|
|
@@ -32,7 +32,7 @@ module Stationery
|
|
|
32
32
|
{ name: "ConformanceLevel", type: "Text", category: "external",
|
|
33
33
|
description: "The conformance level of the embedded XML document" }
|
|
34
34
|
].freeze
|
|
35
|
-
|
|
35
|
+
)
|
|
36
36
|
|
|
37
37
|
attr_reader :xml, :profile, :filename, :version
|
|
38
38
|
|
|
@@ -14,7 +14,7 @@ module Stationery
|
|
|
14
14
|
module Serializer
|
|
15
15
|
NAME_ESCAPE = %r{[^\x21-\x7E]|[#%()/<>\[\]{}]}n
|
|
16
16
|
LITERAL_ESCAPE = /[\\()\r]/n
|
|
17
|
-
UTF16_BOM = "\xFE\xFF".b
|
|
17
|
+
UTF16_BOM = "\xFE\xFF".b.freeze
|
|
18
18
|
# Below it a Float times 10,000 is off by less than TIE / 8, so which
|
|
19
19
|
# way it rounds is certain unless it is within TIE of a half.
|
|
20
20
|
FAST_BELOW = 100_000.0
|
|
@@ -12,7 +12,7 @@ module Stationery
|
|
|
12
12
|
# object set so far and drops it, so objects leave in the order they were
|
|
13
13
|
# flushed and the first bytes leave before the last page is assembled.
|
|
14
14
|
class Writer
|
|
15
|
-
HEADER = "%PDF-1.7\n%\xE2\xE3\xCF\xD3\n".b
|
|
15
|
+
HEADER = "%PDF-1.7\n%\xE2\xE3\xCF\xD3\n".b.freeze
|
|
16
16
|
WRITTEN = Object.new.freeze
|
|
17
17
|
|
|
18
18
|
# `encryption` (an Encryption::StandardSecurity) encrypts every string
|
data/lib/stationery/pdf/xmp.rb
CHANGED
|
@@ -11,7 +11,7 @@ module Stationery
|
|
|
11
11
|
# ids, so two renders of one document are byte-identical.
|
|
12
12
|
module XMP
|
|
13
13
|
PACKET_ID = "W5M0MpCehiHzreSzNTczkc9d"
|
|
14
|
-
PADDING = "#{" " * 63}\n" * 32 # 2 KB of whitespace, as the spec suggests
|
|
14
|
+
PADDING = ("#{" " * 63}\n" * 32).freeze # 2 KB of whitespace, as the spec suggests
|
|
15
15
|
DC = "http://purl.org/dc/elements/1.1/"
|
|
16
16
|
XMP_NS = "http://ns.adobe.com/xap/1.0/"
|
|
17
17
|
PDF_NS = "http://ns.adobe.com/pdf/1.3/"
|
data/lib/stationery/preview.rb
CHANGED
|
@@ -15,20 +15,30 @@ module Stationery
|
|
|
15
15
|
# def around_render(_name, params) = I18n.with_locale(params.fetch("locale", I18n.default_locale)) { yield }
|
|
16
16
|
# end
|
|
17
17
|
class Preview
|
|
18
|
-
REGISTRY_LOCK = Mutex.new
|
|
19
18
|
HOOKS = %w[around_render].freeze
|
|
20
19
|
|
|
20
|
+
# The subclasses defined in one Ractor (see Store), in the order they
|
|
21
|
+
# were, behind their own lock.
|
|
22
|
+
class Registry
|
|
23
|
+
def initialize
|
|
24
|
+
@classes = []
|
|
25
|
+
@lock = Mutex.new
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def <<(subclass) = @lock.synchronize { @classes << subclass }
|
|
29
|
+
def to_a = @lock.synchronize { @classes.dup }
|
|
30
|
+
end
|
|
31
|
+
|
|
21
32
|
class << self
|
|
22
33
|
def inherited(subclass)
|
|
23
34
|
super
|
|
24
|
-
|
|
35
|
+
Preview.registry << subclass
|
|
25
36
|
end
|
|
26
37
|
|
|
27
38
|
# Named subclasses with at least one preview; a name defined again (a
|
|
28
39
|
# reloaded file) keeps its latest class.
|
|
29
40
|
def all
|
|
30
|
-
|
|
31
|
-
classes.select { it.name && it.pdfs.any? }.reverse.uniq(&:name).sort_by(&:preview_name)
|
|
41
|
+
Preview.registry.to_a.select { it.name && it.pdfs.any? }.reverse.uniq(&:name).sort_by(&:preview_name)
|
|
32
42
|
end
|
|
33
43
|
|
|
34
44
|
# `load`, not `require`: edited previews show up without a restart.
|
|
@@ -48,7 +58,7 @@ module Stationery
|
|
|
48
58
|
|
|
49
59
|
protected
|
|
50
60
|
|
|
51
|
-
def registry = (
|
|
61
|
+
def registry = Store.fetch(:stationery_previews) { Registry.new }
|
|
52
62
|
|
|
53
63
|
private
|
|
54
64
|
|
data/lib/stationery/railtie.rb
CHANGED
|
@@ -32,6 +32,9 @@ module Stationery
|
|
|
32
32
|
Railtie.add_previews(app.routes) if app.config.stationery.show_previews
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
+
# Once every initializer has added its font paths: see Stationery.finalize!.
|
|
36
|
+
config.after_initialize { Stationery.finalize! }
|
|
37
|
+
|
|
35
38
|
def self.add_font_paths(root, dirs)
|
|
36
39
|
dirs.each do |dir|
|
|
37
40
|
path = root.join(dir).to_s
|
|
@@ -10,8 +10,8 @@ module Stationery
|
|
|
10
10
|
# written unfiltered, which deflate squeezes well enough for pages that
|
|
11
11
|
# are mostly flat colour, and much faster in Ruby than a filter.
|
|
12
12
|
module PNG
|
|
13
|
-
SIGNATURE = "\x89PNG\r\n\x1A\n".b
|
|
14
|
-
NO_FILTER = "\x00".b
|
|
13
|
+
SIGNATURE = "\x89PNG\r\n\x1A\n".b.freeze
|
|
14
|
+
NO_FILTER = "\x00".b.freeze
|
|
15
15
|
|
|
16
16
|
module_function
|
|
17
17
|
|
|
@@ -17,9 +17,9 @@ module Stationery
|
|
|
17
17
|
# class declares it.
|
|
18
18
|
PDF_ONLY = %i[sign encrypt conformance attachments print tagged page_labels xmp factur_x incremental
|
|
19
19
|
missing_glyphs object_streams].freeze
|
|
20
|
-
GREY = "\x01-\xFE".b
|
|
21
|
-
DARK = "\x00-\x7F".b
|
|
22
|
-
LIGHT = "\x80-\xFF".b
|
|
20
|
+
GREY = "\x01-\xFE".b.freeze
|
|
21
|
+
DARK = "\x00-\x7F".b.freeze
|
|
22
|
+
LIGHT = "\x80-\xFF".b.freeze
|
|
23
23
|
|
|
24
24
|
def self.refuse(options, output = "to_png")
|
|
25
25
|
key = options.keys.first
|
|
@@ -5,7 +5,7 @@ module Stationery
|
|
|
5
5
|
class Renderer
|
|
6
6
|
# Writes inlines into a Text::RunsBuilder, nesting one builder call per mark.
|
|
7
7
|
class Inlines
|
|
8
|
-
STYLE_STEPS =
|
|
8
|
+
STYLE_STEPS = Stationery.shareable(
|
|
9
9
|
weight: ->(value) { [:b] if value == :bold },
|
|
10
10
|
style: ->(value) { [:i] if value == :italic },
|
|
11
11
|
underline: ->(value) { [:u] if value },
|
|
@@ -13,15 +13,15 @@ module Stationery
|
|
|
13
13
|
color: ->(value) { [:color, value] if value },
|
|
14
14
|
size: ->(value) { [:size, value] if value },
|
|
15
15
|
font: ->(value) { [:font, value] if value }
|
|
16
|
-
|
|
16
|
+
)
|
|
17
17
|
|
|
18
18
|
# A true mark switches the style on, a false one (from CSS) switches it off again.
|
|
19
|
-
MARK_STEPS =
|
|
19
|
+
MARK_STEPS = Stationery.shareable(
|
|
20
20
|
bold: { true => [[:b]], false => [[:with, { weight: :regular }]] },
|
|
21
21
|
italic: { true => [[:i]], false => [[:with, { style: :normal }]] },
|
|
22
22
|
underline: { true => [[:u]], false => [[:with, { underline: false }]] },
|
|
23
23
|
strike: { true => [[:strikethrough]], false => [[:with, { strikethrough: false }]] }
|
|
24
|
-
|
|
24
|
+
)
|
|
25
25
|
|
|
26
26
|
def initialize(runs, styles, links = Links.new(:all, nil))
|
|
27
27
|
@runs = runs
|
|
@@ -7,7 +7,7 @@ module Stationery
|
|
|
7
7
|
module Styles
|
|
8
8
|
HEADING_SCALES = [2.0, 1.6, 1.3, 1.1, 1.0, 0.9].freeze
|
|
9
9
|
|
|
10
|
-
DEFAULTS =
|
|
10
|
+
DEFAULTS = Stationery.shareable(
|
|
11
11
|
**HEADING_SCALES.each_with_index.to_h do |scale, index|
|
|
12
12
|
[:"h#{index + 1}", { scale:, weight: :bold, keep_with_next: true }]
|
|
13
13
|
end,
|
|
@@ -22,7 +22,7 @@ module Stationery
|
|
|
22
22
|
ol: {},
|
|
23
23
|
li: {},
|
|
24
24
|
img: { max_width: nil, float_margin: 8 }
|
|
25
|
-
|
|
25
|
+
)
|
|
26
26
|
|
|
27
27
|
module_function
|
|
28
28
|
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stationery
|
|
4
|
+
# `value` deep-frozen, so a constant holding it can be read from any
|
|
5
|
+
# Ractor: Ractor.make_shareable, which also makes a capture-free Proc
|
|
6
|
+
# shareable, or a plain freeze on an engine without Ractor (JRuby,
|
|
7
|
+
# TruffleRuby), where there is nothing to share it with. For the gem's
|
|
8
|
+
# own constants.
|
|
9
|
+
def self.shareable(value) = defined?(::Ractor) ? ::Ractor.make_shareable(value) : value.freeze
|
|
10
|
+
|
|
11
|
+
# Settings an app gave at boot, frozen as far as they can be without
|
|
12
|
+
# touching what the app holds: a Hash, an Array or a Data is a frozen copy
|
|
13
|
+
# of its contents settled in turn, a String or a Time a frozen copy, and a
|
|
14
|
+
# block is made shareable when it uses nothing unshareable from around it.
|
|
15
|
+
# Anything else not shareable already (a shaper or an instrumenter that
|
|
16
|
+
# keeps state, a key, a block that closes over the app's state) is kept as
|
|
17
|
+
# it is, never frozen: the main Ractor uses it as before, and a Ractor
|
|
18
|
+
# cannot. Never raises. For Document.finalize!.
|
|
19
|
+
def self.settled(value)
|
|
20
|
+
return value if defined?(::Ractor) && ::Ractor.shareable?(value)
|
|
21
|
+
|
|
22
|
+
case value
|
|
23
|
+
when Hash then value.transform_values { settled(it) }.freeze
|
|
24
|
+
when Array then value.map { settled(it) }.freeze
|
|
25
|
+
when String, Time then value.frozen? ? value : value.dup.freeze
|
|
26
|
+
when Data then value.with(**value.to_h.transform_values { settled(it) })
|
|
27
|
+
when Proc then settled_block(value)
|
|
28
|
+
else value
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# A capture-free block made shareable where it is, which leaves it as it
|
|
33
|
+
# was when it cannot be: Ractor.make_shareable checks what the block uses
|
|
34
|
+
# before it changes anything.
|
|
35
|
+
def self.settled_block(block)
|
|
36
|
+
defined?(::Ractor) ? ::Ractor.make_shareable(block) : block
|
|
37
|
+
rescue ::Ractor::Error
|
|
38
|
+
block
|
|
39
|
+
end
|
|
40
|
+
private_class_method :settled_block
|
|
41
|
+
end
|
data/lib/stationery/skill.rb
CHANGED
|
@@ -15,8 +15,8 @@ module Stationery
|
|
|
15
15
|
class Skill
|
|
16
16
|
NAME = "stationery"
|
|
17
17
|
GENERATOR = "stationery skill"
|
|
18
|
-
TEMPLATES = File.expand_path("skill", __dir__)
|
|
19
|
-
ROOT = File.expand_path("../..", __dir__)
|
|
18
|
+
TEMPLATES = File.expand_path("skill", __dir__).freeze
|
|
19
|
+
ROOT = File.expand_path("../..", __dir__).freeze
|
|
20
20
|
DOCS = "https://stationery.zoolutions.llc"
|
|
21
21
|
|
|
22
22
|
# The version a SKILL.md was written for, when stationery wrote it.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stationery
|
|
4
|
+
# Where the gem's caches live: one value per key and per Ractor, shared by
|
|
5
|
+
# that Ractor's threads, built by the block the first time it is asked for.
|
|
6
|
+
#
|
|
7
|
+
# Stationery::Store.fetch(:stationery_fonts) { LRU.new(16) }
|
|
8
|
+
#
|
|
9
|
+
# A cache in a constant or a class instance variable cannot be read from a
|
|
10
|
+
# non-main Ractor, so each Ractor keeps its own here and a cache's lock
|
|
11
|
+
# lives inside it (see LRU and Memo). On Ruby 3.4+ this is
|
|
12
|
+
# `Ractor.store_if_absent`, atomic and per Ractor. An engine without it
|
|
13
|
+
# (JRuby, TruffleRuby: no Ractor at all) has one Hash behind a Mutex,
|
|
14
|
+
# both made at load. conformance-kit's ConformanceKit::Store is the same
|
|
15
|
+
# pattern; the gem cannot depend on the kit when it runs.
|
|
16
|
+
module Store
|
|
17
|
+
# A Hash whose every read and write holds one Mutex: the store of an
|
|
18
|
+
# engine without Ractor.store_if_absent.
|
|
19
|
+
class Locked
|
|
20
|
+
def initialize
|
|
21
|
+
@lock = Mutex.new
|
|
22
|
+
@values = {}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def fetch(key)
|
|
26
|
+
@lock.synchronize { @values.fetch(key) { @values[key] = yield } }
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.native? = defined?(::Ractor) ? ::Ractor.respond_to?(:store_if_absent) : false
|
|
31
|
+
|
|
32
|
+
if native?
|
|
33
|
+
def self.fetch(key, &) = ::Ractor.store_if_absent(key, &)
|
|
34
|
+
else
|
|
35
|
+
# Only where there is no Ractor to read it from: made once, at load.
|
|
36
|
+
@locked = Locked.new
|
|
37
|
+
|
|
38
|
+
# rubocop:disable-next ThreadSafety/ClassInstanceVariable -- written once at load; Locked holds its own Mutex
|
|
39
|
+
def self.fetch(key, &) = @locked.fetch(key, &)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -14,7 +14,7 @@ module Stationery
|
|
|
14
14
|
# `password:` opens an encrypted one. Needs the pdf-reader gem, loaded on
|
|
15
15
|
# first use.
|
|
16
16
|
class Inspector
|
|
17
|
-
UTF16_BOM = "\xFE\xFF".b
|
|
17
|
+
UTF16_BOM = "\xFE\xFF".b.freeze
|
|
18
18
|
# id-aa-signatureTimeStampToken: a signature's RFC 3161 timestamp.
|
|
19
19
|
TIMESTAMP_TOKEN = "1.2.840.113549.1.9.16.2.14"
|
|
20
20
|
XMP_PROPERTY = %r{^\s*<((?!rdf:)[\w.-]+:[\w.-]+)>(.*?)</\1>$}
|
|
@@ -19,7 +19,7 @@ module Stationery
|
|
|
19
19
|
set_character_spacing set_word_spacing set_text_rise set_horizontal_text_scaling
|
|
20
20
|
set_text_rendering_mode].freeze
|
|
21
21
|
|
|
22
|
-
UTF16_BOM = "\xFE\xFF".b
|
|
22
|
+
UTF16_BOM = "\xFE\xFF".b.freeze
|
|
23
23
|
# The width of a character in a sequence that moves the pen nowhere, in
|
|
24
24
|
# parts of its size.
|
|
25
25
|
NOMINAL = 0.5
|
|
@@ -15,7 +15,7 @@ module Stationery
|
|
|
15
15
|
set_text_matrix_and_text_line_matrix move_to_start_of_next_line set_text_leading
|
|
16
16
|
set_character_spacing set_word_spacing set_text_rise set_horizontal_text_scaling
|
|
17
17
|
set_text_rendering_mode].freeze
|
|
18
|
-
UTF16_BOM = "\xFE\xFF".b
|
|
18
|
+
UTF16_BOM = "\xFE\xFF".b.freeze
|
|
19
19
|
SUBSET = /\A[A-Z]{6}\+/
|
|
20
20
|
# A gap wider than this share of the size is a space between words;
|
|
21
21
|
# one wider than the size itself starts a new run.
|
|
@@ -15,13 +15,13 @@ module Stationery
|
|
|
15
15
|
class Markup
|
|
16
16
|
TAG = %r{<(/?)([a-zA-Z][a-zA-Z0-9]*)((?:\s+[a-zA-Z_:-]+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>"']+))*)\s*/?>}
|
|
17
17
|
ATTRIBUTE = /([a-zA-Z_:-]+)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>"']+))/
|
|
18
|
-
STYLES =
|
|
18
|
+
STYLES = Stationery.shareable(
|
|
19
19
|
"b" => { weight: :bold }, "strong" => { weight: :bold },
|
|
20
20
|
"i" => { style: :italic }, "em" => { style: :italic },
|
|
21
21
|
"u" => { underline: true },
|
|
22
22
|
"strikethrough" => { strikethrough: true }, "s" => { strikethrough: true }, "del" => { strikethrough: true },
|
|
23
23
|
"sub" => { script: :sub }, "sup" => { script: :sup }
|
|
24
|
-
|
|
24
|
+
)
|
|
25
25
|
|
|
26
26
|
def self.parse(source, style)
|
|
27
27
|
new(source, style).runs
|
|
@@ -19,7 +19,7 @@ module Stationery
|
|
|
19
19
|
{ engine: "mupdf", pattern: /\Awarning: ICC support is not available\z/,
|
|
20
20
|
why: "Debian's mutool (mupdf-tools, in the verify image) is built without colour management and says so " \
|
|
21
21
|
"before it opens any file, even one that is not a PDF." }
|
|
22
|
-
].freeze
|
|
22
|
+
].map { Stationery.shareable(it) }.freeze
|
|
23
23
|
|
|
24
24
|
def self.allows?(entries, engine, message)
|
|
25
25
|
entries.any? { |entry| entry[:engine] == engine && entry[:pattern].match?(message) }
|
|
@@ -8,7 +8,7 @@ module Stationery
|
|
|
8
8
|
class Pdfium < Engine
|
|
9
9
|
NAME = "pdfium"
|
|
10
10
|
INSTALL = "pip install pypdfium2 (STATIONERY_PYTHON names the Python that has it)"
|
|
11
|
-
SCRIPT = File.expand_path("../scripts/pdfium.py", __dir__)
|
|
11
|
+
SCRIPT = File.expand_path("../scripts/pdfium.py", __dir__).freeze
|
|
12
12
|
VERSION = "import pypdfium2.version as v; print(v.PYPDFIUM_INFO, v.PDFIUM_INFO)"
|
|
13
13
|
|
|
14
14
|
# -P everywhere: Python loads no module from beside the script or from
|
|
@@ -10,7 +10,7 @@ module Stationery
|
|
|
10
10
|
class Pdfjs < Engine
|
|
11
11
|
NAME = "pdfjs"
|
|
12
12
|
INSTALL = "npm i pdfjs-dist (STATIONERY_PDFJS names its directory)"
|
|
13
|
-
SCRIPT = File.expand_path("../scripts/pdfjs.mjs", __dir__)
|
|
13
|
+
SCRIPT = File.expand_path("../scripts/pdfjs.mjs", __dir__).freeze
|
|
14
14
|
RESOLVE = "console.log(require('path').dirname(require.resolve('pdfjs-dist/package.json')))"
|
|
15
15
|
|
|
16
16
|
def available? = !directory.nil?
|
|
@@ -8,7 +8,7 @@ module Stationery
|
|
|
8
8
|
class Pdfkit < Engine
|
|
9
9
|
NAME = "pdfkit"
|
|
10
10
|
INSTALL = "macOS only: xcode-select --install"
|
|
11
|
-
SCRIPT = File.expand_path("../scripts/pdfkit.swift", __dir__)
|
|
11
|
+
SCRIPT = File.expand_path("../scripts/pdfkit.swift", __dir__).freeze
|
|
12
12
|
|
|
13
13
|
def available? = RUBY_PLATFORM.include?("darwin") && !Command.which("swift").nil?
|
|
14
14
|
def version = "macOS #{run("sw_vers", "-productVersion").stdout.strip}"
|
data/lib/stationery/version.rb
CHANGED
data/lib/stationery.rb
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "stationery/version"
|
|
4
|
+
require_relative "stationery/shareable"
|
|
4
5
|
require_relative "stationery/errors"
|
|
6
|
+
require_relative "stationery/store"
|
|
7
|
+
require_relative "stationery/lru"
|
|
8
|
+
require_relative "stationery/memo"
|
|
5
9
|
require_relative "stationery/instrumentation"
|
|
6
10
|
require_relative "stationery/pdf/types"
|
|
7
11
|
require_relative "stationery/pdf/serializer"
|
|
@@ -202,11 +206,41 @@ require_relative "stationery/document"
|
|
|
202
206
|
|
|
203
207
|
# Pure-Ruby PDF documents built from Phlex-style components.
|
|
204
208
|
module Stationery
|
|
205
|
-
|
|
209
|
+
# Appended to at boot, on the main Ractor (the Railtie, an app's own
|
|
210
|
+
# setup); finalize! freezes it.
|
|
211
|
+
@font_paths = [] # rubocop:disable ThreadSafety/MutableClassInstanceVariable
|
|
206
212
|
|
|
207
213
|
class << self
|
|
208
214
|
# Directories searched for font files; the Railtie appends the app's.
|
|
215
|
+
# Frozen by finalize!.
|
|
209
216
|
attr_reader :font_paths
|
|
217
|
+
|
|
218
|
+
# Ends boot: what the gem holds for every render is frozen, and
|
|
219
|
+
# Ractor-shareable as far as the app's settings let it be. `font_paths`
|
|
220
|
+
# is frozen, as a frozen copy of each directory; the instrumenter in use
|
|
221
|
+
# is kept, so one loaded later is not picked up; and every document class
|
|
222
|
+
# loaded by then is finalized (Document.finalize!), with its subclasses.
|
|
223
|
+
# Nothing it cannot make shareable raises: it is kept as it is, for the
|
|
224
|
+
# main Ractor. Run it once everything that adds a font path or defines a
|
|
225
|
+
# document class at boot has; the Railtie runs it after the app has
|
|
226
|
+
# initialized. Answers Stationery; running it again finalizes only the
|
|
227
|
+
# classes loaded since.
|
|
228
|
+
def finalize!
|
|
229
|
+
# rubocop:disable-next ThreadSafety/ClassInstanceVariable -- written at boot on the main Ractor, before any render
|
|
230
|
+
@font_paths = @font_paths.map { |path| path.to_s.dup.freeze }.freeze
|
|
231
|
+
Instrumentation.finalize!
|
|
232
|
+
finalize_documents(Document)
|
|
233
|
+
self
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
private
|
|
237
|
+
|
|
238
|
+
def finalize_documents(parent)
|
|
239
|
+
parent.subclasses.each do |document|
|
|
240
|
+
document.finalize!
|
|
241
|
+
finalize_documents(document)
|
|
242
|
+
end
|
|
243
|
+
end
|
|
210
244
|
end
|
|
211
245
|
# Names of the font families shipped inside the gem.
|
|
212
246
|
def self.bundled_fonts = Fonts::Bundled.names
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: stationery
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.13.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mikael Henriksson
|
|
@@ -224,6 +224,7 @@ files:
|
|
|
224
224
|
- lib/stationery/layout/text.rb
|
|
225
225
|
- lib/stationery/layout/wrap.rb
|
|
226
226
|
- lib/stationery/list_markers.rb
|
|
227
|
+
- lib/stationery/lru.rb
|
|
227
228
|
- lib/stationery/markdown/block_parser.rb
|
|
228
229
|
- lib/stationery/markdown/block_parser/containers.rb
|
|
229
230
|
- lib/stationery/markdown/block_parser/tables.rb
|
|
@@ -232,6 +233,7 @@ files:
|
|
|
232
233
|
- lib/stationery/markdown/inline_parser/emphasis.rb
|
|
233
234
|
- lib/stationery/markdown/inline_parser/links.rb
|
|
234
235
|
- lib/stationery/markdown/inline_parser/nodes.rb
|
|
236
|
+
- lib/stationery/memo.rb
|
|
235
237
|
- lib/stationery/minitest.rb
|
|
236
238
|
- lib/stationery/monochrome.rb
|
|
237
239
|
- lib/stationery/monochrome/bitmap.rb
|
|
@@ -301,6 +303,7 @@ files:
|
|
|
301
303
|
- lib/stationery/rich/styles.rb
|
|
302
304
|
- lib/stationery/rspec.rb
|
|
303
305
|
- lib/stationery/shaper.rb
|
|
306
|
+
- lib/stationery/shareable.rb
|
|
304
307
|
- lib/stationery/skill.rb
|
|
305
308
|
- lib/stationery/skill/SKILL.md.erb
|
|
306
309
|
- lib/stationery/skill/recipes/flyer.md.erb
|
|
@@ -317,6 +320,7 @@ files:
|
|
|
317
320
|
- lib/stationery/skill/reference/pages.md.erb
|
|
318
321
|
- lib/stationery/skill/reference/printing.md.erb
|
|
319
322
|
- lib/stationery/skill/reference/testing.md.erb
|
|
323
|
+
- lib/stationery/store.rb
|
|
320
324
|
- lib/stationery/structure.rb
|
|
321
325
|
- lib/stationery/svg/arc.rb
|
|
322
326
|
- lib/stationery/svg/bounds.rb
|