text-to-canvas 1.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/CHANGELOG.md ADDED
@@ -0,0 +1,10 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## v1.0.0
9
+
10
+ - First official release 🎉
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 Stefan Cameron
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,364 @@
1
+ [![CI](https://github.com/stefcameron/text-to-canvas/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/stefcameron/text-to-canvas/actions/workflows/ci.yml) [![license](https://badgen.now.sh/badge/license/MIT)](./LICENSE)
2
+
3
+ # text-to-canvas
4
+
5
+ Render multiline plain or rich text into textboxes on HTML Canvas with automatic line wrapping.
6
+
7
+ ## Origins and Differences
8
+
9
+ 🙌 This library would not exist were it not for all the work done by its original author, [Geon George](https://geongeorge.com/), in his [canvas-txt](https://github.com/geongeorge/Canvas-Txt) library.
10
+
11
+ The main feature that sparked `text-to-canvas` is a significant [update](https://github.com/geongeorge/Canvas-Txt/pull/95) to the original code base in order to support rich text formatting, which introduced the concept of a `Word` specifying both `text` and (optional) associated CSS-based `format` styles. A sentence is then simply a `Word[]` with/out whitespace (optionally inferred).
12
+
13
+ Plain text (i.e. a `string`) is still supported as a convenience via the `drawText()`, `splitText()`, and `textToWords()` [APIs](#api).
14
+
15
+ The main differences (at `v1.0.0`) between `canvas-txt` and this library are:
16
+
17
+ - Formal support for Node by `canvas-txt` vs this library's support solely focused on the [HTMLCanvasElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement).
18
+ - This library's concerted effort to [support](#web-worker-and-offscreencanvas) [Web Workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) and use of an [OffscreenCanvas](https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas), neither of which is formally supported by `canvas-txt`.
19
+
20
+ The feature gap may widen with future releases of both libraries.
21
+
22
+ While there is a [Node](#node) [demo](./src/demos/node-demo.mts), it only works because the `node-canvas` library being used supports enough of the `HTMLCanvasElement`'s API, not because this library formally supports Node, or `node-canvas`.
23
+
24
+ ## Features
25
+
26
+ - ✅ Rich text formatting (with the exception of words with different font _sizes_ not yet working well in terms of text baseline alignment)
27
+ - ✅ Multiline text
28
+ - ✅ Auto line breaks
29
+ - ✅ Horizontal alignment
30
+ - ✅ Vertical alignment
31
+ - ✅ Justification
32
+ - ✅ Optimized performance with support for Web Workers and `OffscreenCanvas`
33
+
34
+ ## Demo
35
+
36
+ See demo [here](https://stefcameron.github.io/text-to-canvas/).
37
+
38
+ # Installation
39
+
40
+ ```bash
41
+ $ npm install text-to-canvas
42
+ # OR
43
+ $ yarn add text-to-canvas
44
+ ```
45
+
46
+ > 💡 If this fails with a `node-pre-gyp` compilation error, please see [Compilation of the canvas package](#compilation-of-canvas-package) for help.
47
+
48
+ ## Compilation of canvas package
49
+
50
+ This project __optionally__ depends on the [canvas](https://github.com/Automattic/node-canvas) package which enables it to be used in a Node [demo](#node).
51
+
52
+ Since this package needs to be compiled for use on the platform on which you intend to install/use it, the author must either include pre-built binaries specific to your OS when they make a [release](https://github.com/Automattic/node-canvas/releases), or a new binary must be compiled by your package manager (i.e. `npm`) upon installation.
53
+
54
+ If you're installing on a newer Apple M1, M2, or M3 computer, or if you're using a version of Node newer than v20 (the latest LTS at time of writing), you may experience a `node-pre-gyp` failure because `canvas` doesn't provide pre-built binaries for the ARM64 architecture, only providing x86-64 (Intel x64) binaries for Node v20.
55
+
56
+ > ❗️ __Before installing text-to-canvas__, refer to the `canvas` [compilation](https://github.com/Automattic/node-canvas?tab=readme-ov-file#compiling) page for your OS/architecture, especially if you aren't on an Apple computer.
57
+
58
+ For Apple M computers (ARM64), this worked for me using [HomeBrew](https://brew.sh/) and [pyenv](https://github.com/pyenv/pyenv) to install additional compiler dependencies:
59
+
60
+ ```bash
61
+ $ brew install pkg-config cairo pango libpng jpeg giflib librsvg pixman
62
+ $ pyenv install 3.12.1 # install Python 3.12 on which `cairo` depends
63
+ $ pyenv local 3.12.1
64
+ $ npm install # should succeed
65
+ ```
66
+
67
+ # Usage
68
+
69
+ Use with a bundler (Webpack, Rollup, Vite, etc) or directly in a browser is supported.
70
+
71
+ Use in Node is only supported to the extent that appropriate bundles are provided. Make sure you use a Node-base Canvas library that supports the [HTMLCanvasElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement) API.
72
+
73
+ ## Bundler
74
+
75
+ Two bundles are provided for this type of target:
76
+
77
+ - `./dist/text-to-canvas.esm.min.js` (ESM, `import`, ES2020+)
78
+ - `./dist/text-to-canvas.min.js` (CJS, `require()`, ES2019+)
79
+
80
+ Used implicitly when using the library in a larger app bundled with a bundler like Webpack, Rollup, or Vite.
81
+
82
+ Declare a Canvas in your DOM (directly, via JSX, or other):
83
+
84
+ ```html
85
+ <canvas id="my-canvas" width="500" height="500"></canvas>
86
+ ```
87
+
88
+ Call the `drawText()` [API](#api):
89
+
90
+ ```javascript
91
+ import { drawText, Word } from 'text-to-canvas';
92
+
93
+ const canvas = document.getElementById('my-canvas');
94
+ const ctx = canvas.getContext('2d');
95
+
96
+ ctx.clearRect(0, 0, 500, 500);
97
+
98
+ // plain text
99
+ const text = 'Lorem ipsum dolor sit amet';
100
+ // OR with some formatting
101
+ const text: Word[] = [
102
+ { text: 'Lorem' },
103
+ { text: 'ipsum', format: { fontWeight: 'bold', color: 'red' } },
104
+ { text: 'dolor', format: { fontStyle: 'italic' } },
105
+ { text: 'sit' },
106
+ { text: 'amet' },
107
+ ];
108
+
109
+ drawText(ctx, text, {
110
+ x: 100,
111
+ y: 200,
112
+ width: 200,
113
+ height: 200,
114
+ fontSize: 24,
115
+ });
116
+ ```
117
+
118
+ If you need to know the total render height, `drawText()` returns it:
119
+
120
+ ```javascript
121
+ const { height } = drawText(...);
122
+ ```
123
+
124
+ > ⚠️ The library doesn't yet fully support varying font sizes, so you'll get best results by keeping the size consistent (via the [base font size](#drawtext-config)) and changing other formatting options on a per-`Word` basis.
125
+
126
+ ## Browser
127
+
128
+ One bundle is provided for this type of target:
129
+
130
+ - `./dist/text-to-canvas.umd.min.js` (UMD, ES2019+)
131
+
132
+ Used implicitly when loading the library directly in a browser:
133
+
134
+ ```html
135
+ <body>
136
+ <canvas id="my-canvas" width="500" height="500"></canvas>
137
+ <script src="//unpkg.com/text-to-canvas"></script>
138
+ <script>
139
+ const { drawText, getTextHeight, splitText } = window.textToCanvas;
140
+ /// ...remainder is the same
141
+ </script>
142
+ </body>
143
+ ```
144
+
145
+ ## Node
146
+
147
+ Two bundles are provided for this type of target:
148
+
149
+ - `./dist/text-to-canvas.mjs` (ESM/MJS, `import`, Node v20.11.1+)
150
+ - `./dist/text-to-canvas.cjs` (CJS, `require()`, Node v20.11.1+)
151
+
152
+ > ⚠️ Other than the bundles, __Node is not formally supported by this library__, and neither is the `node-canvas` library used in the demo. Whatever "Node Canvas" library you use, make sure it supports the [HTMLCanvasElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement) API and it _should_ work.
153
+
154
+ Used implicitly when importing or requiring the library in your Node scripts:
155
+
156
+ ```javascript
157
+ import { drawText } from 'text-to-canvas'; // MJS
158
+ // OR
159
+ const { drawText } = require('text-to-canvas'); // CJS
160
+ ```
161
+
162
+ See Node demo in [./src/demo/node-demo.ts](https://github.com/stefcameron/text-to-canvas/blob/master/src/demos/node-demo.mts) for an example.
163
+
164
+ You can run this demo locally with `npm run node:demo`
165
+
166
+ # API
167
+
168
+ ## drawText config
169
+
170
+ ![](./src/docs/canvas.jpg)
171
+
172
+ | Properties | Default | Description |
173
+ | :---------------: | :----------: | :-----------------------------------------------------------: |
174
+ | `width` | **Required** | Width of the text box. |
175
+ | `height` | **Required** | Height of the text box. |
176
+ | `x` | `0` | X position of the text box. |
177
+ | `y` | `0` | Y position of the text box. |
178
+ | `align` | `center` | Text align. Other possible values: `left`, `right`. |
179
+ | `vAlign` | `middle` | Text vertical align. Other possible values: `top`, `bottom`. |
180
+ | `font` | `Arial` | Base font family of the text. |
181
+ | `fontSize` | `14` | Base font size of the text in px. |
182
+ | `fontStyle` | `''` | Base font style, same as css font-style. Examples: `italic`, `oblique 40deg`. |
183
+ | `fontVariant` | `''` | Base font variant, same as css font-variant. Examples: `small-caps`. |
184
+ | `fontWeight` | `'400'` | Base font weight, same as css font-weight. Examples: `bold`, `100`. |
185
+ | `fontColor` | `'black'` | Base font color, same as css color. Examples: `blue`, `#00ff00`. |
186
+ | `justify` | `false` | Justify text if `true`, it will insert spaces between words when necessary. |
187
+ | `inferWhitespace` | `true` | If whitespace in the text should be inferred. Only applies if the text given to `drawText()` is a `Word[]`. If the text is a `string`, this config setting is ignored. |
188
+ | `debug` | `false` | Draws the border and alignment lines of the text box for debugging purposes. |
189
+
190
+ ## Functions
191
+
192
+ ```js
193
+ import {
194
+ drawText,
195
+ specToJson,
196
+ splitText,
197
+ splitWords,
198
+ textToWords,
199
+ wordsToJson,
200
+ getTextHeight,
201
+ getWordHeight,
202
+ getTextStyle,
203
+ getTextFormat,
204
+ } from 'text-to-canvas'
205
+ ```
206
+
207
+ > ⚠️ Varying font sizes on a `Word` level (as given to `drawText()` or `splitWords()`) is not supported very well at this time. For best results, keep the font size consistent by relying on a single base font size as specified in the `drawText()` [config options](#drawtext-config).
208
+
209
+ - `drawText()`: Draws text (`string` or `Word[]`) to a given Canvas.
210
+ - `specToJson()`: Converts a `RenderSpec` to a JSON string. Useful for sending it as a message through `Worker.postMessage()`.
211
+ - `splitText()`: Splits a given `string` into wrapped lines.
212
+ - This is just a convenience over `splitWords()` if you aren't needing rich text. It's only real value is that it will return the input text as an array of strings according to how the text would be wrapped on Canvas.
213
+ - `splitWords()`: Splits a given `Word[]` into wrapped lines.
214
+ - `textToWords()`: Converts a `string` into a `Word[]`. Useful if you want to then apply rich formatting to certain words.
215
+ - `wordsToJson()`: Converts a `Word[]` to a JSON string. Useful for sending it as a message to a Worker thread via `Worker.postMessage()`.
216
+ - `getTextHeight()`: Gets the measured height of a given `string` using a given text style.
217
+ - `getWordHeight()`: Gets the measured height of a given `Word` using its text style.
218
+ - `getTextStyle()`: Generates a CSS Font `string` from a given `TextFormat` for use with `canvas.getContext('2d').font`
219
+ - `getTextFormat()`: Generates a "full" `TextFormat` object (all properties specified) given one with only partial properties using prescribed defaults.
220
+
221
+ TypeScript integration should provide helpful JSDocs for every function and each of its parameters to further help with their use.
222
+
223
+ # Examples
224
+
225
+ ## Web Worker and OffscreenCanvas
226
+
227
+ If you want to draw the text yourself, or even offload the work of splitting the words to a [Web Worker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) using an [OffscreenCanvas](https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas), you can use the `splitWords()` [API](#api) directly.
228
+
229
+ > This requires using `wordsToJson()` and `specToJson()` APIs to ensure all required information is properly transferred between the UI/main thread and the worker thread, particularly concerning the cached [TextMetrics](https://developer.mozilla.org/en-US/docs/Web/API/TextMetrics).
230
+
231
+ <details>
232
+ <summary>Sample code</summary>
233
+ <br/>
234
+
235
+ Add a Canvas to your DOM:
236
+
237
+ ```html
238
+ <canvas id="my-canvas" width="500" height="500"></canvas>
239
+ ```
240
+
241
+ Define a Web Worker, `worker.js`:
242
+
243
+ ```javascript
244
+ import { splitWords, specToJson } from 'text-to-canvas';
245
+
246
+ const wrapLines = ({ containerWidth, wordsStr, baseFormat }) => {
247
+ // NOTE: height doesn't really matter (aside from being >0) as text won't be
248
+ // constrained by it; just affects alignment, especially if you're wanting to
249
+ // do bottom/middle vertical alignment; for top/left-aligned, height for
250
+ // splitting is basically inconsequential
251
+ const canvas = new OffscreenCanvas(containerWidth, 100);
252
+ const context = canvas.getContext('2d');
253
+
254
+ const words = JSON.parse(wordsStr);
255
+ const spec = splitWords({
256
+ ctx: context,
257
+ words,
258
+ x: 0,
259
+ y: 0,
260
+ width: containerWidth,
261
+ align: 'left',
262
+ vAlign: 'top',
263
+ format: baseFormat,
264
+ // doesn't really matter (aside from being >0) as long as you only want
265
+ // top/left alignment
266
+ height: 100,
267
+ });
268
+
269
+ self.postMessage({
270
+ type: 'renderSpec',
271
+ specStr: specToJson(spec), // because of `TextMetrics` objects that fail serialization
272
+ });
273
+ };
274
+
275
+ self.onmessage = (event) => {
276
+ if (event.data.type === 'split') {
277
+ wrapLines(event.data);
278
+ }
279
+ };
280
+
281
+ export {}; // make bundler consider this an ES Module
282
+ ```
283
+
284
+ Use the Worker thread to offload the work of measuring and splitting the words:
285
+
286
+ ```typescript
287
+ import { Word, RenderSpec, TextFormat, wordsToJson, getTextStyle } from 'text-to-canvas';
288
+
289
+ const canvas = document.getElementById('my-canvas');
290
+ const ctx = canvas.getContext('2d');
291
+
292
+ const drawWords = (baseFormat: TextFormat, spec: RenderSpec) => {
293
+ const {
294
+ lines,
295
+ height: totalHeight,
296
+ textBaseline,
297
+ textAlign,
298
+ } = spec;
299
+
300
+ ctx.save();
301
+ ctx.textAlign = textAlign;
302
+ ctx.textBaseline = textBaseline;
303
+ ctx.font = getTextStyle(baseFormat);
304
+ ctx.fillStyle = baseFormat.fontColor;
305
+
306
+ lines.forEach((line) => {
307
+ line.forEach((pw) => {
308
+ if (!pw.isWhitespace) {
309
+ if (pw.format) {
310
+ ctx.save();
311
+ ctx.font = getTextStyle(pw.format);
312
+ if (pw.format.fontColor) {
313
+ ctx.fillStyle = pw.format.fontColor;
314
+ }
315
+ }
316
+ ctx.fillText(pw.word.text, pw.x, pw.y);
317
+ if (pw.format) {
318
+ ctx.restore();
319
+ }
320
+ }
321
+ });
322
+ });
323
+ };
324
+
325
+ const words: Word[] = [
326
+ { text: 'Lorem' },
327
+ { text: 'ipsum', format: { fontWeight: 'bold', color: 'red' } },
328
+ { text: 'dolor', format: { fontStyle: 'italic' } },
329
+ { text: 'sit' },
330
+ { text: 'amet' },
331
+ ];
332
+
333
+ const baseFormat: TextFormat = {
334
+ fontSize: 12,
335
+ fontFamily: 'Times New Roman',
336
+ fontColor: 'black',
337
+ };
338
+
339
+ // create a worker thread
340
+ const worker = new Worker('./worker.js', { type: 'module' });
341
+
342
+ // queue the worker to split and measure the words as necessary for the
343
+ // specified width given base and any word-specific formatting
344
+ worker.postMessage({
345
+ type: 'split',
346
+ containerWidth: 500,
347
+ wordsStr: wordsToJson(words),
348
+ baseFormat,
349
+ });
350
+
351
+ // listen for the "done" signal from the worker
352
+ worker.onmessage = (event) => {
353
+ if (event.data?.type === 'renderSpec') {
354
+ worker.terminate();
355
+ const spec: RenderSpec = JSON.parse(event.data.specStr);
356
+
357
+ // render the spec (containing the `PositionedWord[]` with all the necessary
358
+ // information)
359
+ drawWords(baseFormat, spec);
360
+ }
361
+ };
362
+ ```
363
+
364
+ </details>
package/SECURITY.md ADDED
@@ -0,0 +1,37 @@
1
+ # Security Policy
2
+
3
+ ## Supported Versions
4
+
5
+ The most recently published version is the only supported version. We simply do not have the maintainer capacity to support multiple versions.
6
+
7
+ ## Security Releases
8
+
9
+ The most recently published version is the only supported version. If there's a security issue in that version, then we will fix it by publishing a new version that addresses the vulnerability, but we will not support or update any previous versions.
10
+
11
+ ### Example Scenario
12
+
13
+ Let's say we publish 9.0.0 and a security issue is found in 8.1.3, and it's still in 9.0.0, then we will fix it in 9.0.1 or 9.1.0 (or possibly 10.0.0 if it requires breaking backward compatibility for some reason -- this should be rare), but we will not also publish 8.1.4 or 8.2.1 to fix it.
14
+
15
+ There could also be a scenario where we're in a pre-release on a new major and a security issue is discovered in the current 8.1.3 release. In that case, we would try to fix it in the current non-pre-release, and bring that forward into the pre-release, but that's as far back as we would go (though we don't consider that going back because the latest release isn't a "full" release, it's still in pre-release stage, so we don't expect everyone to want to adopt a pre-release to get security fix).
16
+
17
+ ## Release Cadence
18
+
19
+ This happens whenever there's something new to publish, regardless of the [Semver](https://semver.org/) bump, though we try to avoid breaking changes (majors) as much as possible. That time may come, however, and the major version change is an indication that there _may_ be a large change/break in functionality. We may also publish a major out of an abundance of caution, even if there are technically no known backward compatibility breaks, if there have been many internal changes.
20
+
21
+ When planning a major break in functionality for a new major release where we wish to gather feedback from the community prior to officially publishing it, we would leverage the pre-release version indicator by publishing 9.0.0-alpha.1, for example. After gathering some feedback, we may publishing additional pre-release versions, until we would finally officially publish as 9.0.0.
22
+
23
+ We may not always leverage pre-releases for breaking changes, however. One scenario would be a complex security issue that would force a breaking change, and needs immediate fixing.
24
+
25
+ ## Backwards Compatibility
26
+
27
+ This is only guaranteed _within_ a major, not from one major to the next. [Semver](https://semver.org/) states that, "the major version is incremented if any backwards _incompatible_ changes are introduced." That is what we respect for this package. Patches are bug fixes that remain backward compatible (to the current major), minors for new features (or significant internal changes) that remain backward-compatible (to the current major), and majors are for breaking changes (from the previous major).
28
+
29
+ ## Reporting Vulnerabilities
30
+
31
+ If you believe you have found a security vulnerability in this package, please contact one of the maintainers directly and provide them with details, severity, and a reproduction. We would also welcome a suggested fix if you have one.
32
+
33
+ Any verified and accepted security vulnerabilities will be rewarded with a listing in a special "Hall of Fame" section of our README. We do NOT offer any type of reward whatsoever other than this listing, and we do NOT guarantee that the listing will remain in the repository for any release made after the one which will address the vulnerability.
34
+
35
+ ### Maintainers
36
+
37
+ - [Stefan Cameron](mailto:stefan@stefcameron.com)