@hprint/plugins 0.0.1-alpha.2 → 0.0.1-alpha.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.
@@ -1,329 +1,322 @@
1
- import { fabric } from '@hprint/core';
2
- import bwipjs from 'bwip-js';
3
- import { utils } from '@hprint/shared';
4
- import { getUnit, processOptions, formatOriginValues } from '../utils/units';
5
- import type { IEditor, IPluginTempl } from '@hprint/core';
6
-
7
- type IPlugin = Pick<QrCodePlugin, 'addQrCode' | 'setQrCode'>;
8
-
9
- declare module '@hprint/core' {
10
- // eslint-disable-next-line @typescript-eslint/no-empty-interface
11
- interface IEditor extends IPlugin { }
12
- }
13
-
14
- // 二维码生成参数
15
-
16
- enum DotsType {
17
- rounded = 'rounded',
18
- dots = 'dots',
19
- classy = 'classy',
20
- classy_rounded = 'classy-rounded',
21
- square = 'square',
22
- extra_rounded = 'extra-rounded',
23
- }
24
-
25
- enum CornersType {
26
- dot = 'dot',
27
- square = 'square',
28
- extra_rounded = 'extra-rounded',
29
- }
30
-
31
- enum cornersDotType {
32
- dot = 'dot',
33
- square = 'square',
34
- }
35
-
36
- enum errorCorrectionLevelType {
37
- L = 'L',
38
- M = 'M',
39
- Q = 'Q',
40
- H = 'H',
41
- }
42
-
43
- class QrParamsDefaults {
44
- width = 300;
45
- height = 300;
46
- type = 'canvas' as const;
47
- data = ' ';
48
- ecLevel = 'M' as const;
49
- }
50
-
51
- class QrCodePlugin implements IPluginTempl {
52
- static pluginName = 'QrCodePlugin';
53
- static apis = ['addQrCode', 'setQrCode'];
54
- constructor(
55
- public canvas: fabric.Canvas,
56
- public editor: IEditor
57
- ) { }
58
-
59
- async hookTransform(object: any) {
60
- if (object.extensionType === 'qrcode') {
61
- const paramsOption = this._paramsToOption(object.extension);
62
- const url = await this._getBase64Str(paramsOption);
63
- object.src = url;
64
- }
65
- }
66
-
67
- async hookTransformObjectEnd({ originObject, fabricObject }: { originObject: any, fabricObject: any }) {
68
- if (originObject.extensionType === 'qrcode') {
69
- this._bindQrCodeEvents(fabricObject);
70
- }
71
- }
72
-
73
- async _getBase64Str(options: any): Promise<string> {
74
- const zoom = this.canvas.getZoom() || 1;
75
- const dpr = (window && (window as any).devicePixelRatio) || 1;
76
-
77
- const targetWidth = (options.width || 300) * zoom * dpr;
78
- // 估算 module 数量,QR Code 通常在 21-177 之间,取一个中间值作为估算基础
79
- const estimatedModules = 35;
80
- let bwipScale = Math.ceil(targetWidth / estimatedModules);
81
- if (bwipScale < 2) bwipScale = 2;
82
-
83
- const canvas = document.createElement('canvas');
84
-
85
- const barColor = options.color?.replace('#', '') || '000000';
86
- const bgColor = options.bgColor?.replace('#', '') || 'ffffff';
87
- const ecLevel = options.ecLevel || 'M';
88
-
89
- try {
90
- bwipjs.toCanvas(canvas, {
91
- bcid: 'qrcode',
92
- text: options.data || ' ',
93
- scale: bwipScale,
94
- eclevel: ecLevel,
95
- barcolor: barColor,
96
- backgroundcolor: bgColor,
97
- } as any);
98
- return canvas.toDataURL('image/png');
99
- } catch (error) {
100
- console.error('QR Code generation failed:', error);
101
- return '';
102
- }
103
- }
104
-
105
- _defaultBarcodeOption() {
106
- return {
107
- value: '@hprint/print',
108
- width: 300,
109
- margin: 10,
110
- ecLevel: 'M',
111
- };
112
- }
113
-
114
- /**
115
- * 将内部参数转换为二维码库需要的参数
116
- */
117
- _paramsToOption(option: any) {
118
- const hasW = Number.isFinite(option.width);
119
- const hasH = Number.isFinite(option.height);
120
- const size = hasW && hasH
121
- ? Math.max(option.width, option.height)
122
- : (hasW ? option.width : (hasH ? option.height : undefined));
123
- const options = {
124
- ...option,
125
- width: size,
126
- height: size ?? option.width,
127
- type: 'canvas',
128
- data: option.value != null ? String(option.value) : undefined,
129
- margin: option.margin,
130
- };
131
- return options;
132
- }
133
-
134
- private async _updateQrCodeImage(imgEl: fabric.Image, immediate = false) {
135
- const extension = imgEl.get('extension');
136
- if (!extension) return;
137
- const updateFn = async () => {
138
- const currentWidth = imgEl.getScaledWidth();
139
- const currentHeight = imgEl.getScaledHeight();
140
- const size = Math.max(currentWidth, currentHeight);
141
- const options = {
142
- ...extension,
143
- width: size,
144
- height: size,
145
- };
146
- const paramsOption = this._paramsToOption(options);
147
- try {
148
- const url = await this._getBase64Str(paramsOption);
149
- await new Promise<void>((resolve) => {
150
- imgEl.setSrc(url, () => {
151
- this._setImageScale(imgEl, currentWidth, currentHeight);
152
- imgEl.set('extension', options);
153
- this.canvas.renderAll();
154
- resolve();
155
- });
156
- });
157
- } catch (error) {
158
- console.error(error);
159
- }
160
- };
161
- if (immediate) {
162
- await updateFn();
163
- } else {
164
- setTimeout(updateFn, 300);
165
- }
166
- }
167
-
168
- /**
169
- * 设置图片缩放到目标宽高
170
- */
171
- private _setImageScale(
172
- imgEl: fabric.Image,
173
- targetWidth: number,
174
- targetHeight: number
175
- ) {
176
- const imgWidth = imgEl.width || 0;
177
- const imgHeight = imgEl.height || 0;
178
- if (imgWidth > 0 && imgHeight > 0) {
179
- const scaleX = targetWidth / imgWidth;
180
- const scaleY = targetHeight / imgHeight;
181
- imgEl.set({
182
- scaleX,
183
- scaleY,
184
- });
185
- }
186
- }
187
-
188
- /**
189
- * 绑定二维码相关事件与方法
190
- */
191
- private _bindQrCodeEvents(imgEl: fabric.Image) {
192
- (imgEl as any).setExtension = async (fields: Record<string, any>) => {
193
- const currentExt = (imgEl.get('extension') as any) || {};
194
- const merged = { ...currentExt, ...(fields || {}) };
195
- imgEl.set('extension', merged);
196
- await this._updateQrCodeImage(imgEl, true);
197
- };
198
- (imgEl as any).setExtensionByUnit = async (
199
- fields: Record<string, any>,
200
- dpi?: number
201
- ) => {
202
- const curUnit = getUnit(this.editor);
203
- const { processed, originByUnit } = processOptions(fields || {}, curUnit, dpi);
204
- const precision = (this.editor as any).getPrecision?.();
205
- const formattedOrigin = formatOriginValues(originByUnit[curUnit] || {}, precision);
206
- const originSize = (imgEl as any)._originSize || {};
207
- const unitOrigin = originSize[curUnit] || {};
208
- unitOrigin.extension = { ...(unitOrigin.extension || {}), ...formattedOrigin };
209
- (imgEl as any)._originSize = { ...originSize, [curUnit]: unitOrigin };
210
- const currentExt = (imgEl.get('extension') as any) || {};
211
- const merged = { ...currentExt, ...processed };
212
- imgEl.set('extension', merged);
213
- await this._updateQrCodeImage(imgEl, true);
214
- };
215
- (imgEl as any).off?.('modified');
216
- (imgEl as any).off?.('scaled');
217
- imgEl.on('modified', async (event: any) => {
218
- const target = (event?.target as fabric.Image) || imgEl;
219
- await this._updateQrCodeImage(target, true);
220
- });
221
- imgEl.on('scaled', async () => {
222
- await this._updateQrCodeImage(imgEl, true);
223
- });
224
- }
225
-
226
- /**
227
- * 创建二维码,支持传入内容与样式,进行单位转换并存储原始尺寸
228
- */
229
- async addQrCode(
230
- data?: string,
231
- opts?: {
232
- left?: number;
233
- top?: number;
234
- width?: number;
235
- height?: number;
236
- ecLevel?: 'L' | 'M' | 'Q' | 'H';
237
- color: string;
238
- bgColor: string;
239
- },
240
- dpi?: number
241
- ): Promise<fabric.Image> {
242
- const option = {
243
- ...this._defaultBarcodeOption(),
244
- ...(opts || {}),
245
- ...(data ? { value: data } : {}),
246
- };
247
- const unit = getUnit(this.editor);
248
- const { processed, originByUnit } = processOptions(option, unit, dpi, ['left', 'top', 'width', 'height', 'margin']);
249
- const finalOption = { ...option, ...processed };
250
- const paramsOption = this._paramsToOption(finalOption);
251
- const url = await this._getBase64Str(paramsOption);
252
- return new Promise<fabric.Image>((resolve) => {
253
- fabric.Image.fromURL(
254
- url,
255
- (imgEl) => {
256
- const safeLeft = Number.isFinite(processed.left)
257
- ? processed.left
258
- : 0;
259
- const safeTop = Number.isFinite(processed.top)
260
- ? processed.top
261
- : 0;
262
- imgEl.set({
263
- left: safeLeft,
264
- top: safeTop,
265
- extensionType: 'qrcode',
266
- extension: finalOption,
267
- imageSmoothing: false,
268
- });
269
-
270
- const targetWidth =
271
- typeof finalOption.width === 'number'
272
- ? finalOption.width
273
- : imgEl.width ?? 0;
274
- const targetHeight =
275
- typeof finalOption.height === 'number'
276
- ? finalOption.height
277
- : targetWidth;
278
- this._setImageScale(imgEl, targetWidth, targetHeight);
279
-
280
- const origin = originByUnit[unit] || {};
281
- const originMapped: Record<string, any> = { ...origin };
282
- if (
283
- originMapped.height === undefined &&
284
- originMapped.width !== undefined
285
- ) {
286
- originMapped.height = originMapped.width;
287
- }
288
- (imgEl as any)._originSize = { [unit]: originMapped };
289
- this._bindQrCodeEvents(imgEl);
290
- resolve(imgEl);
291
- },
292
- { crossOrigin: 'anonymous' }
293
- );
294
- });
295
- }
296
-
297
- async setQrCode(option: any) {
298
- try {
299
- const paramsOption = this._paramsToOption(option);
300
- const url = await this._getBase64Str(paramsOption);
301
- const activeObject = this.canvas.getActiveObjects()[0];
302
- fabric.Image.fromURL(
303
- url,
304
- (imgEl) => {
305
- imgEl.set({
306
- left: activeObject.left,
307
- top: activeObject.top,
308
- extensionType: 'qrcode',
309
- extension: { ...option },
310
- });
311
- imgEl.scaleToWidth(activeObject.getScaledWidth());
312
- this._bindQrCodeEvents(imgEl);
313
- this.editor.del();
314
- this.canvas.add(imgEl);
315
- this.canvas.setActiveObject(imgEl);
316
- },
317
- { crossOrigin: 'anonymous' }
318
- );
319
- } catch (error) {
320
- console.log(error);
321
- }
322
- }
323
-
324
- destroy() {
325
- console.log('pluginDestroy');
326
- }
327
- }
328
-
329
- export default QrCodePlugin;
1
+ import { fabric } from '@hprint/core';
2
+ import bwipjs from 'bwip-js';
3
+ import { utils } from '@hprint/shared';
4
+ import { getUnit, processOptions, formatOriginValues } from '../utils/units';
5
+ import type { IEditor, IPluginTempl } from '@hprint/core';
6
+
7
+ type IPlugin = Pick<QrCodePlugin, 'addQrCode' | 'setQrCode'>;
8
+
9
+ declare module '@hprint/core' {
10
+ // eslint-disable-next-line @typescript-eslint/no-empty-interface
11
+ interface IEditor extends IPlugin { }
12
+ }
13
+
14
+ class QrCodePlugin implements IPluginTempl {
15
+ static pluginName = 'QrCodePlugin';
16
+ static apis = ['addQrCode', 'setQrCode'];
17
+ constructor(
18
+ public canvas: fabric.Canvas,
19
+ public editor: IEditor
20
+ ) { }
21
+
22
+ async hookTransform(object: any) {
23
+ if (object.extensionType === 'qrcode') {
24
+ const paramsOption = this._paramsToOption(object.extension);
25
+ const { url, width, height } = await this._getQrCodeResult(paramsOption);
26
+ object.src = url;
27
+
28
+ // 修复 base64 生成后,没有拉伸到容器大小的问题
29
+ if (width > 0 && height > 0) {
30
+ const oldWidth = object.width || 0;
31
+ const oldHeight = object.height || 0;
32
+ const oldScaleX = object.scaleX || 1;
33
+ const oldScaleY = object.scaleY || 1;
34
+
35
+ const displayWidth = oldWidth * oldScaleX;
36
+ const displayHeight = oldHeight * oldScaleY;
37
+
38
+ if (displayWidth > 0 && displayHeight > 0) {
39
+ object.width = width;
40
+ object.height = height;
41
+ object.scaleX = displayWidth / width;
42
+ object.scaleY = displayHeight / height;
43
+ }
44
+ }
45
+ }
46
+ }
47
+
48
+ async hookTransformObjectEnd({ originObject, fabricObject }: { originObject: any, fabricObject: any }) {
49
+ if (originObject.extensionType === 'qrcode') {
50
+ this._bindQrCodeEvents(fabricObject);
51
+ }
52
+ }
53
+
54
+ async _getQrCodeResult(options: any): Promise<{ url: string; width: number; height: number }> {
55
+ const zoom = this.canvas.getZoom() || 1;
56
+ const dpr = (window && (window as any).devicePixelRatio) || 1;
57
+
58
+ const targetWidth = (options.width || 300) * zoom * dpr;
59
+ // 估算 module 数量,QR Code 通常在 21-177 之间,取一个中间值作为估算基础
60
+ const estimatedModules = 35;
61
+ // 计算需要的缩放比例,确保生成的图片足够大
62
+ let bwipScale = Math.ceil(targetWidth / estimatedModules);
63
+ // 保证最小缩放比例,避免过小
64
+ if (bwipScale < 2) bwipScale = 2;
65
+
66
+ const canvas = document.createElement('canvas');
67
+
68
+ const barColor = options.color?.replace('#', '') || '000000';
69
+ const bgColor = options.bgColor?.replace('#', '') || 'ffffff';
70
+ const ecLevel = options.ecLevel || 'M';
71
+
72
+ try {
73
+ bwipjs.toCanvas(canvas, {
74
+ bcid: 'qrcode',
75
+ text: options.data || ' ',
76
+ scale: bwipScale,
77
+ eclevel: ecLevel,
78
+ barcolor: barColor,
79
+ backgroundcolor: bgColor,
80
+ } as any);
81
+ return {
82
+ url: canvas.toDataURL('image/png'),
83
+ width: canvas.width,
84
+ height: canvas.height
85
+ };
86
+ } catch (error) {
87
+ console.error('QR Code generation failed:', error);
88
+ return { url: '', width: 0, height: 0 };
89
+ }
90
+ }
91
+
92
+ async _getBase64Str(options: any): Promise<string> {
93
+ const { url } = await this._getQrCodeResult(options);
94
+ return url;
95
+ }
96
+
97
+ _defaultBarcodeOption() {
98
+ return {
99
+ value: '@hprint/print',
100
+ width: 300,
101
+ margin: 10,
102
+ ecLevel: 'M',
103
+ };
104
+ }
105
+
106
+ /**
107
+ * 将内部参数转换为二维码库需要的参数
108
+ */
109
+ _paramsToOption(option: any) {
110
+ const hasW = Number.isFinite(option.width);
111
+ const hasH = Number.isFinite(option.height);
112
+ const size = hasW && hasH
113
+ ? Math.max(option.width, option.height)
114
+ : (hasW ? option.width : (hasH ? option.height : undefined));
115
+ const options = {
116
+ ...option,
117
+ width: size,
118
+ height: size ?? option.width,
119
+ type: 'canvas',
120
+ data: option.value != null ? String(option.value) : undefined,
121
+ margin: option.margin,
122
+ };
123
+ return options;
124
+ }
125
+
126
+ private async _updateQrCodeImage(imgEl: fabric.Image, immediate = false) {
127
+ const extension = imgEl.get('extension');
128
+ if (!extension) return;
129
+ const updateFn = async () => {
130
+ const currentWidth = imgEl.getScaledWidth();
131
+ const currentHeight = imgEl.getScaledHeight();
132
+ const size = Math.max(currentWidth, currentHeight);
133
+ const options = {
134
+ ...extension,
135
+ width: size,
136
+ height: size,
137
+ };
138
+ const paramsOption = this._paramsToOption(options);
139
+ try {
140
+ const url = await this._getBase64Str(paramsOption);
141
+ await new Promise<void>((resolve) => {
142
+ imgEl.setSrc(url, () => {
143
+ this._setImageScale(imgEl, currentWidth, currentHeight);
144
+ imgEl.set('extension', options);
145
+ this.canvas.renderAll();
146
+ resolve();
147
+ });
148
+ });
149
+ } catch (error) {
150
+ console.error(error);
151
+ }
152
+ };
153
+ if (immediate) {
154
+ await updateFn();
155
+ } else {
156
+ setTimeout(updateFn, 300);
157
+ }
158
+ }
159
+
160
+ /**
161
+ * 设置图片缩放到目标宽高
162
+ */
163
+ private _setImageScale(
164
+ imgEl: fabric.Image,
165
+ targetWidth: number,
166
+ targetHeight: number
167
+ ) {
168
+ const imgWidth = imgEl.width || 0;
169
+ const imgHeight = imgEl.height || 0;
170
+ if (imgWidth > 0 && imgHeight > 0) {
171
+ const scaleX = targetWidth / imgWidth;
172
+ const scaleY = targetHeight / imgHeight;
173
+ imgEl.set({
174
+ scaleX,
175
+ scaleY,
176
+ });
177
+ }
178
+ }
179
+
180
+ /**
181
+ * 绑定二维码相关事件与方法
182
+ */
183
+ private _bindQrCodeEvents(imgEl: fabric.Image) {
184
+ (imgEl as any).setExtension = async (fields: Record<string, any>) => {
185
+ const currentExt = (imgEl.get('extension') as any) || {};
186
+ const merged = { ...currentExt, ...(fields || {}) };
187
+ imgEl.set('extension', merged);
188
+ await this._updateQrCodeImage(imgEl, true);
189
+ };
190
+ (imgEl as any).setExtensionByUnit = async (
191
+ fields: Record<string, any>,
192
+ dpi?: number
193
+ ) => {
194
+ const curUnit = getUnit(this.editor);
195
+ const { processed, originByUnit } = processOptions(fields || {}, curUnit, dpi);
196
+ const precision = (this.editor as any).getPrecision?.();
197
+ const formattedOrigin = formatOriginValues(originByUnit[curUnit] || {}, precision);
198
+ const originSize = (imgEl as any)._originSize || {};
199
+ const unitOrigin = originSize[curUnit] || {};
200
+ unitOrigin.extension = { ...(unitOrigin.extension || {}), ...formattedOrigin };
201
+ (imgEl as any)._originSize = { ...originSize, [curUnit]: unitOrigin };
202
+ const currentExt = (imgEl.get('extension') as any) || {};
203
+ const merged = { ...currentExt, ...processed };
204
+ imgEl.set('extension', merged);
205
+ await this._updateQrCodeImage(imgEl, true);
206
+ };
207
+ (imgEl as any).off?.('modified');
208
+ (imgEl as any).off?.('scaled');
209
+ imgEl.on('modified', async (event: any) => {
210
+ const target = (event?.target as fabric.Image) || imgEl;
211
+ await this._updateQrCodeImage(target, true);
212
+ });
213
+ imgEl.on('scaled', async () => {
214
+ await this._updateQrCodeImage(imgEl, true);
215
+ });
216
+
217
+ }
218
+
219
+ /**
220
+ * 创建二维码,支持传入内容与样式,进行单位转换并存储原始尺寸
221
+ */
222
+ async addQrCode(
223
+ data?: string,
224
+ opts?: {
225
+ left?: number;
226
+ top?: number;
227
+ width?: number;
228
+ height?: number;
229
+ ecLevel?: 'L' | 'M' | 'Q' | 'H';
230
+ color: string;
231
+ bgColor: string;
232
+ },
233
+ dpi?: number
234
+ ): Promise<fabric.Image> {
235
+ const option = {
236
+ ...this._defaultBarcodeOption(),
237
+ ...(opts || {}),
238
+ ...(data ? { value: data } : {}),
239
+ };
240
+ const unit = getUnit(this.editor);
241
+ const { processed, originByUnit } = processOptions(option, unit, dpi, ['left', 'top', 'width', 'height', 'margin']);
242
+ const finalOption = { ...option, ...processed };
243
+ const paramsOption = this._paramsToOption(finalOption);
244
+ const url = await this._getBase64Str(paramsOption);
245
+ return new Promise<fabric.Image>((resolve) => {
246
+ fabric.Image.fromURL(
247
+ url,
248
+ (imgEl) => {
249
+ const safeLeft = Number.isFinite(processed.left)
250
+ ? processed.left
251
+ : 0;
252
+ const safeTop = Number.isFinite(processed.top)
253
+ ? processed.top
254
+ : 0;
255
+ imgEl.set({
256
+ left: safeLeft,
257
+ top: safeTop,
258
+ extensionType: 'qrcode',
259
+ extension: finalOption,
260
+ imageSmoothing: false,
261
+ });
262
+
263
+ const targetWidth =
264
+ typeof finalOption.width === 'number'
265
+ ? finalOption.width
266
+ : imgEl.width ?? 0;
267
+ const targetHeight =
268
+ typeof finalOption.height === 'number'
269
+ ? finalOption.height
270
+ : targetWidth;
271
+ this._setImageScale(imgEl, targetWidth, targetHeight);
272
+
273
+ const origin = originByUnit[unit] || {};
274
+ const originMapped: Record<string, any> = { ...origin };
275
+ if (
276
+ originMapped.height === undefined &&
277
+ originMapped.width !== undefined
278
+ ) {
279
+ originMapped.height = originMapped.width;
280
+ }
281
+ (imgEl as any)._originSize = { [unit]: originMapped };
282
+ this._bindQrCodeEvents(imgEl);
283
+ resolve(imgEl);
284
+ },
285
+ { crossOrigin: 'anonymous' }
286
+ );
287
+ });
288
+ }
289
+
290
+ async setQrCode(option: any) {
291
+ try {
292
+ const paramsOption = this._paramsToOption(option);
293
+ const url = await this._getBase64Str(paramsOption);
294
+ const activeObject = this.canvas.getActiveObjects()[0];
295
+ fabric.Image.fromURL(
296
+ url,
297
+ (imgEl) => {
298
+ imgEl.set({
299
+ left: activeObject.left,
300
+ top: activeObject.top,
301
+ extensionType: 'qrcode',
302
+ extension: { ...option },
303
+ });
304
+ imgEl.scaleToWidth(activeObject.getScaledWidth());
305
+ this._bindQrCodeEvents(imgEl);
306
+ this.editor.del();
307
+ this.canvas.add(imgEl);
308
+ this.canvas.setActiveObject(imgEl);
309
+ },
310
+ { crossOrigin: 'anonymous' }
311
+ );
312
+ } catch (error) {
313
+ console.log(error);
314
+ }
315
+ }
316
+
317
+ destroy() {
318
+ console.log('pluginDestroy');
319
+ }
320
+ }
321
+
322
+ export default QrCodePlugin;