imagescript 1.3.0 → 3.0.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.
- package/LICENSE +4 -1
- package/README.md +64 -26
- package/THIRD_PARTY_NOTICES.md +74 -0
- package/dist/codecs/crc.d.ts +9 -0
- package/dist/codecs/crc.js +29 -0
- package/dist/codecs/exif.d.ts +11 -0
- package/dist/codecs/exif.js +27 -0
- package/dist/codecs/gif.d.ts +44 -0
- package/dist/codecs/gif.js +401 -0
- package/dist/codecs/jpeg.d.ts +35 -0
- package/dist/codecs/jpeg.js +980 -0
- package/dist/codecs/png.d.ts +44 -0
- package/dist/codecs/png.js +442 -0
- package/dist/codecs/quantize.d.ts +25 -0
- package/dist/codecs/quantize.js +302 -0
- package/dist/codecs/tiff.d.ts +6 -0
- package/dist/codecs/tiff.js +546 -0
- package/dist/codecs/zlib.d.ts +22 -0
- package/dist/codecs/zlib.js +607 -0
- package/dist/frame.d.ts +57 -0
- package/dist/frame.js +65 -0
- package/dist/gif.d.ts +44 -0
- package/dist/gif.js +93 -0
- package/dist/image-type.d.ts +20 -0
- package/dist/image-type.js +49 -0
- package/dist/image.d.ts +521 -0
- package/dist/image.js +1118 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +13 -0
- package/dist/package.json +1 -0
- package/dist/render/font.d.ts +63 -0
- package/dist/render/font.js +857 -0
- package/dist/render/svg.d.ts +11 -0
- package/dist/render/svg.js +1717 -0
- package/dist/render/text.d.ts +61 -0
- package/dist/render/text.js +317 -0
- package/dist/text-layout.d.ts +46 -0
- package/dist/text-layout.js +35 -0
- package/dist/util/blur.d.ts +13 -0
- package/dist/util/blur.js +146 -0
- package/dist/util/mem.d.ts +10 -0
- package/dist/util/mem.js +23 -0
- package/dist/util/resample.d.ts +12 -0
- package/dist/util/resample.js +99 -0
- package/dist/util/utf8.d.ts +5 -0
- package/dist/util/utf8.js +55 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +1 -0
- package/licenses/Apache-2.0.txt +202 -0
- package/licenses/README.ijg +260 -0
- package/package.json +31 -7
- package/ImageScript.d.ts +0 -235
- package/ImageScript.js +0 -1669
- package/bench/_.mjs +0 -166
- package/bench/all.mjs +0 -4
- package/bench/blur.mjs +0 -59
- package/bench/flip.mjs +0 -63
- package/bench/overlay.mjs +0 -35
- package/bench/package.json +0 -7
- package/bench/readme.md +0 -64
- package/bench/resize.mjs +0 -52
- package/codecs/LICENSE +0 -21
- package/codecs/lib.d.ts +0 -50
- package/codecs/lib.js +0 -6
- package/codecs/lib.mjs +0 -1
- package/codecs/node/bin/arm64-darwin.node +0 -0
- package/codecs/node/bin/arm64-linux.node +0 -0
- package/codecs/node/bin/x64-darwin.node +0 -0
- package/codecs/node/bin/x64-linux.node +0 -0
- package/codecs/node/bin/x64-win32.node +0 -0
- package/codecs/node/index.js +0 -3
- package/codecs/node/index.mjs +0 -6
- package/codecs/package.json +0 -14
- package/codecs/readme.md +0 -38
- package/codecs/tests/gif.js +0 -0
- package/codecs/tests/jpeg.js +0 -0
- package/codecs/tests/png.js +0 -0
- package/codecs/tests/results/gif/1024.nis.gif +0 -0
- package/codecs/tests/results/gif/1024.skyra.gif +0 -0
- package/codecs/tests/results/gif/256.nis.gif +0 -0
- package/codecs/tests/results/gif/256.skyra.gif +0 -0
- package/codecs/tests/results/jpeg/256.jpegjs.jpeg +0 -0
- package/codecs/tests/results/jpeg/256.nis.jpeg +0 -0
- package/codecs/tests/results/jpeg/256.sharp.jpeg +0 -0
- package/codecs/tests/results/jpeg/512.jpegjs.jpeg +0 -0
- package/codecs/tests/results/jpeg/512.nis.jpeg +0 -0
- package/codecs/tests/results/jpeg/512.sharp.jpeg +0 -0
- package/codecs/tests/results/jpeg/64.jpegjs.jpeg +0 -0
- package/codecs/tests/results/jpeg/64.nis.jpeg +0 -0
- package/codecs/tests/results/jpeg/64.sharp.jpeg +0 -0
- package/codecs/tests/results/png/256.jis.png +0 -0
- package/codecs/tests/results/png/256.nis.png +0 -0
- package/codecs/tests/results/png/256.sharp.png +0 -0
- package/codecs/tests/results/png/256.upng.png +0 -0
- package/codecs/tests/results/png/512.jis.png +0 -0
- package/codecs/tests/results/png/512.nis.png +0 -0
- package/codecs/tests/results/png/512.sharp.png +0 -0
- package/codecs/tests/results/png/512.upng.png +0 -0
- package/codecs/tests/results/png/64.jis.png +0 -0
- package/codecs/tests/results/png/64.nis.png +0 -0
- package/codecs/tests/results/png/64.sharp.png +0 -0
- package/codecs/tests/results/png/64.upng.png +0 -0
- package/codecs/tests/webp.js +0 -0
- package/codecs/wasm/bin/codecs.wasm +0 -0
- package/codecs/wasm/bin/simd.wasm +0 -0
- package/codecs/wasm/bundle/browser.js +0 -1
- package/codecs/wasm/index.js +0 -1
- package/codecs/wasm/index.mjs +0 -1
- package/png/README.md +0 -1
- package/png/node.js +0 -1137
- package/png/src/crc.mjs +0 -49
- package/png/src/mem.mjs +0 -24
- package/png/src/png.mjs +0 -254
- package/png/src/zlib.mjs +0 -618
- package/tsconfig.json +0 -21
- package/utils/mem.js +0 -26
- package/v2/codecs/magic.js +0 -25
- package/v2/framebuffer.d.ts +0 -67
- package/v2/framebuffer.js +0 -2352
- package/v2/framebuffer.mjs +0 -126
- package/v2/framebuffer.rs +0 -1264
- package/v2/index.d.ts +0 -84
- package/v2/index.js +0 -261
- package/v2/ops/blur.mjs +0 -303
- package/v2/ops/color.mjs +0 -277
- package/v2/ops/crop.mjs +0 -64
- package/v2/ops/fill.mjs +0 -33
- package/v2/ops/flip.mjs +0 -33
- package/v2/ops/index.mjs +0 -9
- package/v2/ops/iterator.mjs +0 -34
- package/v2/ops/overlay.mjs +0 -63
- package/v2/ops/resize.mjs +0 -166
- package/v2/ops/rotate.mjs +0 -117
- package/v2/util/mem.mjs +0 -24
- package/wasm/any/font.wasm +0 -0
- package/wasm/any/gif.wasm +0 -0
- package/wasm/any/jpeg.wasm +0 -0
- package/wasm/any/png.wasm +0 -0
- package/wasm/any/svg.wasm +0 -0
- package/wasm/any/tiff.wasm +0 -0
- package/wasm/node/font.js +0 -142
- package/wasm/node/gif.js +0 -76
- package/wasm/node/jpeg.js +0 -92
- package/wasm/node/png.js +0 -49
- package/wasm/node/svg.js +0 -56
- package/wasm/node/tiff.js +0 -47
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* The Rasterizer class is translated from font-rs (https://github.com/raphlinus/font-rs)
|
|
3
|
+
* Copyright 2015 Google Inc., Apache License 2.0 (see THIRD_PARTY_NOTICES.md and licenses/Apache-2.0.txt)
|
|
4
|
+
*/
|
|
5
|
+
import { PathCommand } from './font.js';
|
|
6
|
+
export interface Layout {
|
|
7
|
+
maxWidth: number;
|
|
8
|
+
maxHeight: number;
|
|
9
|
+
wrapStyle: 'word' | 'char';
|
|
10
|
+
horizontalAlign: 'left' | 'center' | 'right';
|
|
11
|
+
verticalAlign: 'top' | 'middle' | 'bottom';
|
|
12
|
+
wrapHardBreaks: boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface GlyphBitmap {
|
|
15
|
+
data: Uint8Array;
|
|
16
|
+
width: number;
|
|
17
|
+
height: number;
|
|
18
|
+
/** offset from the pen position */
|
|
19
|
+
left: number;
|
|
20
|
+
/** offset above the baseline */
|
|
21
|
+
top: number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Lays out and rasterizes text
|
|
25
|
+
* Lines start at ceil(ascender) and advance by ceil(ascender - descender + lineGap); glyphs are placed with
|
|
26
|
+
* subpixel precision using their advance widths and the font's kerning; the canvas spans from the origin to the
|
|
27
|
+
* furthest glyph edge
|
|
28
|
+
* @param {Uint8Array} data TrueType/OpenType font
|
|
29
|
+
* @param {number} size font size in pixels per em
|
|
30
|
+
* @param {string} text
|
|
31
|
+
* @param {Layout} layout
|
|
32
|
+
* @return {{width: number, height: number, coverage: Uint8Array}}
|
|
33
|
+
*/
|
|
34
|
+
export declare function render(data: Uint8Array, size: number, text: string, layout: Layout): {
|
|
35
|
+
width: number;
|
|
36
|
+
height: number;
|
|
37
|
+
coverage: Uint8Array;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Rasterizes a glyph outline into an 8-bit coverage bitmap
|
|
41
|
+
* @param {Array} path outline in font units (y up)
|
|
42
|
+
* @param {number} scale pixels per font unit
|
|
43
|
+
* @param {number} [offset=0] horizontal subpixel offset
|
|
44
|
+
* @return {{data: Uint8Array, width: number, height: number, left: number, top: number}|null}
|
|
45
|
+
* left/top position the bitmap relative to the pen position on the baseline (top is measured upwards)
|
|
46
|
+
*/
|
|
47
|
+
export declare function rasterizeGlyph(path: PathCommand[], scale: number, offset?: number): GlyphBitmap | null;
|
|
48
|
+
/**
|
|
49
|
+
* Signed area coverage rasterizer: every line segment adds its area to an accumulation buffer,
|
|
50
|
+
* a prefix sum over each row yields the coverage
|
|
51
|
+
*/
|
|
52
|
+
export declare class Rasterizer {
|
|
53
|
+
width: number;
|
|
54
|
+
height: number;
|
|
55
|
+
accumulation: Float32Array;
|
|
56
|
+
constructor(width: number, height: number);
|
|
57
|
+
line(x0: number, y0: number, x1: number, y1: number): void;
|
|
58
|
+
quad(x0: number, y0: number, x1: number, y1: number, x2: number, y2: number): void;
|
|
59
|
+
cubic(x0: number, y0: number, x1: number, y1: number, x2: number, y2: number, x3: number, y3: number): void;
|
|
60
|
+
coverage(): Uint8Array;
|
|
61
|
+
}
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* The Rasterizer class is translated from font-rs (https://github.com/raphlinus/font-rs)
|
|
3
|
+
* Copyright 2015 Google Inc., Apache License 2.0 (see THIRD_PARTY_NOTICES.md and licenses/Apache-2.0.txt)
|
|
4
|
+
*/
|
|
5
|
+
import { Font } from "./font.js";
|
|
6
|
+
const SUBPIXELS = 4;
|
|
7
|
+
const fonts = new WeakMap();
|
|
8
|
+
function loadFont(data) {
|
|
9
|
+
let regions = fonts.get(data.buffer);
|
|
10
|
+
if (!regions)
|
|
11
|
+
fonts.set(data.buffer, regions = new Map());
|
|
12
|
+
const key = `${data.byteOffset}:${data.byteLength}`;
|
|
13
|
+
let font = regions.get(key);
|
|
14
|
+
if (!font)
|
|
15
|
+
regions.set(key, font = new Font(data));
|
|
16
|
+
return font;
|
|
17
|
+
}
|
|
18
|
+
export function render(data, size, text, layout) {
|
|
19
|
+
const font = loadFont(data);
|
|
20
|
+
const scale = size / font.unitsPerEm;
|
|
21
|
+
const ascent = Math.ceil(font.ascender * scale);
|
|
22
|
+
const lineHeight = Math.ceil((font.ascender - font.descender + font.lineGap) * scale);
|
|
23
|
+
const glyphs = new Map();
|
|
24
|
+
const glyph = (codepoint) => {
|
|
25
|
+
let g = glyphs.get(codepoint);
|
|
26
|
+
if (!g) {
|
|
27
|
+
const index = font.glyphIndex(codepoint);
|
|
28
|
+
const path = font.outline(index);
|
|
29
|
+
g = { index, advance: font.advance(index) * scale, ink: inkExtent(path, scale), bitmaps: [] };
|
|
30
|
+
glyphs.set(codepoint, g);
|
|
31
|
+
}
|
|
32
|
+
return g;
|
|
33
|
+
};
|
|
34
|
+
const kerning = font.kerning;
|
|
35
|
+
const kern = kerning ? (left, right) => kerning(left.index, right.index) * scale : () => 0;
|
|
36
|
+
const lines = wrap(text, layout, glyph, kern);
|
|
37
|
+
let visible = lines;
|
|
38
|
+
if (isFinite(layout.maxHeight)) {
|
|
39
|
+
const count = Math.max(1, Math.floor(layout.maxHeight / lineHeight));
|
|
40
|
+
if (count < lines.length) {
|
|
41
|
+
const start = layout.verticalAlign === 'bottom' ? lines.length - count
|
|
42
|
+
: layout.verticalAlign === 'middle' ? lines.length - count >> 1 : 0;
|
|
43
|
+
visible = lines.slice(start, start + count);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const width = Math.ceil(Math.max(0, ...visible.map(line => line.extent)));
|
|
47
|
+
const height = visible.length * lineHeight;
|
|
48
|
+
const coverage = new Uint8Array(width * height);
|
|
49
|
+
visible.forEach((line, n) => {
|
|
50
|
+
const offset = layout.horizontalAlign === 'right' ? width - line.extent
|
|
51
|
+
: layout.horizontalAlign === 'center' ? (width - line.extent) / 2 : 0;
|
|
52
|
+
const baseline = ascent + n * lineHeight;
|
|
53
|
+
for (const { x, glyph: g } of line.glyphs) {
|
|
54
|
+
if (!g.ink)
|
|
55
|
+
continue;
|
|
56
|
+
const position = Math.round((offset + x) * SUBPIXELS) / SUBPIXELS;
|
|
57
|
+
const pixel = Math.floor(position);
|
|
58
|
+
const subpixel = Math.round((position - pixel) * SUBPIXELS);
|
|
59
|
+
if (g.bitmaps[subpixel] === undefined)
|
|
60
|
+
g.bitmaps[subpixel] = rasterizeGlyph(font.outline(g.index), scale, subpixel / SUBPIXELS);
|
|
61
|
+
const bitmap = g.bitmaps[subpixel];
|
|
62
|
+
if (bitmap)
|
|
63
|
+
blit(coverage, width, height, bitmap, pixel + bitmap.left, baseline - bitmap.top);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
return { width, height, coverage };
|
|
67
|
+
}
|
|
68
|
+
function inkExtent(path, scale) {
|
|
69
|
+
let left = Infinity, right = -Infinity;
|
|
70
|
+
for (const command of path)
|
|
71
|
+
for (let i = 1; i < command.length; i += 2) {
|
|
72
|
+
left = Math.min(left, command[i] * scale);
|
|
73
|
+
right = Math.max(right, command[i] * scale);
|
|
74
|
+
}
|
|
75
|
+
return right > left ? { left, right } : null;
|
|
76
|
+
}
|
|
77
|
+
function wrap(text, layout, glyph, kern) {
|
|
78
|
+
const lines = [];
|
|
79
|
+
const paragraphs = layout.wrapHardBreaks ? text.split(/\r\n|\r|\n/) : [text.replace(/\r\n|\r|\n/g, '')];
|
|
80
|
+
const edge = (x, g) => g.ink ? x + g.ink.right : 0;
|
|
81
|
+
for (const paragraph of paragraphs) {
|
|
82
|
+
let line = { glyphs: [], extent: 0, soft: false };
|
|
83
|
+
let x = 0;
|
|
84
|
+
let breakAt = -1;
|
|
85
|
+
const next = (g) => line.glyphs.length ? x + kern(line.glyphs[line.glyphs.length - 1].glyph, g) : x;
|
|
86
|
+
const add = (g) => {
|
|
87
|
+
const position = next(g);
|
|
88
|
+
line.glyphs.push({ x: position, glyph: g });
|
|
89
|
+
line.extent = Math.max(line.extent, edge(position, g));
|
|
90
|
+
x = position + g.advance;
|
|
91
|
+
};
|
|
92
|
+
const newLine = () => {
|
|
93
|
+
lines.push(line);
|
|
94
|
+
line = { glyphs: [], extent: 0, soft: true };
|
|
95
|
+
x = 0;
|
|
96
|
+
breakAt = -1;
|
|
97
|
+
};
|
|
98
|
+
for (const char of paragraph) {
|
|
99
|
+
const codepoint = char.codePointAt(0);
|
|
100
|
+
const g = glyph(codepoint);
|
|
101
|
+
const space = codepoint === 0x20 || codepoint === 0x09;
|
|
102
|
+
if (!space && line.glyphs.length && edge(next(g), g) > layout.maxWidth) {
|
|
103
|
+
if (layout.wrapStyle === 'word' && breakAt > 0) {
|
|
104
|
+
const word = line.glyphs.splice(breakAt);
|
|
105
|
+
line.extent = measure(line.glyphs, edge);
|
|
106
|
+
newLine();
|
|
107
|
+
for (const item of word)
|
|
108
|
+
add(item.glyph);
|
|
109
|
+
}
|
|
110
|
+
else
|
|
111
|
+
newLine();
|
|
112
|
+
}
|
|
113
|
+
if (space && line.soft && !line.glyphs.length)
|
|
114
|
+
continue;
|
|
115
|
+
add(g);
|
|
116
|
+
if (space)
|
|
117
|
+
breakAt = line.glyphs.length;
|
|
118
|
+
}
|
|
119
|
+
lines.push(line);
|
|
120
|
+
}
|
|
121
|
+
return lines;
|
|
122
|
+
}
|
|
123
|
+
function measure(glyphs, edge) {
|
|
124
|
+
let extent = 0;
|
|
125
|
+
for (const { x, glyph } of glyphs)
|
|
126
|
+
extent = Math.max(extent, edge(x, glyph));
|
|
127
|
+
return extent;
|
|
128
|
+
}
|
|
129
|
+
function blit(target, width, height, bitmap, x0, y0) {
|
|
130
|
+
for (let y = 0; y < bitmap.height; y++) {
|
|
131
|
+
const ty = y0 + y;
|
|
132
|
+
if (ty < 0 || ty >= height)
|
|
133
|
+
continue;
|
|
134
|
+
for (let x = 0; x < bitmap.width; x++) {
|
|
135
|
+
const tx = x0 + x;
|
|
136
|
+
if (tx < 0 || tx >= width)
|
|
137
|
+
continue;
|
|
138
|
+
const v = bitmap.data[y * bitmap.width + x];
|
|
139
|
+
if (v) {
|
|
140
|
+
const o = ty * width + tx;
|
|
141
|
+
target[o] = Math.min(255, target[o] + v);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
export function rasterizeGlyph(path, scale, offset = 0) {
|
|
147
|
+
if (!path.length)
|
|
148
|
+
return null;
|
|
149
|
+
let xmin = Infinity, ymin = Infinity, xmax = -Infinity, ymax = -Infinity;
|
|
150
|
+
for (const command of path)
|
|
151
|
+
for (let i = 1; i < command.length; i += 2) {
|
|
152
|
+
const x = command[i] * scale + offset, y = command[i + 1] * scale;
|
|
153
|
+
if (x < xmin)
|
|
154
|
+
xmin = x;
|
|
155
|
+
if (x > xmax)
|
|
156
|
+
xmax = x;
|
|
157
|
+
if (y < ymin)
|
|
158
|
+
ymin = y;
|
|
159
|
+
if (y > ymax)
|
|
160
|
+
ymax = y;
|
|
161
|
+
}
|
|
162
|
+
const left = Math.floor(xmin), right = Math.ceil(xmax);
|
|
163
|
+
const bottom = Math.floor(ymin), top = Math.ceil(ymax);
|
|
164
|
+
const width = right - left, height = top - bottom;
|
|
165
|
+
if (width <= 0 || height <= 0)
|
|
166
|
+
return null;
|
|
167
|
+
const raster = new Rasterizer(width, height);
|
|
168
|
+
const tx = (x) => x * scale + offset - left;
|
|
169
|
+
const ty = (y) => top - y * scale;
|
|
170
|
+
let sx = 0, sy = 0, cx = 0, cy = 0;
|
|
171
|
+
for (const command of path) {
|
|
172
|
+
switch (command[0]) {
|
|
173
|
+
case 'M':
|
|
174
|
+
sx = cx = tx(command[1]);
|
|
175
|
+
sy = cy = ty(command[2]);
|
|
176
|
+
break;
|
|
177
|
+
case 'L': {
|
|
178
|
+
const x = tx(command[1]), y = ty(command[2]);
|
|
179
|
+
raster.line(cx, cy, x, y);
|
|
180
|
+
cx = x;
|
|
181
|
+
cy = y;
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
case 'Q': {
|
|
185
|
+
const x = tx(command[3]), y = ty(command[4]);
|
|
186
|
+
raster.quad(cx, cy, tx(command[1]), ty(command[2]), x, y);
|
|
187
|
+
cx = x;
|
|
188
|
+
cy = y;
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
case 'C': {
|
|
192
|
+
const x = tx(command[5]), y = ty(command[6]);
|
|
193
|
+
raster.cubic(cx, cy, tx(command[1]), ty(command[2]), tx(command[3]), ty(command[4]), x, y);
|
|
194
|
+
cx = x;
|
|
195
|
+
cy = y;
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
case 'Z':
|
|
199
|
+
if (cx !== sx || cy !== sy)
|
|
200
|
+
raster.line(cx, cy, sx, sy);
|
|
201
|
+
cx = sx;
|
|
202
|
+
cy = sy;
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return { data: raster.coverage(), width, height, left, top };
|
|
207
|
+
}
|
|
208
|
+
export class Rasterizer {
|
|
209
|
+
width;
|
|
210
|
+
height;
|
|
211
|
+
accumulation;
|
|
212
|
+
constructor(width, height) {
|
|
213
|
+
this.width = width;
|
|
214
|
+
this.height = height;
|
|
215
|
+
this.accumulation = new Float32Array(width * height + 4);
|
|
216
|
+
}
|
|
217
|
+
line(x0, y0, x1, y1) {
|
|
218
|
+
if (y0 === y1)
|
|
219
|
+
return;
|
|
220
|
+
let dir = 1;
|
|
221
|
+
if (y0 > y1) {
|
|
222
|
+
dir = -1;
|
|
223
|
+
[x0, y0, x1, y1] = [x1, y1, x0, y0];
|
|
224
|
+
}
|
|
225
|
+
const a = this.accumulation;
|
|
226
|
+
const w = this.width;
|
|
227
|
+
const dxdy = (x1 - x0) / (y1 - y0);
|
|
228
|
+
let x = x0;
|
|
229
|
+
if (y0 < 0)
|
|
230
|
+
x -= y0 * dxdy;
|
|
231
|
+
const yEnd = Math.min(this.height, Math.ceil(y1));
|
|
232
|
+
for (let y = Math.max(0, y0 | 0); y < yEnd; y++) {
|
|
233
|
+
const lineStart = y * w;
|
|
234
|
+
const dy = Math.min(y + 1, y1) - Math.max(y, y0);
|
|
235
|
+
const xNext = x + dxdy * dy;
|
|
236
|
+
const d = dy * dir;
|
|
237
|
+
const xa = x < xNext ? x : xNext, xb = x < xNext ? xNext : x;
|
|
238
|
+
const xaFloor = Math.floor(xa);
|
|
239
|
+
const xai = xaFloor;
|
|
240
|
+
const xbCeil = Math.ceil(xb);
|
|
241
|
+
const xbi = xbCeil;
|
|
242
|
+
const index = lineStart + xai;
|
|
243
|
+
if (index >= 0) {
|
|
244
|
+
if (xbi <= xai + 1) {
|
|
245
|
+
const xmf = 0.5 * (x + xNext) - xaFloor;
|
|
246
|
+
a[index] += d - d * xmf;
|
|
247
|
+
a[index + 1] += d * xmf;
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
const s = 1 / (xb - xa);
|
|
251
|
+
const xaf = xa - xaFloor;
|
|
252
|
+
const a0 = 0.5 * s * (1 - xaf) * (1 - xaf);
|
|
253
|
+
const xbf = xb - xbCeil + 1;
|
|
254
|
+
const am = 0.5 * s * xbf * xbf;
|
|
255
|
+
a[index] += d * a0;
|
|
256
|
+
if (xbi === xai + 2)
|
|
257
|
+
a[index + 1] += d * (1 - a0 - am);
|
|
258
|
+
else {
|
|
259
|
+
const a1 = s * (1.5 - xaf);
|
|
260
|
+
a[index + 1] += d * (a1 - a0);
|
|
261
|
+
for (let xi = xai + 2; xi < xbi - 1; xi++)
|
|
262
|
+
a[lineStart + xi] += d * s;
|
|
263
|
+
const a2 = a1 + (xbi - xai - 3) * s;
|
|
264
|
+
a[lineStart + xbi - 1] += d * (1 - a2 - am);
|
|
265
|
+
}
|
|
266
|
+
a[lineStart + xbi] += d * am;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
x = xNext;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
quad(x0, y0, x1, y1, x2, y2) {
|
|
273
|
+
const devx = x0 - 2 * x1 + x2;
|
|
274
|
+
const devy = y0 - 2 * y1 + y2;
|
|
275
|
+
const devsq = devx * devx + devy * devy;
|
|
276
|
+
if (devsq < 0.333)
|
|
277
|
+
return this.line(x0, y0, x2, y2);
|
|
278
|
+
const n = 1 + Math.floor(Math.sqrt(Math.sqrt(3 * devsq)));
|
|
279
|
+
let px = x0, py = y0;
|
|
280
|
+
for (let i = 1; i <= n; i++) {
|
|
281
|
+
const t = i / n, mt = 1 - t;
|
|
282
|
+
const x = mt * mt * x0 + 2 * mt * t * x1 + t * t * x2;
|
|
283
|
+
const y = mt * mt * y0 + 2 * mt * t * y1 + t * t * y2;
|
|
284
|
+
this.line(px, py, x, y);
|
|
285
|
+
px = x;
|
|
286
|
+
py = y;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
cubic(x0, y0, x1, y1, x2, y2, x3, y3) {
|
|
290
|
+
const dx = Math.max(Math.abs(x0 - 2 * x1 + x2), Math.abs(x1 - 2 * x2 + x3));
|
|
291
|
+
const dy = Math.max(Math.abs(y0 - 2 * y1 + y2), Math.abs(y1 - 2 * y2 + y3));
|
|
292
|
+
const devsq = dx * dx + dy * dy;
|
|
293
|
+
if (devsq < 0.333)
|
|
294
|
+
return this.line(x0, y0, x3, y3);
|
|
295
|
+
const n = 1 + Math.floor(Math.sqrt(Math.sqrt(6 * devsq)));
|
|
296
|
+
let px = x0, py = y0;
|
|
297
|
+
for (let i = 1; i <= n; i++) {
|
|
298
|
+
const t = i / n, mt = 1 - t;
|
|
299
|
+
const x = mt * mt * mt * x0 + 3 * mt * mt * t * x1 + 3 * mt * t * t * x2 + t * t * t * x3;
|
|
300
|
+
const y = mt * mt * mt * y0 + 3 * mt * mt * t * y1 + 3 * mt * t * t * y2 + t * t * t * y3;
|
|
301
|
+
this.line(px, py, x, y);
|
|
302
|
+
px = x;
|
|
303
|
+
py = y;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
coverage() {
|
|
307
|
+
const length = this.width * this.height;
|
|
308
|
+
const out = new Uint8Array(length);
|
|
309
|
+
let acc = 0;
|
|
310
|
+
for (let i = 0; i < length; i++) {
|
|
311
|
+
acc += this.accumulation[i];
|
|
312
|
+
const v = acc < 0 ? -acc : acc;
|
|
313
|
+
out[i] = v >= 1 ? 255 : v * 255 + 0.5 | 0;
|
|
314
|
+
}
|
|
315
|
+
return out;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** Where lines are wrapped: between words or at any character */
|
|
2
|
+
export type WrapStyle = 'word' | 'char';
|
|
3
|
+
/** Alignment of the lines relative to the widest line */
|
|
4
|
+
export type HorizontalAlign = 'left' | 'center' | 'right';
|
|
5
|
+
/** Which lines to keep when the text is cut off at `maxHeight` */
|
|
6
|
+
export type VerticalAlign = 'top' | 'middle' | 'bottom';
|
|
7
|
+
/** Options of {@link TextLayout} */
|
|
8
|
+
export interface TextLayoutOptions {
|
|
9
|
+
/** Longer lines are wrapped. Default: `Infinity` */
|
|
10
|
+
maxWidth?: number;
|
|
11
|
+
/** Lines that don't fit are cut off. Default: `Infinity` */
|
|
12
|
+
maxHeight?: number;
|
|
13
|
+
/** Whether to wrap between words or at any character. Default: `'word'` */
|
|
14
|
+
wrapStyle?: WrapStyle;
|
|
15
|
+
/**
|
|
16
|
+
* Alignment of the lines relative to the widest line. Default: `'left'`
|
|
17
|
+
* (before 3.0 this was called verticalAlign, values are accepted under either name)
|
|
18
|
+
*/
|
|
19
|
+
horizontalAlign?: HorizontalAlign | VerticalAlign;
|
|
20
|
+
/**
|
|
21
|
+
* Which lines to keep when cutting off at `maxHeight`. Default: `'top'`
|
|
22
|
+
* (before 3.0 this was called horizontalAlign, values are accepted under either name)
|
|
23
|
+
*/
|
|
24
|
+
verticalAlign?: VerticalAlign | HorizontalAlign;
|
|
25
|
+
/** Whether to break lines at newline characters. Default: `true` */
|
|
26
|
+
wrapHardBreaks?: boolean;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Layout options for {@link Image.renderText}
|
|
30
|
+
*/
|
|
31
|
+
export declare class TextLayout {
|
|
32
|
+
/** Longer lines are wrapped */
|
|
33
|
+
maxWidth: number;
|
|
34
|
+
/** Lines that don't fit are cut off */
|
|
35
|
+
maxHeight: number;
|
|
36
|
+
/** Whether to wrap between words or at any character */
|
|
37
|
+
wrapStyle: WrapStyle;
|
|
38
|
+
/** Alignment of the lines relative to the widest line */
|
|
39
|
+
horizontalAlign: HorizontalAlign;
|
|
40
|
+
/** Which lines to keep when cutting off at `maxHeight` */
|
|
41
|
+
verticalAlign: VerticalAlign;
|
|
42
|
+
/** Whether to break lines at newline characters */
|
|
43
|
+
wrapHardBreaks: boolean;
|
|
44
|
+
/** @param options See {@link TextLayoutOptions} */
|
|
45
|
+
constructor(options?: TextLayoutOptions);
|
|
46
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const HORIZONTAL = ['left', 'center', 'right'];
|
|
2
|
+
const VERTICAL = ['top', 'middle', 'bottom'];
|
|
3
|
+
export class TextLayout {
|
|
4
|
+
maxWidth;
|
|
5
|
+
maxHeight;
|
|
6
|
+
wrapStyle;
|
|
7
|
+
horizontalAlign = 'left';
|
|
8
|
+
verticalAlign = 'top';
|
|
9
|
+
wrapHardBreaks;
|
|
10
|
+
constructor(options = {}) {
|
|
11
|
+
const { maxWidth, maxHeight, wrapStyle, verticalAlign, horizontalAlign, wrapHardBreaks } = options;
|
|
12
|
+
this.maxWidth = maxWidth || Infinity;
|
|
13
|
+
if (isNaN(this.maxWidth) || this.maxWidth < 1)
|
|
14
|
+
throw new RangeError('Invalid maxWidth');
|
|
15
|
+
this.maxHeight = maxHeight || Infinity;
|
|
16
|
+
if (isNaN(this.maxHeight) || this.maxHeight < 1)
|
|
17
|
+
throw new RangeError('Invalid maxHeight');
|
|
18
|
+
this.wrapStyle = wrapStyle || 'word';
|
|
19
|
+
if (!['word', 'char'].includes(this.wrapStyle))
|
|
20
|
+
throw new RangeError('Invalid wrapStyle');
|
|
21
|
+
for (const value of [horizontalAlign, verticalAlign]) {
|
|
22
|
+
if (value === undefined)
|
|
23
|
+
continue;
|
|
24
|
+
if (HORIZONTAL.includes(value))
|
|
25
|
+
this.horizontalAlign = value;
|
|
26
|
+
else if (VERTICAL.includes(value))
|
|
27
|
+
this.verticalAlign = value;
|
|
28
|
+
else
|
|
29
|
+
throw new RangeError(`Invalid alignment: ${value}`);
|
|
30
|
+
}
|
|
31
|
+
this.wrapHardBreaks = typeof wrapHardBreaks === 'undefined' ? true : wrapHardBreaks;
|
|
32
|
+
if (typeof this.wrapHardBreaks !== 'boolean')
|
|
33
|
+
throw new TypeError('Invalid wrapHardBreaks');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blurs premultiplied RGBA float pixels in place
|
|
3
|
+
* @param sigma standard deviation of the gaussian in pixels
|
|
4
|
+
*/
|
|
5
|
+
export declare function gaussianBlur(pixels: Float32Array, width: number, height: number, sigma: number): void;
|
|
6
|
+
/**
|
|
7
|
+
* Converts 8-bit RGBA to premultiplied floats
|
|
8
|
+
*/
|
|
9
|
+
export declare function premultiply(rgba: Uint8ClampedArray): Float32Array;
|
|
10
|
+
/**
|
|
11
|
+
* Converts premultiplied floats back to 8-bit RGBA (the clamped output rounds)
|
|
12
|
+
*/
|
|
13
|
+
export declare function unpremultiply(pixels: Float32Array, out: Uint8ClampedArray): void;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
function boxSizes(sigma) {
|
|
2
|
+
const n = 3;
|
|
3
|
+
let lower = Math.floor(Math.sqrt(12 * sigma * sigma / n + 1));
|
|
4
|
+
if (lower % 2 === 0)
|
|
5
|
+
lower--;
|
|
6
|
+
const upper = lower + 2;
|
|
7
|
+
const m = Math.round((12 * sigma * sigma - n * lower * lower - 4 * n * lower - 3 * n) / (-4 * lower - 4));
|
|
8
|
+
return Array.from({ length: n }, (_, i) => i < m ? lower : upper);
|
|
9
|
+
}
|
|
10
|
+
function boxPass(src, dst, count, length, lineStride, step, radius) {
|
|
11
|
+
const scale = 1 / (2 * radius + 1);
|
|
12
|
+
const last = (length - 1) * step;
|
|
13
|
+
for (let line = 0; line < count; line++) {
|
|
14
|
+
const base = line * lineStride;
|
|
15
|
+
const first = base, end = base + last;
|
|
16
|
+
let r = 0, g = 0, b = 0, a = 0;
|
|
17
|
+
for (let k = -radius; k <= radius; k++) {
|
|
18
|
+
const o = k < 0 ? first : Math.min(end, base + k * step);
|
|
19
|
+
r += src[o];
|
|
20
|
+
g += src[o + 1];
|
|
21
|
+
b += src[o + 2];
|
|
22
|
+
a += src[o + 3];
|
|
23
|
+
}
|
|
24
|
+
for (let i = 0, o = base; i < length; i++, o += step) {
|
|
25
|
+
dst[o] = r * scale;
|
|
26
|
+
dst[o + 1] = g * scale;
|
|
27
|
+
dst[o + 2] = b * scale;
|
|
28
|
+
dst[o + 3] = a * scale;
|
|
29
|
+
const add = Math.min(end, o + (radius + 1) * step), remove = Math.max(first, o - radius * step);
|
|
30
|
+
r += src[add] - src[remove];
|
|
31
|
+
g += src[add + 1] - src[remove + 1];
|
|
32
|
+
b += src[add + 2] - src[remove + 2];
|
|
33
|
+
a += src[add + 3] - src[remove + 3];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function boxPassRows(src, dst, width, height, radius) {
|
|
38
|
+
const stride = width * 4;
|
|
39
|
+
const scale = 1 / (2 * radius + 1);
|
|
40
|
+
const sum = new Float64Array(stride);
|
|
41
|
+
const row = (y) => (y < 0 ? 0 : y >= height ? height - 1 : y) * stride;
|
|
42
|
+
for (let k = -radius; k <= radius; k++) {
|
|
43
|
+
const o = row(k);
|
|
44
|
+
for (let x = 0; x < stride; x++)
|
|
45
|
+
sum[x] += src[o + x];
|
|
46
|
+
}
|
|
47
|
+
for (let y = 0; y < height; y++) {
|
|
48
|
+
const o = y * stride, add = row(y + radius + 1), remove = row(y - radius);
|
|
49
|
+
for (let x = 0; x < stride; x++) {
|
|
50
|
+
dst[o + x] = sum[x] * scale;
|
|
51
|
+
sum[x] += src[add + x] - src[remove + x];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function gaussianKernel(sigma) {
|
|
56
|
+
const radius = Math.ceil(3 * sigma);
|
|
57
|
+
const kernel = new Float64Array(2 * radius + 1);
|
|
58
|
+
let total = 0;
|
|
59
|
+
for (let i = -radius; i <= radius; i++)
|
|
60
|
+
total += kernel[i + radius] = Math.exp(-i * i / (2 * sigma * sigma));
|
|
61
|
+
for (let i = 0; i < kernel.length; i++)
|
|
62
|
+
kernel[i] /= total;
|
|
63
|
+
return kernel;
|
|
64
|
+
}
|
|
65
|
+
function kernelPass(src, dst, count, length, lineStride, step, kernel) {
|
|
66
|
+
const radius = (kernel.length - 1) / 2;
|
|
67
|
+
const last = length - 1;
|
|
68
|
+
for (let line = 0; line < count; line++) {
|
|
69
|
+
const base = line * lineStride;
|
|
70
|
+
for (let i = 0; i < length; i++) {
|
|
71
|
+
let r = 0, g = 0, b = 0, a = 0;
|
|
72
|
+
for (let k = -radius; k <= radius; k++) {
|
|
73
|
+
const j = i + k;
|
|
74
|
+
const o = base + (j < 0 ? 0 : j > last ? last : j) * step;
|
|
75
|
+
const w = kernel[k + radius];
|
|
76
|
+
r += src[o] * w;
|
|
77
|
+
g += src[o + 1] * w;
|
|
78
|
+
b += src[o + 2] * w;
|
|
79
|
+
a += src[o + 3] * w;
|
|
80
|
+
}
|
|
81
|
+
const o = base + i * step;
|
|
82
|
+
dst[o] = r;
|
|
83
|
+
dst[o + 1] = g;
|
|
84
|
+
dst[o + 2] = b;
|
|
85
|
+
dst[o + 3] = a;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function kernelPassRows(src, dst, width, height, kernel) {
|
|
90
|
+
const stride = width * 4;
|
|
91
|
+
const radius = (kernel.length - 1) / 2;
|
|
92
|
+
for (let y = 0; y < height; y++) {
|
|
93
|
+
const o = y * stride;
|
|
94
|
+
for (let k = -radius; k <= radius; k++) {
|
|
95
|
+
const j = y + k;
|
|
96
|
+
const s = (j < 0 ? 0 : j >= height ? height - 1 : j) * stride;
|
|
97
|
+
const w = kernel[k + radius];
|
|
98
|
+
for (let x = 0; x < stride; x++)
|
|
99
|
+
dst[o + x] += src[s + x] * w;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
export function gaussianBlur(pixels, width, height, sigma) {
|
|
104
|
+
if (!(sigma > 0))
|
|
105
|
+
return;
|
|
106
|
+
const tmp = new Float32Array(pixels.length);
|
|
107
|
+
if (sigma < 2) {
|
|
108
|
+
const kernel = gaussianKernel(sigma);
|
|
109
|
+
kernelPass(pixels, tmp, height, width, width * 4, 4, kernel);
|
|
110
|
+
pixels.fill(0);
|
|
111
|
+
kernelPassRows(tmp, pixels, width, height, kernel);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
for (const size of boxSizes(sigma)) {
|
|
115
|
+
const radius = (size - 1) / 2;
|
|
116
|
+
if (radius < 1)
|
|
117
|
+
continue;
|
|
118
|
+
boxPass(pixels, tmp, height, width, width * 4, 4, radius);
|
|
119
|
+
boxPassRows(tmp, pixels, width, height, radius);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
export function premultiply(rgba) {
|
|
123
|
+
const out = new Float32Array(rgba.length);
|
|
124
|
+
for (let i = 0; i < rgba.length; i += 4) {
|
|
125
|
+
const a = rgba[i + 3];
|
|
126
|
+
out[i] = rgba[i] * a / 255;
|
|
127
|
+
out[i + 1] = rgba[i + 1] * a / 255;
|
|
128
|
+
out[i + 2] = rgba[i + 2] * a / 255;
|
|
129
|
+
out[i + 3] = a;
|
|
130
|
+
}
|
|
131
|
+
return out;
|
|
132
|
+
}
|
|
133
|
+
export function unpremultiply(pixels, out) {
|
|
134
|
+
for (let i = 0; i < pixels.length; i += 4) {
|
|
135
|
+
const a = pixels[i + 3];
|
|
136
|
+
if (a < 0.5) {
|
|
137
|
+
out[i] = out[i + 1] = out[i + 2] = out[i + 3] = 0;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
const f = 255 / a;
|
|
141
|
+
out[i] = pixels[i] * f;
|
|
142
|
+
out[i + 1] = pixels[i + 1] * f;
|
|
143
|
+
out[i + 2] = pixels[i + 2] * f;
|
|
144
|
+
out[i + 3] = a;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Data accepted wherever binary input is expected */
|
|
2
|
+
export type BinaryData = ArrayBuffer | ArrayBufferView;
|
|
3
|
+
/**
|
|
4
|
+
* Returns a Uint8Array view of the given buffer without copying
|
|
5
|
+
*/
|
|
6
|
+
export declare function view(buffer: BinaryData): Uint8Array;
|
|
7
|
+
/**
|
|
8
|
+
* Concatenates byte arrays
|
|
9
|
+
*/
|
|
10
|
+
export declare function concat(parts: ArrayLike<number>[]): Uint8Array;
|
package/dist/util/mem.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function view(buffer) {
|
|
2
|
+
if (buffer instanceof Uint8Array && buffer.constructor === Uint8Array)
|
|
3
|
+
return buffer;
|
|
4
|
+
if (buffer instanceof ArrayBuffer)
|
|
5
|
+
return new Uint8Array(buffer);
|
|
6
|
+
if (ArrayBuffer.isView(buffer))
|
|
7
|
+
return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
|
|
8
|
+
if (typeof SharedArrayBuffer !== 'undefined' && buffer instanceof SharedArrayBuffer)
|
|
9
|
+
return new Uint8Array(buffer);
|
|
10
|
+
throw new TypeError('The provided value is not of type \'(ArrayBuffer or ArrayBufferView)\'');
|
|
11
|
+
}
|
|
12
|
+
export function concat(parts) {
|
|
13
|
+
let length = 0;
|
|
14
|
+
for (const part of parts)
|
|
15
|
+
length += part.length;
|
|
16
|
+
const out = new Uint8Array(length);
|
|
17
|
+
let offset = 0;
|
|
18
|
+
for (const part of parts) {
|
|
19
|
+
out.set(part, offset);
|
|
20
|
+
offset += part.length;
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface Filter {
|
|
2
|
+
/** radius of the filter at scale 1 */
|
|
3
|
+
support: number;
|
|
4
|
+
weight: (x: number) => number;
|
|
5
|
+
}
|
|
6
|
+
export declare const BOX: Filter;
|
|
7
|
+
export declare const TRIANGLE: Filter;
|
|
8
|
+
export declare const CUBIC: Filter;
|
|
9
|
+
/**
|
|
10
|
+
* Resamples RGBA pixels with the given filter, in premultiplied alpha so transparent pixels don't bleed their color
|
|
11
|
+
*/
|
|
12
|
+
export declare function resample(src: Uint8ClampedArray, width: number, height: number, outWidth: number, outHeight: number, filter: Filter, out: Uint8ClampedArray): void;
|