@igxjs/text2png 3.0.3 → 3.0.4

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 CHANGED
@@ -1,6 +1,7 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2018
3
+ Copyright (c) 2018 Taka Kojima (original author)
4
+ Copyright (c) 2026 Michael (igxjs) (modifications and maintenance)
4
5
 
5
6
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
7
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -1,89 +1,217 @@
1
- # Text to PNG
1
+ # text2png
2
2
 
3
- We upgraded the `node-canvas` to latest in order to support Node.js v24+.
3
+ Convert text to PNG images with optional fixed dimensions and auto-scaling.
4
4
 
5
- ```js
6
- text2png('Create png image\nfrom multi-line text!');
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @igxjs/text2png
7
9
  ```
8
10
 
9
- ![text2png](./img/text2png.png)
11
+ **Requirements:** [node-canvas](https://github.com/Automattic/node-canvas) (see [installation guide](https://github.com/Automattic/node-canvas/wiki))
10
12
 
11
- ## Quick start
13
+ ## Quick Start
12
14
 
13
- text2png depends on [node-canvas](https://github.com/Automattic/node-canvas).
14
- See [node-canvas wiki](https://github.com/Automattic/node-canvas/wiki) on installing node-canvas.
15
+ ```js
16
+ const fs = require('fs');
17
+ const text2png = require('@igxjs/text2png');
15
18
 
16
- ```
17
- $ npm install @igxjs/text2png
19
+ // Basic usage
20
+ fs.writeFileSync('output.png', text2png('Hello World!'));
21
+
22
+ // With styling
23
+ fs.writeFileSync('styled.png', text2png('Hello!', {
24
+ font: '40px Arial',
25
+ color: 'blue',
26
+ backgroundColor: 'white',
27
+ padding: 20
28
+ }));
29
+
30
+ // Fixed size (great for avatars!)
31
+ fs.writeFileSync('avatar.png', text2png('IGX', {
32
+ width: 200,
33
+ height: 200,
34
+ font: '80px Arial',
35
+ textColor: 'white',
36
+ backgroundColor: '#4F46E5',
37
+ textAlign: 'center',
38
+ verticalAlign: 'middle'
39
+ }));
18
40
  ```
19
41
 
42
+ ## API
43
+
44
+ ### `text2png(text, options)`
45
+
46
+ #### Options
47
+
48
+ | Option | Type | Default | Description |
49
+ |--------|------|---------|-------------|
50
+ | **Text Styling** |
51
+ | `font` | string | `'30px sans-serif'` | CSS font string |
52
+ | `color` / `textColor` | string | `'black'` | Text color (any CSS color) |
53
+ | `textAlign` | string | `'left'` | Horizontal alignment: `'left'`, `'center'`, `'right'` |
54
+ | `strokeWidth` | number | `0` | Text stroke/outline width |
55
+ | `strokeColor` | string | `'white'` | Text stroke color |
56
+ | **Background & Spacing** |
57
+ | `backgroundColor` / `bgColor` | string | `transparent` | Background color |
58
+ | `padding` | number | `0` | Padding on all sides |
59
+ | `paddingLeft/Right/Top/Bottom` | number | `0` | Individual side padding |
60
+ | `lineSpacing` | number | `0` | Extra spacing between lines |
61
+ | **Border** |
62
+ | `borderWidth` | number | `0` | Border width on all sides |
63
+ | `borderLeft/Right/Top/BottomWidth` | number | `0` | Individual border widths |
64
+ | `borderColor` | string | `'black'` | Border color |
65
+ | **Fixed Dimensions** ⭐ |
66
+ | `width` | number | `null` | Fixed width (auto-scales content to fit) |
67
+ | `height` | number | `null` | Fixed height (auto-scales content to fit) |
68
+ | `minFontSize` | number | `8` | Minimum font size when auto-scaling |
69
+ | `verticalAlign` | string | `'middle'` | Vertical alignment: `'top'`, `'middle'`, `'bottom'` |
70
+ | **Font Loading** |
71
+ | `localFontPath` | string | `null` | Path to custom font file |
72
+ | `localFontName` | string | `null` | Name to register custom font as |
73
+ | **Output** |
74
+ | `output` | string | `'buffer'` | Output format: `'buffer'`, `'stream'`, `'dataURL'`, `'canvas'` |
75
+ | `imageSmoothingEnabled` | boolean | `false` | Enable image smoothing |
76
+
77
+ ## Examples
78
+
79
+ ### Auto-sized Image (Default)
20
80
  ```js
21
- const fs = require('fs');
22
- const text2png = require('text2png');
23
- fs.writeFileSync('out.png', text2png('Hello!', { color: 'blue' }));
81
+ const image = text2png('Hello\nWorld', {
82
+ font: '30px Arial',
83
+ color: 'teal',
84
+ backgroundColor: 'linen',
85
+ padding: 20
86
+ });
87
+ // Image size automatically fits the text
24
88
  ```
25
89
 
26
- ## Option
90
+ ![Auto-sized Example](./samples/test-auto-size.png)
91
+
92
+ ### Fixed Size with Auto-scaling
93
+ ```js
94
+ // Long text automatically scales down to fit
95
+ const banner = text2png('This is a very long text that needs to fit!', {
96
+ width: 300,
97
+ height: 100,
98
+ font: '40px Arial',
99
+ textAlign: 'center',
100
+ verticalAlign: 'middle',
101
+ backgroundColor: '#f0f0f0'
102
+ });
103
+ // Text font size automatically scales to fit 300x100
104
+ ```
27
105
 
28
- ``text2png(text, option)``
106
+ ![Auto-scaled Text Example](./samples/test-scaled.png)
29
107
 
30
- |param|default|
31
- |---|---|
32
- |text|(required)|
33
- |option.font|'30px sans-serif'|
34
- |option.textAlign|'left'|
35
- |option.color (or option.textColor)|'black'|
36
- |option.backgroundColor (or option.bgColor)|'transparent'|
37
- |option.lineSpacing|0|
38
- |option.strokeWidth|0|
39
- |option.strokeColor|'white'|
40
- |option.padding|0|
41
- |option.padding(Left\|Top\|Right\|Bottom)|0|
42
- |option.borderWidth|0|
43
- |option.border(Left\|Top\|Right\|Bottom)Width|0|
44
- |option.borderColor|'black'|
45
- |option.localFontPath||
46
- |option.localFontName||
47
- |option.output|'buffer'|
108
+ ### User Avatar
109
+ ```js
110
+ const avatar = text2png('IGX', {
111
+ width: 200,
112
+ height: 200,
113
+ font: '80px Arial',
114
+ textColor: 'white',
115
+ backgroundColor: '#4F46E5',
116
+ textAlign: 'center',
117
+ verticalAlign: 'middle'
118
+ });
119
+ ```
48
120
 
49
- ``option.color = '#000' | 'rgb(0, 0, 0)' | 'black' | ...``
121
+ ![Avatar Example](./samples/test-avatar.png)
50
122
 
51
- ``option.output = 'buffer' | 'stream' | 'dataURL' | 'canvas'``
123
+ ### Multi-line with Fixed Dimensions
124
+ ```js
125
+ const multiLine = text2png('Line 1\nLine 2\nLine 3', {
126
+ width: 250,
127
+ height: 150,
128
+ font: '24px Arial',
129
+ textColor: '#333',
130
+ backgroundColor: '#fff',
131
+ textAlign: 'center',
132
+ verticalAlign: 'middle',
133
+ padding: 10,
134
+ borderWidth: 1,
135
+ borderColor: '#ccc',
136
+ lineSpacing: 8 // Add spacing between lines
137
+ });
138
+ ```
52
139
 
53
- ``option.strokeWidth = 1 | 2 | ... `` A padding may have to be set to avoid cutoff of stroke
140
+ ![Multi-line Example](./samples/test-multiline.png)
54
141
 
55
- ``'canvas'`` returns [node-canvas](https://github.com/Automattic/node-canvas) object.
142
+ ### Fixed Width, Auto Height
143
+ ```js
144
+ const card = text2png('Hello World', {
145
+ width: 400, // Fixed width
146
+ // Height auto-calculates to 65px
147
+ font: '30px Arial',
148
+ textColor: 'blue',
149
+ backgroundColor: 'white',
150
+ textAlign: 'center',
151
+ padding: 20,
152
+ borderWidth: 2,
153
+ borderColor: 'blue'
154
+ });
155
+ ```
56
156
 
57
- If you want to use any custom fonts without installing, use `localFontPath` and `localFontName` property.
157
+ ![Fixed Width Example](./samples/test-fixed-width.png)
58
158
 
159
+ ### Custom Font
59
160
  ```js
60
- text2png('with custom fonts', {
161
+ const styled = text2png('Fancy Text', {
61
162
  font: '50px Lobster',
62
- localFontPath: 'fonts/Lobstar-Regular.ttf',
163
+ localFontPath: './fonts/Lobster-Regular.ttf',
63
164
  localFontName: 'Lobster'
64
165
  });
65
166
  ```
66
167
 
67
- ## Command line interface
168
+ ## How Fixed Dimensions Work
68
169
 
69
- ```
70
- $ npm install -g text2png
71
- $ text2png --help
72
- $ text2png -t "Hello!" -o "output.png"
170
+ When you specify `width` and/or `height`:
171
+
172
+ 1. **Fits naturally** → Content is centered based on alignment options
173
+ 2. 📏 **Too large** Font automatically scales down to fit (respects `minFontSize`)
174
+ 3. ✂️ **Exceeds minimum** → Content is clipped at boundaries
175
+
176
+ Without `width`/`height`, the image auto-sizes to fit content (original behavior).
177
+
178
+ ## CLI Usage
179
+
180
+ ```bash
181
+ npm install -g @igxjs/text2png
182
+ text2png --help
183
+ text2png -t "Hello!" -o output.png
184
+
185
+ # Create an avatar
186
+ text2png -t "IGX" -o avatar.png --width 200 --height 200 \
187
+ --font "80px Arial" --color white --backgroundColor "#4F46E5" \
188
+ --textAlign center --verticalAlign middle
73
189
  ```
74
190
 
75
- ## Example
191
+ ## Output Formats
76
192
 
77
193
  ```js
78
- text2png('Example\nText', {
79
- font: '80px Futura',
80
- color: 'teal',
81
- backgroundColor: 'linen',
82
- lineSpacing: 10,
83
- padding: 20
84
- });
194
+ // Buffer (default) - for file writing
195
+ const buffer = text2png('text', { output: 'buffer' });
196
+ fs.writeFileSync('out.png', buffer);
197
+
198
+ // Stream - for piping
199
+ const stream = text2png('text', { output: 'stream' });
200
+ stream.pipe(fs.createWriteStream('out.png'));
201
+
202
+ // Data URL - for HTML/CSS
203
+ const dataURL = text2png('text', { output: 'dataURL' });
204
+ // image/png;base64,...
205
+
206
+ // Canvas - for further manipulation
207
+ const canvas = text2png('text', { output: 'canvas' });
208
+ const ctx = canvas.getContext('2d');
85
209
  ```
86
210
 
87
- ![ExampleText](./img/exampleText.png)
211
+ ## License
212
+
213
+ MIT
214
+
215
+ ## Credits
88
216
 
89
- Enjoy!
217
+ Built on [node-canvas](https://github.com/Automattic/node-canvas). Updated for Node.js v24+ compatibility.
package/bin/text2png.js CHANGED
@@ -1,10 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const fs = require("fs");
4
- const path = require("path");
3
+ const fs = require("node:fs");
4
+ const path = require("node:path");
5
+
5
6
  const commander = require("commander");
6
- const version = require("../package.json").version;
7
- const text2png = require("../index.js");
7
+
8
+ const { version } = require("../package.json");
9
+ const text2png = require('../index.js');
8
10
 
9
11
  commander
10
12
  .version(version)
@@ -18,7 +20,12 @@ commander
18
20
  .option("-s, --lineSpacing <number>", "line spacing")
19
21
 
20
22
  .option("--strokeWidth <number>", "stroke width")
21
- .option("--strokeColor <number>", "stroke color")
23
+ .option("--strokeColor <color>", "stroke color")
24
+
25
+ .option("--width <number>", "fixed width in pixels")
26
+ .option("--height <number>", "fixed height in pixels")
27
+ .option("--minFontSize <number>", "minimum font size when auto-scaling (default: 8)")
28
+ .option("--verticalAlign <alignment>", "vertical alignment: top, middle, bottom (default: middle)")
22
29
 
23
30
  .option(
24
31
  "-p, --padding <number>",
@@ -47,40 +54,57 @@ commander
47
54
 
48
55
  .parse(process.argv);
49
56
 
57
+ // Helper function to convert string to number
58
+ const toNumber = value => value ? +value : undefined;
59
+
60
+ // Helper function to build options object from commander
61
+ const buildOptions = (commander) => ({
62
+ font: commander.font,
63
+ textAlign: commander.textAlign,
64
+ color: commander.color,
65
+ backgroundColor: commander.backgroundColor,
66
+ lineSpacing: toNumber(commander.lineSpacing),
67
+
68
+ strokeWidth: toNumber(commander.strokeWidth),
69
+ strokeColor: commander.strokeColor,
70
+
71
+ padding: toNumber(commander.padding),
72
+ paddingLeft: toNumber(commander.paddingLeft),
73
+ paddingTop: toNumber(commander.paddingTop),
74
+ paddingRight: toNumber(commander.paddingRight),
75
+ paddingBottom: toNumber(commander.paddingBottom),
76
+
77
+ borderWidth: toNumber(commander.borderWidth),
78
+ borderLeftWidth: toNumber(commander.borderLeftWidth),
79
+ borderTopWidth: toNumber(commander.borderTopWidth),
80
+ borderRightWidth: toNumber(commander.borderRightWidth),
81
+ borderBottomWidth: toNumber(commander.borderBottomWidth),
82
+ borderColor: commander.borderColor,
83
+
84
+ localFontPath: commander.localFontPath,
85
+ localFontName: commander.localFontName,
86
+
87
+ width: toNumber(commander.width),
88
+ height: toNumber(commander.height),
89
+ minFontSize: toNumber(commander.minFontSize),
90
+ verticalAlign: commander.verticalAlign,
91
+
92
+ output: "stream",
93
+ imageSmoothingEnabled: false
94
+ });
95
+
50
96
  const exec = text => {
51
- if ((commander.text || text) && commander.output) {
52
- const stream = text2png(commander.text || text, {
53
- font: commander.font,
54
- textAlign: commander.textAlign,
55
- color: commander.color,
56
- backgroundColor: commander.backgroundColor,
57
- lineSpacing: commander.lineSpacing && +commander.lineSpacing,
58
-
59
- padding: commander.padding && +commander.padding,
60
- paddingLeft: commander.paddingLeft && +commander.paddingLeft,
61
- paddingTop: commander.paddingTop && +commander.paddingTop,
62
- paddingRight: commander.paddingRight && +commander.paddingRight,
63
- paddingBottom: commander.paddingBottom && +commander.paddingBottom,
64
-
65
- borderWidth: commander.borderWidth && +commander.borderWidth,
66
- borderLeftWidth: commander.borderLeftWidth && +commander.borderLeftWidth,
67
- borderTopWidth: commander.borderTopWidth && +commander.borderTopWidth,
68
- borderRightWidth:
69
- commander.borderRightWidth && +commander.borderRightWidth,
70
- borderBottomWidth:
71
- commander.borderBottomWidth && +commander.borderBottomWidth,
72
- borderColor: commander.borderColor,
73
-
74
- localFontPath: commander.localFontPath,
75
- localFontName: commander.localFontName,
76
-
77
- output: "stream"
78
- });
79
- const outputPath = path.resolve(process.cwd(), commander.output);
80
- stream.pipe(fs.createWriteStream(outputPath));
81
- } else {
97
+ const textInput = commander.text || text;
98
+
99
+ if (!textInput || !commander.output) {
82
100
  commander.outputHelp();
101
+ return;
83
102
  }
103
+
104
+ const options = buildOptions(commander);
105
+ const stream = text2png(textInput, options);
106
+ const outputPath = path.resolve(process.cwd(), commander.output);
107
+ stream.pipe(fs.createWriteStream(outputPath));
84
108
  };
85
109
 
86
110
  if (process.stdin.isTTY) {
package/index.js CHANGED
@@ -7,30 +7,54 @@ const { registerFont, createCanvas } = require("canvas");
7
7
  * @returns {string|Buffer|import('node:stream').Readable|import('canvas').Canvas} Returns PNG data as specified by options.output
8
8
  */
9
9
  const text2png = (text, options = {}) => {
10
- // Options
11
10
  options = parseOptions(options);
11
+ registerCustomFont(options);
12
12
 
13
- // Register a custom font
13
+ const canvas = createCanvas(0, 0);
14
+ const ctx = canvas.getContext("2d");
15
+
16
+ // Measure text and calculate natural dimensions
17
+ const textMetrics = measureTextMetrics(ctx, text, options);
18
+ const naturalDimensions = calculateNaturalDimensions(textMetrics, options);
19
+
20
+ // Apply dimensions and scaling
21
+ const dimensions = applyDimensions(canvas, textMetrics, naturalDimensions, options);
22
+
23
+ // Render background and border
24
+ renderBackground(ctx, canvas, options);
25
+
26
+ // Configure context for text rendering
27
+ configureContext(ctx, dimensions.finalFont, dimensions.scaleFactor, options);
28
+
29
+ // Calculate positioning offsets
30
+ const offsets = calculateOffsets(canvas, dimensions, options);
31
+
32
+ // Render text lines
33
+ renderTextLines(ctx, dimensions.scaledLineProps, dimensions.scaledMax, dimensions.scaledLineHeight, offsets, options, canvas);
34
+
35
+ return formatOutput(canvas, options);
36
+ };
37
+
38
+ /**
39
+ * Register custom font if provided
40
+ */
41
+ function registerCustomFont(options) {
14
42
  if (options.localFontPath && options.localFontName) {
15
43
  try {
16
44
  registerFont(options.localFontPath, { family: options.localFontName });
17
- }
18
- catch(error) {
45
+ } catch (error) {
19
46
  throw new Error(`Failed to load local font from path: ${options.localFontPath}, error: ${error.message}`);
20
47
  }
21
48
  }
49
+ }
22
50
 
23
- const canvas = createCanvas(0, 0);
24
- const ctx = canvas.getContext("2d");
25
-
26
- const max = {
27
- left: 0,
28
- right: 0,
29
- ascent: 0,
30
- descent: 0
31
- };
32
-
51
+ /**
52
+ * Measure text and calculate metrics for all lines
53
+ */
54
+ function measureTextMetrics(ctx, text, options) {
55
+ const max = { left: 0, right: 0, ascent: 0, descent: 0 };
33
56
  let lastDescent;
57
+
34
58
  const lineProps = text.split("\n").map(line => {
35
59
  ctx.font = options.font;
36
60
  const metrics = ctx.measureText(line);
@@ -50,89 +74,220 @@ const text2png = (text, options = {}) => {
50
74
  });
51
75
 
52
76
  const lineHeight = max.ascent + max.descent + options.lineSpacing;
53
-
54
77
  const contentWidth = max.left + max.right;
55
- const contentHeight =
56
- lineHeight * lineProps.length -
57
- options.lineSpacing -
58
- (max.descent - lastDescent);
59
-
60
- canvas.width =
61
- contentWidth +
62
- options.borderLeftWidth +
63
- options.borderRightWidth +
64
- options.paddingLeft +
65
- options.paddingRight;
66
-
67
- canvas.height =
68
- contentHeight +
69
- options.borderTopWidth +
70
- options.borderBottomWidth +
71
- options.paddingTop +
72
- options.paddingBottom;
73
-
74
- const hasBorder =
75
- options.borderLeftWidth ||
76
- options.borderTopWidth ||
77
- options.borderRightWidth ||
78
- options.borderBottomWidth || false;
78
+ const contentHeight = lineHeight * lineProps.length - options.lineSpacing - (max.descent - lastDescent);
79
+
80
+ return { max, lineProps, lastDescent, lineHeight, contentWidth, contentHeight };
81
+ }
82
+
83
+ /**
84
+ * Calculate natural canvas dimensions without fixed sizing
85
+ */
86
+ function calculateNaturalDimensions(textMetrics, options) {
87
+ const width = textMetrics.contentWidth +
88
+ options.borderLeftWidth + options.borderRightWidth +
89
+ options.paddingLeft + options.paddingRight;
90
+
91
+ const height = textMetrics.contentHeight +
92
+ options.borderTopWidth + options.borderBottomWidth +
93
+ options.paddingTop + options.paddingBottom;
94
+
95
+ return { width, height };
96
+ }
97
+
98
+ /**
99
+ * Apply dimensions and calculate scaling if needed
100
+ */
101
+ function applyDimensions(canvas, textMetrics, naturalDimensions, options) {
102
+ const hasFixedDimensions = options.width || options.height;
103
+
104
+ if (!hasFixedDimensions) {
105
+ // Auto-size mode
106
+ canvas.width = naturalDimensions.width;
107
+ canvas.height = naturalDimensions.height;
108
+ return {
109
+ scaleFactor: 1,
110
+ finalFont: options.font,
111
+ scaledMax: { ...textMetrics.max },
112
+ scaledLineProps: textMetrics.lineProps,
113
+ scaledLineHeight: textMetrics.lineHeight
114
+ };
115
+ }
116
+
117
+ // Fixed dimension mode
118
+ const targetWidth = options.width || naturalDimensions.width;
119
+ const targetHeight = options.height || naturalDimensions.height;
120
+
121
+ canvas.width = targetWidth;
122
+ canvas.height = targetHeight;
123
+
124
+ const availableWidth = targetWidth - options.borderLeftWidth - options.borderRightWidth - options.paddingLeft - options.paddingRight;
125
+ const availableHeight = targetHeight - options.borderTopWidth - options.borderBottomWidth - options.paddingTop - options.paddingBottom;
126
+
127
+ let scaleFactor = calculateScaleFactor(textMetrics.contentWidth, textMetrics.contentHeight, availableWidth, availableHeight);
128
+
129
+ if (scaleFactor < 1) {
130
+ const scalingResult = applyFontScaling(options.font, scaleFactor, options.minFontSize);
131
+ scaleFactor = scalingResult.actualScaleFactor;
132
+
133
+ return {
134
+ scaleFactor,
135
+ finalFont: scalingResult.finalFont,
136
+ scaledMax: scaleMetrics(textMetrics.max, scaleFactor),
137
+ scaledLineProps: scaleLineProps(textMetrics.lineProps, scaleFactor),
138
+ scaledLineHeight: textMetrics.lineHeight * scaleFactor
139
+ };
140
+ }
141
+
142
+ return {
143
+ scaleFactor: 1,
144
+ finalFont: options.font,
145
+ scaledMax: { ...textMetrics.max },
146
+ scaledLineProps: textMetrics.lineProps,
147
+ scaledLineHeight: textMetrics.lineHeight
148
+ };
149
+ }
150
+
151
+ /**
152
+ * Apply font scaling based on scale factor and minimum font size
153
+ */
154
+ function applyFontScaling(font, scaleFactor, minFontSize) {
155
+ const originalFontSize = extractFontSize(font);
156
+ const scaledFontSize = Math.max(originalFontSize * scaleFactor, minFontSize);
157
+ const actualScaleFactor = scaledFontSize === minFontSize ? scaledFontSize / originalFontSize : scaleFactor;
158
+
159
+ return {
160
+ finalFont: setFontSize(font, scaledFontSize),
161
+ actualScaleFactor
162
+ };
163
+ }
164
+
165
+ /**
166
+ * Scale metrics object
167
+ */
168
+ function scaleMetrics(max, scaleFactor) {
169
+ return {
170
+ left: max.left * scaleFactor,
171
+ right: max.right * scaleFactor,
172
+ ascent: max.ascent * scaleFactor,
173
+ descent: max.descent * scaleFactor
174
+ };
175
+ }
176
+
177
+ /**
178
+ * Scale line properties
179
+ */
180
+ function scaleLineProps(lineProps, scaleFactor) {
181
+ return lineProps.map(prop => ({
182
+ line: prop.line,
183
+ left: prop.left * scaleFactor,
184
+ right: prop.right * scaleFactor,
185
+ ascent: prop.ascent * scaleFactor,
186
+ descent: prop.descent * scaleFactor
187
+ }));
188
+ }
189
+
190
+ /**
191
+ * Render background and border
192
+ */
193
+ function renderBackground(ctx, canvas, options) {
194
+ const hasBorder = options.borderLeftWidth || options.borderTopWidth || options.borderRightWidth || options.borderBottomWidth;
79
195
 
80
196
  if (hasBorder) {
81
197
  ctx.fillStyle = options.borderColor;
82
198
  ctx.fillRect(0, 0, canvas.width, canvas.height);
83
199
  }
84
200
 
201
+ const innerX = options.borderLeftWidth;
202
+ const innerY = options.borderTopWidth;
203
+ const innerWidth = canvas.width - (options.borderLeftWidth + options.borderRightWidth);
204
+ const innerHeight = canvas.height - (options.borderTopWidth + options.borderBottomWidth);
205
+
85
206
  if (options.backgroundColor) {
86
207
  ctx.fillStyle = options.backgroundColor;
87
- ctx.fillRect(
88
- options.borderLeftWidth,
89
- options.borderTopWidth,
90
- canvas.width - (options.borderLeftWidth + options.borderRightWidth),
91
- canvas.height - (options.borderTopWidth + options.borderBottomWidth)
92
- );
208
+ ctx.fillRect(innerX, innerY, innerWidth, innerHeight);
93
209
  } else if (hasBorder) {
94
- ctx.clearRect(
95
- options.borderLeftWidth,
96
- options.borderTopWidth,
97
- canvas.width - (options.borderLeftWidth + options.borderRightWidth),
98
- canvas.height - (options.borderTopWidth + options.borderBottomWidth)
99
- );
210
+ ctx.clearRect(innerX, innerY, innerWidth, innerHeight);
100
211
  }
212
+ }
101
213
 
102
- ctx.font = options.font;
214
+ /**
215
+ * Configure canvas context for text rendering
216
+ */
217
+ function configureContext(ctx, finalFont, scaleFactor, options) {
218
+ ctx.font = finalFont;
103
219
  ctx.fillStyle = options.textColor;
104
220
  ctx.antialias = 'gray';
105
221
  ctx.imageSmoothingEnabled = options.imageSmoothingEnabled;
106
222
  ctx.textAlign = options.textAlign;
107
- ctx.lineWidth = options.strokeWidth;
223
+ ctx.lineWidth = options.strokeWidth * scaleFactor;
108
224
  ctx.strokeStyle = options.strokeColor;
225
+ }
109
226
 
110
- let offsetY = options.borderTopWidth + options.paddingTop;
111
- lineProps.forEach(lineProp => {
112
- // Calculate Y
113
- let x = 0;
114
- const y = max.ascent + offsetY;
115
-
116
- // Calculate X
117
- switch (options.textAlign) {
118
- case "start":
119
- case "left":
120
- x = lineProp.left + options.borderLeftWidth + options.paddingLeft;
121
- break;
122
-
123
- case "end":
124
- case "right":
125
- x =
126
- canvas.width -
127
- lineProp.left -
128
- options.borderRightWidth -
129
- options.paddingRight;
130
- break;
131
-
132
- case "center":
133
- x = contentWidth / 2 + options.borderLeftWidth + options.paddingLeft;
134
- break;
135
- }
227
+ /**
228
+ * Calculate vertical and horizontal offsets for text positioning
229
+ */
230
+ function calculateOffsets(canvas, dimensions, options) {
231
+ const verticalOffset = calculateVerticalOffset(canvas, dimensions, options);
232
+ const horizontalOffset = calculateHorizontalOffset(canvas, dimensions, options);
233
+
234
+ return { verticalOffset, horizontalOffset };
235
+ }
236
+
237
+ /**
238
+ * Calculate vertical offset based on alignment
239
+ */
240
+ function calculateVerticalOffset(canvas, dimensions, options) {
241
+ if (!options.height) {
242
+ return 0;
243
+ }
244
+
245
+ const availableHeight = canvas.height - options.borderTopWidth - options.borderBottomWidth - options.paddingTop - options.paddingBottom;
246
+ const scaledContentHeight = dimensions.scaledLineHeight * dimensions.scaledLineProps.length -
247
+ options.lineSpacing * dimensions.scaleFactor -
248
+ (dimensions.scaledMax.descent - dimensions.scaledLineProps[dimensions.scaledLineProps.length - 1].descent);
249
+
250
+ switch (options.verticalAlign) {
251
+ case "top":
252
+ return 0;
253
+ case "middle":
254
+ return (availableHeight - scaledContentHeight) / 2;
255
+ case "bottom":
256
+ return availableHeight - scaledContentHeight;
257
+ default:
258
+ return 0;
259
+ }
260
+ }
261
+
262
+ /**
263
+ * Calculate horizontal offset based on alignment
264
+ */
265
+ function calculateHorizontalOffset(canvas, dimensions, options) {
266
+ if (!options.width) {
267
+ return 0;
268
+ }
269
+
270
+ const availableWidth = canvas.width - options.borderLeftWidth - options.borderRightWidth - options.paddingLeft - options.paddingRight;
271
+ const scaledContentWidth = dimensions.scaledMax.left + dimensions.scaledMax.right;
272
+
273
+ if (options.textAlign === 'center') {
274
+ return (availableWidth - scaledContentWidth) / 2;
275
+ } else if (options.textAlign === 'right' || options.textAlign === 'end') {
276
+ return availableWidth - scaledContentWidth;
277
+ }
278
+
279
+ return 0;
280
+ }
281
+
282
+ /**
283
+ * Render text lines
284
+ */
285
+ function renderTextLines(ctx, scaledLineProps, scaledMax, scaledLineHeight, offsets, options, canvas) {
286
+ let offsetY = options.borderTopWidth + options.paddingTop + offsets.verticalOffset;
287
+
288
+ scaledLineProps.forEach(lineProp => {
289
+ const x = calculateTextX(lineProp, scaledMax, offsets.horizontalOffset, options, canvas);
290
+ const y = scaledMax.ascent + offsetY;
136
291
 
137
292
  ctx.fillText(lineProp.line, x, y);
138
293
 
@@ -140,9 +295,35 @@ const text2png = (text, options = {}) => {
140
295
  ctx.strokeText(lineProp.line, x, y);
141
296
  }
142
297
 
143
- offsetY += lineHeight;
298
+ offsetY += scaledLineHeight;
144
299
  });
300
+ }
301
+
302
+ /**
303
+ * Calculate X position for text based on alignment
304
+ */
305
+ function calculateTextX(lineProp, scaledMax, horizontalOffset, options, canvas) {
306
+ switch (options.textAlign) {
307
+ case "start":
308
+ case "left":
309
+ return lineProp.left + options.borderLeftWidth + options.paddingLeft + horizontalOffset;
145
310
 
311
+ case "end":
312
+ case "right":
313
+ return canvas.width - options.borderRightWidth - options.paddingRight - horizontalOffset;
314
+
315
+ case "center":
316
+ return (scaledMax.left + scaledMax.right) / 2 + options.borderLeftWidth + options.paddingLeft + horizontalOffset;
317
+
318
+ default:
319
+ return lineProp.left + options.borderLeftWidth + options.paddingLeft + horizontalOffset;
320
+ }
321
+ }
322
+
323
+ /**
324
+ * Format output based on options
325
+ */
326
+ function formatOutput(canvas, options) {
146
327
  switch (options.output) {
147
328
  case "buffer":
148
329
  return canvas.toBuffer();
@@ -155,7 +336,7 @@ const text2png = (text, options = {}) => {
155
336
  default:
156
337
  throw new Error(`output type:${options.output} is not supported.`);
157
338
  }
158
- };
339
+ }
159
340
 
160
341
  function parseOptions(options) {
161
342
  return {
@@ -184,7 +365,12 @@ function parseOptions(options) {
184
365
 
185
366
  output: orOr(options.output, "buffer"),
186
367
 
187
- imageSmoothingEnabled: orOr(options.imageSmoothingEnabled, false)
368
+ imageSmoothingEnabled: orOr(options.imageSmoothingEnabled, false),
369
+
370
+ width: orOr(options.width, null),
371
+ height: orOr(options.height, null),
372
+ minFontSize: orOr(options.minFontSize, 8),
373
+ verticalAlign: orOr(options.verticalAlign, "middle")
188
374
  };
189
375
  }
190
376
 
@@ -197,6 +383,42 @@ function orOr() {
197
383
  return arguments[arguments.length - 1];
198
384
  }
199
385
 
386
+ /**
387
+ * Extract font size from font string (e.g., "30px sans-serif" -> 30)
388
+ * @param {string} font Font string
389
+ * @returns {number} Font size in pixels
390
+ */
391
+ function extractFontSize(font) {
392
+ const regex = /(\d+(?:\.\d+)?)\s*px/i;
393
+ const match = regex.exec(font);
394
+ return match ? Number.parseFloat(match[1]) : 30;
395
+ }
396
+
397
+ /**
398
+ * Create new font string with different size
399
+ * @param {string} font Original font string
400
+ * @param {number} newSize New size in pixels
401
+ * @returns {string} New font string
402
+ */
403
+ function setFontSize(font, newSize) {
404
+ const regex = /(\d+(?:\.\d+)?)\s*px/i;
405
+ return font.replace(regex, `${newSize}px`);
406
+ }
407
+
408
+ /**
409
+ * Calculate scale factor to fit content in fixed dimensions
410
+ * @param {number} contentWidth Natural content width
411
+ * @param {number} contentHeight Natural content height
412
+ * @param {number} availableWidth Available width
413
+ * @param {number} availableHeight Available height
414
+ * @returns {number} Scale factor (1 = no scaling needed)
415
+ */
416
+ function calculateScaleFactor(contentWidth, contentHeight, availableWidth, availableHeight) {
417
+ const widthScale = availableWidth / contentWidth;
418
+ const heightScale = availableHeight / contentHeight;
419
+ return Math.min(widthScale, heightScale, 1);
420
+ }
421
+
200
422
  module.exports = text2png;
201
423
 
202
424
  module.exports.text2png = text2png;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@igxjs/text2png",
3
- "version": "3.0.3",
3
+ "version": "3.0.4",
4
4
  "description": "Convert text to png for node.js",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -13,7 +13,7 @@
13
13
  },
14
14
  "repository": {
15
15
  "type": "git",
16
- "url": "git+https://github.com/ibm-garage-experience/text2png.git"
16
+ "url": "git+https://github.com/igxjs/text2png.git"
17
17
  },
18
18
  "keywords": [
19
19
  "text",
@@ -22,12 +22,12 @@
22
22
  "publishConfig": {
23
23
  "access": "public"
24
24
  },
25
- "author": "tkrkts <tkrkt2773@gmail.com> (http://github.com/tkrkt)",
25
+ "author": "Michael",
26
26
  "license": "MIT",
27
27
  "bugs": {
28
- "url": "https://github.com/ibm-garage-experience/text2png/issues"
28
+ "url": "https://github.com/igxjs/text2png/issues"
29
29
  },
30
- "homepage": "https://github.com/ibm-garage-experience/text2png#readme",
30
+ "homepage": "https://github.com/igxjs/text2png#readme",
31
31
  "files": [
32
32
  "bin",
33
33
  "README.md",
package/types.d.ts CHANGED
@@ -73,6 +73,18 @@ export interface Text2PngOptions {
73
73
 
74
74
  /** Enable or disable image smoothing (default: false) */
75
75
  imageSmoothingEnabled?: boolean;
76
+
77
+ /** Fixed width in pixels (optional, defaults to auto-calculated) */
78
+ width?: number;
79
+
80
+ /** Fixed height in pixels (optional, defaults to auto-calculated) */
81
+ height?: number;
82
+
83
+ /** Minimum font size when auto-scaling to fit fixed dimensions (default: 8) */
84
+ minFontSize?: number;
85
+
86
+ /** Vertical alignment when height is fixed (default: "middle") */
87
+ verticalAlign?: "top" | "middle" | "bottom";
76
88
  }
77
89
 
78
90
  declare function text2png(text: string, options?: Text2PngOptions): string | Buffer | Readable | Canvas;