stationery 0.10.0 → 0.11.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 +18 -0
- data/README.md +247 -30
- data/lib/stationery/builder.rb +9 -0
- data/lib/stationery/canvas/text.rb +8 -2
- data/lib/stationery/document.rb +56 -14
- data/lib/stationery/elements.rb +31 -3
- data/lib/stationery/errors.rb +5 -1
- data/lib/stationery/fonts/font.rb +54 -4
- data/lib/stationery/fonts/font_book.rb +14 -5
- data/lib/stationery/fonts/glyph_run.rb +35 -5
- data/lib/stationery/fonts/shaped_run.rb +160 -0
- data/lib/stationery/fonts/shaping.rb +63 -0
- data/lib/stationery/forms/typeface.rb +10 -3
- data/lib/stationery/html/css.rb +38 -3
- data/lib/stationery/html/tree_builder/blocks.rb +9 -3
- data/lib/stationery/images/images.rb +6 -6
- data/lib/stationery/images/resampled.rb +3 -2
- data/lib/stationery/images/webp/bit_reader.rb +78 -0
- data/lib/stationery/images/webp/container.rb +62 -0
- data/lib/stationery/images/webp/image_stream.rb +168 -0
- data/lib/stationery/images/webp/lossless.rb +101 -0
- data/lib/stationery/images/webp/predictor.rb +118 -0
- data/lib/stationery/images/webp/prefix_code.rb +168 -0
- data/lib/stationery/images/webp/transforms.rb +93 -0
- data/lib/stationery/images/webp.rb +71 -0
- data/lib/stationery/layout/columns/balancer.rb +66 -0
- data/lib/stationery/layout/columns/pour.rb +47 -0
- data/lib/stationery/layout/columns.rb +191 -0
- data/lib/stationery/layout/floated.rb +50 -0
- data/lib/stationery/layout/flow/floating.rb +51 -0
- data/lib/stationery/layout/flow/placement.rb +147 -0
- data/lib/stationery/layout/flow/splitter.rb +194 -0
- data/lib/stationery/layout/flow.rb +20 -117
- data/lib/stationery/layout/image.rb +1 -1
- data/lib/stationery/layout/mark.rb +3 -1
- data/lib/stationery/layout/node.rb +14 -0
- data/lib/stationery/layout/paginator.rb +7 -2
- data/lib/stationery/layout/table.rb +52 -6
- data/lib/stationery/layout/text.rb +31 -12
- data/lib/stationery/page.rb +26 -1
- data/lib/stationery/page_templates.rb +1 -1
- data/lib/stationery/pdf/assembler.rb +20 -4
- data/lib/stationery/pdf/conformance.rb +11 -1
- data/lib/stationery/pdf/page_sealer.rb +35 -0
- data/lib/stationery/rich/nodes.rb +3 -2
- data/lib/stationery/rich/renderer/boxes.rb +25 -3
- data/lib/stationery/rich/renderer.rb +5 -3
- data/lib/stationery/rich/styles.rb +1 -1
- data/lib/stationery/shaper.rb +109 -0
- data/lib/stationery/testing/marked_text.rb +34 -4
- data/lib/stationery/text/exclusions.rb +32 -0
- data/lib/stationery/text/line.rb +7 -2
- data/lib/stationery/text/paragraph.rb +69 -10
- data/lib/stationery/text/wrapper/remainder.rb +59 -0
- data/lib/stationery/text/wrapper.rb +54 -10
- data/lib/stationery/version.rb +1 -1
- data/lib/stationery.rb +10 -0
- metadata +22 -1
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stationery
|
|
4
|
+
module Images
|
|
5
|
+
class WebP
|
|
6
|
+
# A VP8L bitstream (WebP Lossless Bitstream Specification): the header
|
|
7
|
+
# is read on creation, `pixels` decodes the image to an Array of ARGB
|
|
8
|
+
# Integers, row by row from the top left.
|
|
9
|
+
class Lossless
|
|
10
|
+
SIGNATURE = 0x2F
|
|
11
|
+
HEADER_BYTES = 5
|
|
12
|
+
# What one image may decode to; the format itself allows 16384 a
|
|
13
|
+
# side, which no page needs and an Array of pixels cannot hold cheaply.
|
|
14
|
+
MAX_PIXELS = 1 << 25
|
|
15
|
+
PREDICTOR = 0
|
|
16
|
+
CROSS_COLOR = 1
|
|
17
|
+
SUBTRACT_GREEN = 2
|
|
18
|
+
|
|
19
|
+
attr_reader :width, :height
|
|
20
|
+
|
|
21
|
+
def initialize(data)
|
|
22
|
+
raise UnsupportedImage, "truncated WebP image" if data.bytesize < HEADER_BYTES
|
|
23
|
+
|
|
24
|
+
@reader = BitReader.new(data)
|
|
25
|
+
raise UnsupportedImage, "invalid WebP image: not a lossless bitstream" unless @reader.read(8) == SIGNATURE
|
|
26
|
+
|
|
27
|
+
@width = @reader.read(14) + 1
|
|
28
|
+
@height = @reader.read(14) + 1
|
|
29
|
+
@alpha = @reader.read(1) == 1
|
|
30
|
+
raise UnsupportedImage, "invalid WebP image: unknown lossless version" unless @reader.read(3).zero?
|
|
31
|
+
return if @width * @height <= MAX_PIXELS
|
|
32
|
+
|
|
33
|
+
raise UnsupportedImage, "WebP image too large: #{@width}x#{@height} pixels"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Whether the alpha channel means anything; when not, every pixel is opaque.
|
|
37
|
+
def alpha? = @alpha
|
|
38
|
+
|
|
39
|
+
def pixels
|
|
40
|
+
transforms = read_transforms
|
|
41
|
+
argb = ImageStream.decode(@reader, @coded_width, @height, top: true)
|
|
42
|
+
raise UnsupportedImage, "truncated WebP image" if @reader.overrun?
|
|
43
|
+
|
|
44
|
+
transforms.reverse_each { |transform| argb = transform.call(argb) }
|
|
45
|
+
argb
|
|
46
|
+
rescue UnsupportedImage
|
|
47
|
+
raise unless @reader.overrun?
|
|
48
|
+
|
|
49
|
+
raise UnsupportedImage, "truncated WebP image"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
# The transforms in the order they were applied, each as a callable
|
|
55
|
+
# that undoes it. A transform may appear once.
|
|
56
|
+
def read_transforms
|
|
57
|
+
@coded_width = @width
|
|
58
|
+
seen = []
|
|
59
|
+
transforms = []
|
|
60
|
+
while @reader.read(1) == 1
|
|
61
|
+
type = @reader.read(2)
|
|
62
|
+
raise UnsupportedImage, "invalid WebP image: transform used twice" if seen.include?(type)
|
|
63
|
+
|
|
64
|
+
seen << type
|
|
65
|
+
transforms << read_transform(type)
|
|
66
|
+
end
|
|
67
|
+
transforms
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def read_transform(type)
|
|
71
|
+
width = @coded_width
|
|
72
|
+
case type
|
|
73
|
+
when PREDICTOR
|
|
74
|
+
bits, image = read_blocks(width)
|
|
75
|
+
->(argb) { Predictor.inverse(argb, width, @height, bits, image) }
|
|
76
|
+
when CROSS_COLOR
|
|
77
|
+
bits, image = read_blocks(width)
|
|
78
|
+
->(argb) { Transforms.cross_color(argb, width, @height, bits, image) }
|
|
79
|
+
when SUBTRACT_GREEN then Transforms.method(:add_green)
|
|
80
|
+
else read_color_indexing(width)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# An image of one value per block of 2**bits pixels a side.
|
|
85
|
+
def read_blocks(width)
|
|
86
|
+
bits = @reader.read(3) + 2
|
|
87
|
+
[bits, ImageStream.decode(@reader, ImageStream.subsample(width, bits),
|
|
88
|
+
ImageStream.subsample(@height, bits))]
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def read_color_indexing(width)
|
|
92
|
+
size = @reader.read(8) + 1
|
|
93
|
+
table = Transforms.color_table(ImageStream.decode(@reader, size, 1))
|
|
94
|
+
bits = Transforms.bundling(size)
|
|
95
|
+
@coded_width = ImageStream.subsample(width, bits)
|
|
96
|
+
->(argb) { Transforms.color_indexing(argb, width, @height, bits, table) }
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stationery
|
|
4
|
+
module Images
|
|
5
|
+
class WebP
|
|
6
|
+
# Reverses the predictor transform: every pixel is stored as its
|
|
7
|
+
# difference from a prediction made of its left (L), top (T), top-left
|
|
8
|
+
# (TL) and top-right (TR) neighbours, the mode chosen per block. All
|
|
9
|
+
# arithmetic is per channel, modulo 256, on packed ARGB Integers.
|
|
10
|
+
module Predictor
|
|
11
|
+
BLACK = 0xFF000000
|
|
12
|
+
ODD = 0xFF00FF00
|
|
13
|
+
EVEN = 0x00FF00FF
|
|
14
|
+
# Clears each channel's lowest bit, so halving cannot borrow from
|
|
15
|
+
# the channel above.
|
|
16
|
+
HALVES = 0xFEFEFEFE
|
|
17
|
+
|
|
18
|
+
module_function
|
|
19
|
+
|
|
20
|
+
def inverse(argb, width, height, bits, modes)
|
|
21
|
+
first_row(argb, width)
|
|
22
|
+
blocks_wide = ImageStream.subsample(width, bits)
|
|
23
|
+
(1...height).each do |y|
|
|
24
|
+
row = y * width
|
|
25
|
+
argb[row] = add(argb[row], argb[row - width])
|
|
26
|
+
column = 1
|
|
27
|
+
while column < width
|
|
28
|
+
mode = (modes[((y >> bits) * blocks_wide) + (column >> bits)] >> 8) & 0xF
|
|
29
|
+
stop = [((column >> bits) + 1) << bits, width].min
|
|
30
|
+
run(mode, argb, row + column, row + stop, width)
|
|
31
|
+
column = stop
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
argb
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# The top-left pixel is predicted black, the rest of the row from the left.
|
|
38
|
+
def first_row(argb, width)
|
|
39
|
+
argb[0] = add(argb[0], BLACK)
|
|
40
|
+
(1...width).each { |x| argb[x] = add(argb[x], argb[x - 1]) }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def run(mode, argb, pos, stop, width)
|
|
44
|
+
while pos < stop
|
|
45
|
+
argb[pos] = add(argb[pos], predict(mode, argb, pos, width))
|
|
46
|
+
pos += 1
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# rubocop:disable-next Metrics/AbcSize -- one branch per mode of the format, run per pixel
|
|
51
|
+
def predict(mode, argb, pos, width)
|
|
52
|
+
top = pos - width
|
|
53
|
+
case mode
|
|
54
|
+
when 1 then argb[pos - 1]
|
|
55
|
+
when 2 then argb[top]
|
|
56
|
+
when 3 then argb[top + 1]
|
|
57
|
+
when 4 then argb[top - 1]
|
|
58
|
+
when 5 then average(average(argb[pos - 1], argb[top + 1]), argb[top])
|
|
59
|
+
when 6 then average(argb[pos - 1], argb[top - 1])
|
|
60
|
+
when 7 then average(argb[pos - 1], argb[top])
|
|
61
|
+
when 8 then average(argb[top - 1], argb[top])
|
|
62
|
+
when 9 then average(argb[top], argb[top + 1])
|
|
63
|
+
when 10 then average(average(argb[pos - 1], argb[top - 1]), average(argb[top], argb[top + 1]))
|
|
64
|
+
when 11 then select(argb[pos - 1], argb[top], argb[top - 1])
|
|
65
|
+
when 12 then Gradient.full(argb[pos - 1], argb[top], argb[top - 1])
|
|
66
|
+
when 13 then Gradient.half(average(argb[pos - 1], argb[top]), argb[top - 1])
|
|
67
|
+
else BLACK # mode 0, and 14 and 15 which the format leaves unused
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def add(a, b)
|
|
72
|
+
(((a & ODD) + (b & ODD)) & ODD) | (((a & EVEN) + (b & EVEN)) & EVEN)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def average(a, b)
|
|
76
|
+
(((a ^ b) & HALVES) >> 1) + (a & b)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Left or top, whichever is nearer to the gradient L + T - TL.
|
|
80
|
+
def select(left, top, top_left)
|
|
81
|
+
distance(top, top_left) < distance(left, top_left) ? left : top
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def distance(a, b)
|
|
85
|
+
((a >> 24) - (b >> 24)).abs + (((a >> 16) & 0xFF) - ((b >> 16) & 0xFF)).abs +
|
|
86
|
+
(((a >> 8) & 0xFF) - ((b >> 8) & 0xFF)).abs + ((a & 0xFF) - (b & 0xFF)).abs
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Modes 12 and 13: the gradient of the neighbours, clamped per channel.
|
|
90
|
+
module Gradient
|
|
91
|
+
module_function
|
|
92
|
+
|
|
93
|
+
def full(left, top, top_left)
|
|
94
|
+
(clamp((left >> 24) + (top >> 24) - (top_left >> 24)) << 24) |
|
|
95
|
+
(clamp(((left >> 16) & 0xFF) + ((top >> 16) & 0xFF) - ((top_left >> 16) & 0xFF)) << 16) |
|
|
96
|
+
(clamp(((left >> 8) & 0xFF) + ((top >> 8) & 0xFF) - ((top_left >> 8) & 0xFF)) << 8) |
|
|
97
|
+
clamp((left & 0xFF) + (top & 0xFF) - (top_left & 0xFF))
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def half(average, top_left)
|
|
101
|
+
(step(average >> 24, top_left >> 24) << 24) |
|
|
102
|
+
(step((average >> 16) & 0xFF, (top_left >> 16) & 0xFF) << 16) |
|
|
103
|
+
(step((average >> 8) & 0xFF, (top_left >> 8) & 0xFF) << 8) |
|
|
104
|
+
step(average & 0xFF, top_left & 0xFF)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# a + (a - b) / 2, the division rounding towards zero.
|
|
108
|
+
def step(a, b)
|
|
109
|
+
delta = a - b
|
|
110
|
+
clamp(a + (delta.negative? ? -(-delta >> 1) : delta >> 1))
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def clamp(value) = value.clamp(0, 255)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stationery
|
|
4
|
+
module Images
|
|
5
|
+
class WebP
|
|
6
|
+
# Canonical prefix (Huffman) codes as lookup tables. A table is a flat
|
|
7
|
+
# Array indexed by the next ROOT_BITS bits of the stream: an entry is
|
|
8
|
+
# `length << 16 | symbol`; codes longer than ROOT_BITS share a LINK
|
|
9
|
+
# entry (`LINK | bits << 16 | offset`) to a second-level table appended
|
|
10
|
+
# after the root. A code with a single symbol takes no bits at all.
|
|
11
|
+
module PrefixCode
|
|
12
|
+
ROOT_BITS = 8
|
|
13
|
+
ROOT_SIZE = 1 << ROOT_BITS
|
|
14
|
+
ROOT_MASK = ROOT_SIZE - 1
|
|
15
|
+
LINK = 1 << 20
|
|
16
|
+
MAX_LENGTH = 15
|
|
17
|
+
# The order the lengths of the code-length code are stored in.
|
|
18
|
+
LENGTH_ORDER = [17, 18, 0, 1, 2, 3, 4, 5, 16, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15].freeze
|
|
19
|
+
# Extra bits and base of the repeat codes 16, 17 and 18.
|
|
20
|
+
REPEATS = [[2, 3], [3, 3], [7, 11]].freeze
|
|
21
|
+
|
|
22
|
+
module_function
|
|
23
|
+
|
|
24
|
+
# Reads one prefix code over `size` symbols and returns its table.
|
|
25
|
+
def read(reader, size)
|
|
26
|
+
build(reader.read(1) == 1 ? simple_lengths(reader, size) : coded_lengths(reader, size))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# One or two symbols stored as they are, the code being 0 or 1 bit.
|
|
30
|
+
def simple_lengths(reader, size)
|
|
31
|
+
count = reader.read(1) + 1
|
|
32
|
+
symbols = [reader.read(reader.read(1) == 1 ? 8 : 1)]
|
|
33
|
+
symbols << reader.read(8) if count == 2
|
|
34
|
+
raise UnsupportedImage, "invalid WebP image: prefix code symbol out of range" if symbols.max >= size
|
|
35
|
+
|
|
36
|
+
lengths = Array.new(size, 0)
|
|
37
|
+
symbols.each { |symbol| lengths[symbol] = 1 }
|
|
38
|
+
lengths
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Code lengths that are themselves prefix coded, with repeat codes.
|
|
42
|
+
def coded_lengths(reader, size)
|
|
43
|
+
code_lengths = Array.new(LENGTH_ORDER.size, 0)
|
|
44
|
+
(reader.read(4) + 4).times { |i| code_lengths[LENGTH_ORDER[i]] = reader.read(3) }
|
|
45
|
+
table = build(code_lengths)
|
|
46
|
+
remaining = reader.read(1) == 1 ? 2 + reader.read(2 + (2 * reader.read(3))) : size
|
|
47
|
+
raise UnsupportedImage, "invalid WebP image: too many code lengths" if remaining > size
|
|
48
|
+
|
|
49
|
+
expand(reader, table, size, remaining)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def expand(reader, table, size, remaining)
|
|
53
|
+
lengths = Array.new(size, 0)
|
|
54
|
+
previous = 8
|
|
55
|
+
symbol = 0
|
|
56
|
+
while symbol < size && remaining.positive?
|
|
57
|
+
remaining -= 1
|
|
58
|
+
length = reader.symbol(table)
|
|
59
|
+
if length < 16
|
|
60
|
+
lengths[symbol] = length
|
|
61
|
+
symbol += 1
|
|
62
|
+
previous = length unless length.zero?
|
|
63
|
+
else
|
|
64
|
+
symbol = repeat(reader, lengths, symbol, length, previous)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
lengths
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def repeat(reader, lengths, symbol, code, previous)
|
|
71
|
+
bits, base = REPEATS[code - 16]
|
|
72
|
+
count = reader.read(bits) + base
|
|
73
|
+
raise UnsupportedImage, "invalid WebP image: code lengths overflow" if symbol + count > lengths.size
|
|
74
|
+
|
|
75
|
+
lengths.fill(previous, symbol, count) if code == 16
|
|
76
|
+
symbol + count
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# The lookup table of the canonical code with these lengths, which
|
|
80
|
+
# must be complete (every bit pattern decodes) or hold one symbol.
|
|
81
|
+
def build(lengths)
|
|
82
|
+
counts = Array.new(MAX_LENGTH + 1, 0)
|
|
83
|
+
lengths.each { |length| counts[length] += 1 }
|
|
84
|
+
used = lengths.size - counts[0]
|
|
85
|
+
counts[0] = 0
|
|
86
|
+
return Array.new(ROOT_SIZE, lengths.index(&:positive?)) if used == 1
|
|
87
|
+
|
|
88
|
+
space = (1..MAX_LENGTH).sum { |length| counts[length] << (MAX_LENGTH - length) }
|
|
89
|
+
raise UnsupportedImage, "invalid WebP image: incomplete prefix code" unless space == 1 << MAX_LENGTH
|
|
90
|
+
|
|
91
|
+
fill(lengths, counts)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def fill(lengths, counts)
|
|
95
|
+
codes = reversed_codes(lengths, counts)
|
|
96
|
+
table = Array.new(ROOT_SIZE, 0)
|
|
97
|
+
link(table, lengths, codes)
|
|
98
|
+
lengths.each_with_index do |length, symbol|
|
|
99
|
+
next if length.zero?
|
|
100
|
+
|
|
101
|
+
code = codes[symbol]
|
|
102
|
+
if length <= ROOT_BITS
|
|
103
|
+
spread(table, 0, ROOT_BITS, code, length, (length << 16) | symbol)
|
|
104
|
+
else
|
|
105
|
+
entry = table[code & ROOT_MASK]
|
|
106
|
+
spread(table, entry & 0xFFFF, (entry >> 16) & 0xF, code >> ROOT_BITS, length - ROOT_BITS,
|
|
107
|
+
((length - ROOT_BITS) << 16) | symbol)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
table
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Every symbol's code with its bits reversed, since the stream is
|
|
114
|
+
# read least significant bit first.
|
|
115
|
+
def reversed_codes(lengths, counts)
|
|
116
|
+
code = 0
|
|
117
|
+
first = Array.new(MAX_LENGTH + 1, 0)
|
|
118
|
+
(1..MAX_LENGTH).each { |length| first[length] = code = (code + counts[length - 1]) << 1 }
|
|
119
|
+
lengths.map do |length|
|
|
120
|
+
next 0 if length.zero?
|
|
121
|
+
|
|
122
|
+
value = first[length]
|
|
123
|
+
first[length] += 1
|
|
124
|
+
reverse(value, length)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def reverse(value, length)
|
|
129
|
+
reversed = 0
|
|
130
|
+
length.times do
|
|
131
|
+
reversed = (reversed << 1) | (value & 1)
|
|
132
|
+
value >>= 1
|
|
133
|
+
end
|
|
134
|
+
reversed
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Appends a second-level table for every root entry that long codes
|
|
138
|
+
# share, sized for the longest of them.
|
|
139
|
+
def link(table, lengths, codes)
|
|
140
|
+
longest = {}
|
|
141
|
+
lengths.each_with_index do |length, symbol|
|
|
142
|
+
next unless length > ROOT_BITS
|
|
143
|
+
|
|
144
|
+
root = codes[symbol] & ROOT_MASK
|
|
145
|
+
longest[root] = length if length > longest.fetch(root, 0)
|
|
146
|
+
end
|
|
147
|
+
longest.each do |root, length|
|
|
148
|
+
bits = length - ROOT_BITS
|
|
149
|
+
table[root] = LINK | (bits << 16) | table.size
|
|
150
|
+
table.concat(Array.new(1 << bits, 0))
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Writes `entry` at every index of a `bits`-wide table whose low
|
|
155
|
+
# `length` bits are `code`.
|
|
156
|
+
def spread(table, offset, bits, code, length, entry)
|
|
157
|
+
index = code
|
|
158
|
+
size = 1 << bits
|
|
159
|
+
step = 1 << length
|
|
160
|
+
while index < size
|
|
161
|
+
table[offset + index] = entry
|
|
162
|
+
index += step
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stationery
|
|
4
|
+
module Images
|
|
5
|
+
class WebP
|
|
6
|
+
# The subtract-green, cross-colour and colour-indexing transforms,
|
|
7
|
+
# reversed (the predictor has a file of its own). Each takes and
|
|
8
|
+
# returns the image as an Array of ARGB Integers.
|
|
9
|
+
module Transforms
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
# Green was subtracted from red and blue.
|
|
13
|
+
def add_green(argb)
|
|
14
|
+
argb.map! do |pixel|
|
|
15
|
+
green = (pixel >> 8) & 0xFF
|
|
16
|
+
(pixel & 0xFF00FF00) | (((pixel & 0x00FF00FF) + ((green << 16) | green)) & 0x00FF00FF)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Red was predicted from green, blue from green and red, with
|
|
21
|
+
# signed 3.5 fixed-point multipliers stored per block.
|
|
22
|
+
def cross_color(argb, width, height, bits, elements)
|
|
23
|
+
blocks_wide = ImageStream.subsample(width, bits)
|
|
24
|
+
height.times do |y|
|
|
25
|
+
row = y * width
|
|
26
|
+
column = 0
|
|
27
|
+
while column < width
|
|
28
|
+
element = elements[((y >> bits) * blocks_wide) + (column >> bits)]
|
|
29
|
+
stop = [((column >> bits) + 1) << bits, width].min
|
|
30
|
+
recolor(argb, row + column, row + stop, element)
|
|
31
|
+
column = stop
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
argb
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def recolor(argb, pos, stop, element)
|
|
38
|
+
green_to_red = signed(element & 0xFF)
|
|
39
|
+
green_to_blue = signed((element >> 8) & 0xFF)
|
|
40
|
+
red_to_blue = signed((element >> 16) & 0xFF)
|
|
41
|
+
while pos < stop
|
|
42
|
+
pixel = argb[pos]
|
|
43
|
+
green = signed((pixel >> 8) & 0xFF)
|
|
44
|
+
red = ((pixel >> 16) + ((green_to_red * green) >> 5)) & 0xFF
|
|
45
|
+
blue = (pixel + ((green_to_blue * green) >> 5) + ((red_to_blue * signed(red)) >> 5)) & 0xFF
|
|
46
|
+
argb[pos] = (pixel & 0xFF00FF00) | (red << 16) | blue
|
|
47
|
+
pos += 1
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def signed(byte) = byte > 127 ? byte - 256 : byte
|
|
52
|
+
|
|
53
|
+
# The colour table as stored: every entry is the difference from the
|
|
54
|
+
# one before it. Padded to 256 entries so any index finds a colour
|
|
55
|
+
# (transparent black past the end of the table).
|
|
56
|
+
def color_table(deltas)
|
|
57
|
+
previous = 0
|
|
58
|
+
table = deltas.map { |delta| previous = Predictor.add(delta, previous) }
|
|
59
|
+
table.fill(0, table.size...256)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Green holds an index into the colour table, or 2, 4 or 8 of them
|
|
63
|
+
# packed from the lowest bits up when the table is small.
|
|
64
|
+
def color_indexing(argb, width, height, bits, table)
|
|
65
|
+
return argb.map! { |pixel| table[(pixel >> 8) & 0xFF] } if bits.zero?
|
|
66
|
+
|
|
67
|
+
packed_width = ImageStream.subsample(width, bits)
|
|
68
|
+
depth = 8 >> bits
|
|
69
|
+
mask = (1 << depth) - 1
|
|
70
|
+
out = Array.new(width * height, 0)
|
|
71
|
+
height.times do |y|
|
|
72
|
+
source = y * packed_width
|
|
73
|
+
row = y * width
|
|
74
|
+
width.times do |x|
|
|
75
|
+
packed = (argb[source + (x >> bits)] >> 8) & 0xFF
|
|
76
|
+
out[row + x] = table[(packed >> ((x & ((1 << bits) - 1)) * depth)) & mask]
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
out
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# How many indexes share a pixel for a table of `size` colours, as
|
|
83
|
+
# the power of two.
|
|
84
|
+
def bundling(size)
|
|
85
|
+
return 0 if size > 16
|
|
86
|
+
return 1 if size > 4
|
|
87
|
+
|
|
88
|
+
size > 2 ? 2 : 3
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "zlib"
|
|
4
|
+
|
|
5
|
+
require_relative "webp/container"
|
|
6
|
+
require_relative "webp/bit_reader"
|
|
7
|
+
require_relative "webp/prefix_code"
|
|
8
|
+
require_relative "webp/image_stream"
|
|
9
|
+
require_relative "webp/predictor"
|
|
10
|
+
require_relative "webp/transforms"
|
|
11
|
+
require_relative "webp/lossless"
|
|
12
|
+
|
|
13
|
+
module Stationery
|
|
14
|
+
module Images
|
|
15
|
+
# A lossless WebP (VP8L), decoded when it is loaded and embedded like a
|
|
16
|
+
# PNG with an alpha channel: RGB as a Flate stream, the alpha in a soft
|
|
17
|
+
# mask when any pixel is not opaque. Lossy and animated WebP are refused.
|
|
18
|
+
class WebP
|
|
19
|
+
attr_reader :width, :height
|
|
20
|
+
|
|
21
|
+
def initialize(data)
|
|
22
|
+
container = Container.new(data)
|
|
23
|
+
lossless = Lossless.new(container.bitstream)
|
|
24
|
+
@width = lossless.width
|
|
25
|
+
@height = lossless.height
|
|
26
|
+
if container.canvas && container.canvas != [@width, @height]
|
|
27
|
+
raise UnsupportedImage, "invalid WebP image: canvas and image sizes differ"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
split(lossless.pixels, lossless.alpha?)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def inspect = "#<#{self.class} #{@width}x#{@height}>"
|
|
34
|
+
|
|
35
|
+
# This image scaled down to `width` pixels (a Resampled), memoised per
|
|
36
|
+
# width; the same instance is shared through the image cache.
|
|
37
|
+
def resample(width)
|
|
38
|
+
@resampled ||= {}
|
|
39
|
+
@resampled[width] ||= Resampled.new(pixels, width)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def build(writer)
|
|
43
|
+
dictionary = Images.xobject(@width, @height, :DeviceRGB, 8).merge(Filter: :FlateDecode)
|
|
44
|
+
if @alpha
|
|
45
|
+
mask = Images.xobject(@width, @height, :DeviceGray, 8).merge(Filter: :FlateDecode)
|
|
46
|
+
dictionary[:SMask] = writer.add(PDF::Stream.new(Zlib::Deflate.deflate(@alpha), mask))
|
|
47
|
+
end
|
|
48
|
+
writer.add(PDF::Stream.new(Zlib::Deflate.deflate(@color), dictionary))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
# ARGB pixels as the bytes to embed: three per pixel of colour, and
|
|
54
|
+
# one of alpha unless the image is opaque.
|
|
55
|
+
def split(argb, alpha)
|
|
56
|
+
@color = String.new(capacity: argb.size * 3, encoding: Encoding::BINARY)
|
|
57
|
+
argb.each { |pixel| @color << ((pixel >> 16) & 0xFF) << ((pixel >> 8) & 0xFF) << (pixel & 0xFF) }
|
|
58
|
+
@alpha = argb.map { |pixel| pixel >> 24 }.pack("C*") if alpha && argb.any? { |pixel| pixel < 0xFF000000 }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def pixels
|
|
62
|
+
Pixels.new(width: @width, height: @height, channels: 3, color: rows(@color, @width * 3),
|
|
63
|
+
alpha: @alpha && rows(@alpha, @width))
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def rows(bytes, stride)
|
|
67
|
+
Array.new(@height) { |y| bytes.byteslice(y * stride, stride).bytes }
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stationery
|
|
4
|
+
module Layout
|
|
5
|
+
class Columns
|
|
6
|
+
# Finds the shortest column height at which a pour places everything,
|
|
7
|
+
# so the columns end at nearly the same height.
|
|
8
|
+
#
|
|
9
|
+
# The search runs between a height known to be too short and one known
|
|
10
|
+
# to fit. It starts at the single-column height divided by the column
|
|
11
|
+
# count (what perfectly divisible content needs) and, as the answer is
|
|
12
|
+
# rarely more than a line or two above that, climbs from there in
|
|
13
|
+
# doubling steps until a height fits, then halves the interval. A height
|
|
14
|
+
# that fits is lowered to the tallest column it produced, which is a
|
|
15
|
+
# real break position, so the result is never taller than its content.
|
|
16
|
+
# Fitting is not strictly monotonic in the height (keep_with_next,
|
|
17
|
+
# orphans and widows move content in steps), so the answer is the
|
|
18
|
+
# shortest height found, within TOLERANCE points of the shortest there
|
|
19
|
+
# is. The probe count is capped: the search ends whatever a split
|
|
20
|
+
# returns.
|
|
21
|
+
class Balancer
|
|
22
|
+
TOLERANCE = 0.5
|
|
23
|
+
MAX_PROBES = 40
|
|
24
|
+
|
|
25
|
+
# `limit` is the tallest a column may be; `fallback` is the pour at
|
|
26
|
+
# that height when the caller has it already.
|
|
27
|
+
def initialize(pour, limit: Float::INFINITY, fallback: nil)
|
|
28
|
+
@pour = pour
|
|
29
|
+
@limit = limit
|
|
30
|
+
@fallback = fallback
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def call
|
|
34
|
+
high = [@limit, @pour.total].min
|
|
35
|
+
best = @fallback || @pour.call(high)
|
|
36
|
+
return best unless best.complete? && @pour.count > 1
|
|
37
|
+
|
|
38
|
+
search(best, [high, best.height(@pour.width)].min)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def search(best, high)
|
|
44
|
+
low = 0.0
|
|
45
|
+
probe = @pour.total / @pour.count
|
|
46
|
+
step = TOLERANCE
|
|
47
|
+
MAX_PROBES.times do
|
|
48
|
+
break if high - low <= TOLERANCE || probe >= high
|
|
49
|
+
|
|
50
|
+
poured = @pour.call(probe)
|
|
51
|
+
if poured.complete?
|
|
52
|
+
best = poured
|
|
53
|
+
high = [probe, poured.height(@pour.width)].min
|
|
54
|
+
step = nil
|
|
55
|
+
else
|
|
56
|
+
low = probe
|
|
57
|
+
end
|
|
58
|
+
step *= 2 if step
|
|
59
|
+
probe = [(low + high) / 2.0, step ? low + step : high].min
|
|
60
|
+
end
|
|
61
|
+
best
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stationery
|
|
4
|
+
module Layout
|
|
5
|
+
class Columns
|
|
6
|
+
# What one pour placed: a flow per column that took content, and the
|
|
7
|
+
# flow left over (nil when everything was placed).
|
|
8
|
+
Poured = Data.define(:columns, :rest) do
|
|
9
|
+
def complete? = rest.nil?
|
|
10
|
+
def height(width) = columns.map { |column| column.measure(width) }.max || 0
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Pours a flow into `count` columns of one width: column 1 takes what
|
|
14
|
+
# fits in the height, column 2 continues where it stopped, and so on. A
|
|
15
|
+
# column break is a page break with another height, so every column is
|
|
16
|
+
# cut by Flow#split. A column that places nothing ends the pour, which
|
|
17
|
+
# is what keeps a pour (and every search over pours) finite.
|
|
18
|
+
class Pour
|
|
19
|
+
attr_reader :width, :count
|
|
20
|
+
|
|
21
|
+
def initialize(flow, width, count)
|
|
22
|
+
@flow = flow
|
|
23
|
+
@width = width
|
|
24
|
+
@count = count
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# The height of the flow in a single column.
|
|
28
|
+
def total = @flow.measure(@width)
|
|
29
|
+
|
|
30
|
+
# `fresh:` is true at the top of a page, where a column keeps a child
|
|
31
|
+
# too tall for it rather than placing nothing.
|
|
32
|
+
def call(height, fresh: false)
|
|
33
|
+
columns = []
|
|
34
|
+
rest = @flow.children.empty? ? nil : @flow
|
|
35
|
+
while rest && columns.size < @count
|
|
36
|
+
head, tail = rest.split(@width, height, fresh:)
|
|
37
|
+
break unless head
|
|
38
|
+
|
|
39
|
+
columns << head
|
|
40
|
+
rest = tail
|
|
41
|
+
end
|
|
42
|
+
Poured.new(columns:, rest:)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|