imagescript 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/image.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import * as png from "./codecs/png.js";
2
2
  import * as jpeg from "./codecs/jpeg.js";
3
3
  import * as tiff from "./codecs/tiff.js";
4
+ import * as webp from "./codecs/webp.js";
5
+ import * as tga from "./codecs/tga.js";
4
6
  import * as quantize from "./codecs/quantize.js";
5
7
  import * as textRenderer from "./render/text.js";
6
8
  import * as svgRenderer from "./render/svg.js";
@@ -606,6 +608,29 @@ export class Image {
606
608
  }
607
609
  return this;
608
610
  }
611
+ colorToAlpha(color, options = {}) {
612
+ const { transparencyThreshold = 0, opacityThreshold = 1 } = options;
613
+ if (!(transparencyThreshold >= 0 && opacityThreshold <= 1 && transparencyThreshold < opacityThreshold))
614
+ throw new RangeError('Invalid thresholds, they have to satisfy 0 <= transparencyThreshold < opacityThreshold <= 1');
615
+ const bitmap = this.bitmap;
616
+ const c = color === undefined ? [bitmap[0], bitmap[1], bitmap[2]] : [color >>> 24, color >> 16 & 255, color >> 8 & 255];
617
+ for (let i = 0; i < bitmap.length; i += 4) {
618
+ let distance = 0;
619
+ for (let k = 0; k < 3; k++) {
620
+ const d = bitmap[i + k] - c[k];
621
+ const a = d > 0 ? d / (255 - c[k]) : d < 0 ? -d / c[k] : 0;
622
+ if (a > distance)
623
+ distance = a;
624
+ }
625
+ const alpha = distance <= transparencyThreshold ? 0 : distance >= opacityThreshold ? 1
626
+ : (distance - transparencyThreshold) / (opacityThreshold - transparencyThreshold);
627
+ if (alpha > 0)
628
+ for (let k = 0; k < 3; k++)
629
+ bitmap[i + k] = Math.round((bitmap[i + k] - c[k]) / alpha + c[k]);
630
+ bitmap[i + 3] = Math.round(bitmap[i + 3] * alpha);
631
+ }
632
+ return this;
633
+ }
609
634
  __map_channels__(map) {
610
635
  const lut = new Uint8ClampedArray(256);
611
636
  for (let v = 0; v < 256; v++)
@@ -878,6 +903,12 @@ export class Image {
878
903
  async encodeJPEG(quality = 90) {
879
904
  return jpeg.encode(this.__bytes__(), this.width, this.height, quality);
880
905
  }
906
+ async encodeWebP() {
907
+ return webp.encode(this.__bytes__(), this.width, this.height);
908
+ }
909
+ async encodeTGA() {
910
+ return tga.encode(this.__bytes__(), this.width, this.height);
911
+ }
881
912
  static async decode(input, options = {}) {
882
913
  const { applyOrientation = true } = options;
883
914
  const data = view(input);
@@ -889,6 +920,13 @@ export class Image {
889
920
  decoded = jpeg.decode(data);
890
921
  else if (ImageType.isTIFF(dataView))
891
922
  decoded = await tiff.decode(data);
923
+ else if (ImageType.isWebP(dataView)) {
924
+ if (webp.isAnimated(data))
925
+ throw new Error('Animated WebPs have to be decoded with Animation.decode()');
926
+ decoded = webp.decode(data);
927
+ }
928
+ else if (ImageType.isTGA(dataView))
929
+ decoded = tga.decode(data);
892
930
  else
893
931
  throw new Error('Unsupported image type');
894
932
  const image = new Image(decoded.width, decoded.height);
@@ -963,10 +1001,10 @@ export class Image {
963
1001
  return image;
964
1002
  }
965
1003
  static async renderText(font, size, text, color = 0xffffffff, layout = new TextLayout(), style = {}) {
966
- const { strokeWidth = 0, strokeColor = 0x000000ff } = style;
1004
+ const { strokeWidth = 0, strokeColor = 0x000000ff, hinting = true } = style;
967
1005
  if (isNaN(strokeWidth) || strokeWidth < 0)
968
1006
  throw new RangeError('Invalid stroke width');
969
- const rendered = textRenderer.render(view(font), size, text, layout);
1007
+ const rendered = textRenderer.render(view(font), size, text, layout, hinting);
970
1008
  const [r, g, b, a] = Image.colorToRGBA(color);
971
1009
  const pad = Math.ceil(strokeWidth);
972
1010
  const width = rendered.width + 2 * pad, height = rendered.height + 2 * pad;
package/dist/index.d.ts CHANGED
@@ -1,19 +1,23 @@
1
1
  import { BinaryData } from './util/mem.js';
2
2
  import { Image } from './image.js';
3
3
  import { GIF } from './gif.js';
4
+ import { Animation } from './animation.js';
4
5
  export { Image } from './image.js';
5
- export type { Color, ColorFunction, ResizeMode, SVGScaleMode, PNGMetadata, TextStyle, DecodeOptions, CompositeOptions, BlendMode, CropCircleOptions, DominantColorOptions, RotateOptions } from './image.js';
6
+ export type { Color, ColorFunction, ResizeMode, SVGScaleMode, PNGMetadata, TextStyle, DecodeOptions, CompositeOptions, BlendMode, CropCircleOptions, DominantColorOptions, RotateOptions, ColorToAlphaOptions } from './image.js';
6
7
  export { Frame } from './frame.js';
7
8
  export type { FrameDisposalModeName, FrameDisposalModeId } from './frame.js';
8
9
  export { GIF } from './gif.js';
10
+ export { Animation } from './animation.js';
9
11
  export { TextLayout } from './text-layout.js';
10
12
  export type { TextLayoutOptions, WrapStyle, HorizontalAlign, VerticalAlign } from './text-layout.js';
11
13
  export { ImageType } from './image-type.js';
12
14
  export type { ImageTypeName } from './image-type.js';
13
15
  export type { BinaryData } from './util/mem.js';
14
16
  /**
15
- * Decodes any supported image, GIFs are returned as {@link GIF}
17
+ * Decodes any supported image: GIFs are returned as {@link GIF} (an {@link Animation}), animated WebPs as
18
+ * {@link Animation}, everything else (including APNGs, as their default image) as {@link Image}.
19
+ * {@link Animation.decode} decodes the frames of GIFs, APNGs and WebPs
16
20
  * @param data The image data
17
- * @param onlyExtractFirstFrame Whether to end GIF decoding after the first frame
21
+ * @param onlyExtractFirstFrame Whether to end animation decoding after the first frame
18
22
  */
19
- export declare function decode(data: BinaryData, onlyExtractFirstFrame?: boolean): Promise<GIF | Image>;
23
+ export declare function decode(data: BinaryData, onlyExtractFirstFrame?: boolean): Promise<GIF | Animation | Image>;
package/dist/index.js CHANGED
@@ -1,13 +1,20 @@
1
1
  import { Image } from "./image.js";
2
2
  import { GIF } from "./gif.js";
3
+ import { Animation } from "./animation.js";
4
+ import * as webp from "./codecs/webp.js";
5
+ import { view } from "./util/mem.js";
3
6
  import { ImageType } from "./image-type.js";
4
7
  export { Image } from "./image.js";
5
8
  export { Frame } from "./frame.js";
6
9
  export { GIF } from "./gif.js";
10
+ export { Animation } from "./animation.js";
7
11
  export { TextLayout } from "./text-layout.js";
8
12
  export { ImageType } from "./image-type.js";
9
13
  export function decode(data, onlyExtractFirstFrame = false) {
10
- if (ImageType.getType(data) === 'gif')
14
+ const type = ImageType.getType(data);
15
+ if (type === 'gif')
11
16
  return GIF.decode(data, onlyExtractFirstFrame);
17
+ if (type === 'webp' && webp.isAnimated(view(data)))
18
+ return Animation.decode(data, onlyExtractFirstFrame);
12
19
  return Image.decode(data);
13
20
  }
@@ -3,6 +3,28 @@
3
3
  * ['M', x, y], ['L', x, y], ['Q', cx, cy, x, y], ['C', c1x, c1y, c2x, c2y, x, y], ['Z']
4
4
  */
5
5
  export type PathCommand = [string, ...number[]];
6
+ /** a TrueType glyph's points (font units) and instructions, for hinting */
7
+ export interface GlyfPoints {
8
+ xs: number[];
9
+ ys: number[];
10
+ /** on curve flags */
11
+ on: boolean[];
12
+ /** index of the last point of every contour */
13
+ ends: number[];
14
+ instructions: Uint8Array;
15
+ /** glyph bounding box minimum x (the left side bearing's reference) */
16
+ xMin: number;
17
+ /** components of composite glyphs (their points are not included in xs/ys) */
18
+ components: Array<{
19
+ glyph: number;
20
+ dx: number;
21
+ dy: number;
22
+ matrix: [number, number, number, number];
23
+ round: boolean;
24
+ match: [number, number] | null;
25
+ useMetrics: boolean;
26
+ }> | null;
27
+ }
6
28
  interface CFF {
7
29
  charStrings: Uint8Array[];
8
30
  globalSubrs: Uint8Array[];
@@ -50,6 +72,12 @@ export declare class Font {
50
72
  * @return {number} advance width in font units
51
73
  */
52
74
  advance(glyph: number): number;
75
+ /** left side bearing in font units */
76
+ leftSideBearing(glyph: number): number;
77
+ /**
78
+ * A TrueType glyph's points and instructions (null for empty glyphs or CFF fonts)
79
+ */
80
+ glyfPoints(glyph: number): GlyfPoints | null;
53
81
  /**
54
82
  * @param {number} glyph
55
83
  * @return {Array} path commands in font units
@@ -60,4 +88,5 @@ export declare class Font {
60
88
  compositeGlyph(offset: number, depth: number): PathCommand[];
61
89
  cffOutline(glyph: number): PathCommand[];
62
90
  }
91
+ export declare function contourToPath(path: PathCommand[], xs: ArrayLike<number>, ys: ArrayLike<number>, flags: ArrayLike<number | boolean>, first: number, last: number): void;
63
92
  export {};
@@ -73,6 +73,103 @@ export class Font {
73
73
  const index = Math.min(glyph, this.numberOfHMetrics - 1);
74
74
  return this.u16(hmtx + index * 4);
75
75
  }
76
+ leftSideBearing(glyph) {
77
+ const hmtx = this.tables.hmtx.offset;
78
+ if (glyph < this.numberOfHMetrics)
79
+ return this.i16(hmtx + glyph * 4 + 2);
80
+ return this.i16(hmtx + this.numberOfHMetrics * 4 + (glyph - this.numberOfHMetrics) * 2);
81
+ }
82
+ glyfPoints(glyph) {
83
+ if (this.outlines !== 'glyf' || glyph >= this.numGlyphs)
84
+ return null;
85
+ const loca = this.tables.loca.offset;
86
+ const start = this.indexToLocFormat ? this.u32(loca + glyph * 4) : this.u16(loca + glyph * 2) * 2;
87
+ const end = this.indexToLocFormat ? this.u32(loca + glyph * 4 + 4) : this.u16(loca + glyph * 2 + 2) * 2;
88
+ if (end <= start)
89
+ return null;
90
+ const offset = this.tables.glyf.offset + start;
91
+ const contourCount = this.i16(offset);
92
+ const xMin = this.i16(offset + 2);
93
+ if (contourCount < 0) {
94
+ const components = [];
95
+ let p = offset + 10, flags = 0;
96
+ do {
97
+ flags = this.u16(p);
98
+ const component = this.u16(p + 2);
99
+ p += 4;
100
+ let arg1, arg2;
101
+ if (flags & 1) {
102
+ arg1 = flags & 2 ? this.i16(p) : this.u16(p);
103
+ arg2 = flags & 2 ? this.i16(p + 2) : this.u16(p + 2);
104
+ p += 4;
105
+ }
106
+ else {
107
+ arg1 = flags & 2 ? this.i8(p) : this.u8(p);
108
+ arg2 = flags & 2 ? this.i8(p + 1) : this.u8(p + 1);
109
+ p += 2;
110
+ }
111
+ let matrix = [1, 0, 0, 1];
112
+ const f2dot14 = (o) => this.i16(o) / 16384;
113
+ if (flags & 8) {
114
+ const s = f2dot14(p);
115
+ matrix = [s, 0, 0, s];
116
+ p += 2;
117
+ }
118
+ else if (flags & 0x40) {
119
+ matrix = [f2dot14(p), 0, 0, f2dot14(p + 2)];
120
+ p += 4;
121
+ }
122
+ else if (flags & 0x80) {
123
+ matrix = [f2dot14(p), f2dot14(p + 2), f2dot14(p + 4), f2dot14(p + 6)];
124
+ p += 8;
125
+ }
126
+ components.push({
127
+ glyph: component,
128
+ dx: flags & 2 ? arg1 : 0,
129
+ dy: flags & 2 ? arg2 : 0,
130
+ matrix,
131
+ round: (flags & 4) !== 0,
132
+ match: flags & 2 ? null : [arg1, arg2],
133
+ useMetrics: (flags & 0x200) !== 0
134
+ });
135
+ } while (flags & 0x20);
136
+ const instructions = flags & 0x100 ? this.data.subarray(p + 2, p + 2 + this.u16(p)) : new Uint8Array(0);
137
+ return { xs: [], ys: [], on: [], ends: [], instructions, xMin, components };
138
+ }
139
+ const ends = [];
140
+ for (let i = 0; i < contourCount; i++)
141
+ ends.push(this.u16(offset + 10 + i * 2));
142
+ const pointCount = contourCount ? ends[contourCount - 1] + 1 : 0;
143
+ let p = offset + 10 + contourCount * 2;
144
+ const instructionLength = this.u16(p);
145
+ const instructions = this.data.subarray(p + 2, p + 2 + instructionLength);
146
+ p += 2 + instructionLength;
147
+ const flags = new Uint8Array(pointCount);
148
+ for (let i = 0; i < pointCount;) {
149
+ const flag = this.u8(p++);
150
+ flags[i++] = flag;
151
+ if (flag & 8)
152
+ for (let repeat = this.u8(p++); repeat > 0 && i < pointCount; repeat--)
153
+ flags[i++] = flag;
154
+ }
155
+ const xs = [], ys = [];
156
+ for (const [coords, short, same] of [[xs, 2, 16], [ys, 4, 32]]) {
157
+ let value = 0;
158
+ for (let i = 0; i < pointCount; i++) {
159
+ const flag = flags[i];
160
+ if (flag & short) {
161
+ const delta = this.u8(p++);
162
+ value += flag & same ? delta : -delta;
163
+ }
164
+ else if (!(flag & same)) {
165
+ value += this.i16(p);
166
+ p += 2;
167
+ }
168
+ coords.push(value);
169
+ }
170
+ }
171
+ return { xs, ys, on: Array.from(flags, f => (f & 1) === 1), ends, instructions, xMin, components: null };
172
+ }
76
173
  outline(glyph) {
77
174
  if (glyph >= this.numGlyphs)
78
175
  glyph = 0;
@@ -190,11 +287,11 @@ export class Font {
190
287
  return cffCharString(this.cff, glyph);
191
288
  }
192
289
  }
193
- function contourToPath(path, xs, ys, flags, first, last) {
290
+ export function contourToPath(path, xs, ys, flags, first, last) {
194
291
  if (last < first)
195
292
  return;
196
293
  const count = last - first + 1;
197
- const on = (i) => flags[first + (i % count)] & 1;
294
+ const on = (i) => flags[first + (i % count)] === true || (flags[first + (i % count)] & 1) === 1;
198
295
  const x = (i) => xs[first + (i % count)];
199
296
  const y = (i) => ys[first + (i % count)];
200
297
  let start = -1;
@@ -0,0 +1,110 @@
1
+ /*!
2
+ * TrueType hinting interpreter. Where the TrueType specification leaves details open, its behavior and parts of its
3
+ * logic follow FreeType's TrueType interpreter (src/truetype/ttinterp.c): fixed point rounding, original distances
4
+ * on the unscaled outline (IP, IUP, MDRP, MD), the IUP interpolation, the movement along near-perpendicular vectors
5
+ * and the tolerance of invalid references. Portions of this software are copyright © 2026 The FreeType Project
6
+ * (https://freetype.org). All rights reserved. FreeType License, see THIRD_PARTY_NOTICES.md and licenses/FTL.txt
7
+ */
8
+ import { Font, PathCommand } from './font.js';
9
+ interface Vector {
10
+ x: number;
11
+ y: number;
12
+ }
13
+ interface GraphicsState {
14
+ pv: Vector;
15
+ fv: Vector;
16
+ dv: Vector;
17
+ rp0: number;
18
+ rp1: number;
19
+ rp2: number;
20
+ zp0: number;
21
+ zp1: number;
22
+ zp2: number;
23
+ loop: number;
24
+ minimumDistance: number;
25
+ cvtCutIn: number;
26
+ singleWidthCutIn: number;
27
+ singleWidth: number;
28
+ deltaBase: number;
29
+ deltaShift: number;
30
+ autoFlip: boolean;
31
+ /** null: rounding off */
32
+ round: {
33
+ period: number;
34
+ phase: number;
35
+ threshold: number;
36
+ } | null;
37
+ instructControl: number;
38
+ }
39
+ interface Zone {
40
+ curX: Float64Array;
41
+ curY: Float64Array;
42
+ orgX: Float64Array;
43
+ orgY: Float64Array;
44
+ /** unscaled original positions (font units), measurements between original glyph points use them */
45
+ orusX: Float64Array;
46
+ orusY: Float64Array;
47
+ touchX: Uint8Array;
48
+ touchY: Uint8Array;
49
+ on: Uint8Array;
50
+ ends: number[];
51
+ }
52
+ interface FunctionDef {
53
+ code: Uint8Array;
54
+ start: number;
55
+ end: number;
56
+ }
57
+ /** state of a font at one size, after the font and control value programs ran */
58
+ interface SizeState {
59
+ ppem: number;
60
+ /** 26.6 units per font unit */
61
+ scale: number;
62
+ /** the same as 16.16 fixed point number */
63
+ scaleFixed: number;
64
+ cvt: Float64Array;
65
+ storage: Int32Array;
66
+ state: GraphicsState;
67
+ twilight: Zone;
68
+ functions: Map<number, FunctionDef>;
69
+ instructions: Map<number, FunctionDef>;
70
+ /** set if the font and control value programs failed, the font is then rendered unhinted */
71
+ broken: boolean;
72
+ }
73
+ export interface HintedGlyph {
74
+ /** outline in pixels, relative to the pen position (y up) */
75
+ path: PathCommand[];
76
+ /** advance width in whole pixels */
77
+ advance: number;
78
+ }
79
+ /**
80
+ * Executes TrueType instructions for one font
81
+ */
82
+ export declare class Hinter {
83
+ font: Font;
84
+ fpgm: Uint8Array;
85
+ prep: Uint8Array;
86
+ cvt: Int16Array;
87
+ maxStack: number;
88
+ maxStorage: number;
89
+ maxTwilight: number;
90
+ sizes: Map<number, SizeState>;
91
+ /** 26.6 units per font unit (16.16 fixed point) of the glyph being loaded */
92
+ private scaleFixed;
93
+ constructor(font: Font);
94
+ /** whether the font has hinting instructions (a font program, control value program or control values) */
95
+ get hasInstructions(): boolean;
96
+ private size;
97
+ /**
98
+ * Hints a glyph at a size
99
+ * @param size pixels per em
100
+ * @return the hinted outline, or null if the glyph can't be hinted (it is then rendered unhinted)
101
+ */
102
+ glyph(glyph: number, size: number): HintedGlyph | null;
103
+ /**
104
+ * Loads, scales and hints a glyph (recursively for composites)
105
+ * @return the glyph zone (with 4 phantom points after the outline points)
106
+ */
107
+ private load;
108
+ private phantoms;
109
+ }
110
+ export {};