@hprint/plugins 0.0.1-alpha.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.
Files changed (153) hide show
  1. package/dist/index.css +1 -0
  2. package/dist/index.js +478 -0
  3. package/dist/index.mjs +41731 -0
  4. package/dist/src/index.d.ts +8 -0
  5. package/dist/src/index.d.ts.map +1 -0
  6. package/dist/src/objects/Arrow.d.ts +2 -0
  7. package/dist/src/objects/Arrow.d.ts.map +1 -0
  8. package/dist/src/objects/ThinTailArrow.d.ts +2 -0
  9. package/dist/src/objects/ThinTailArrow.d.ts.map +1 -0
  10. package/dist/src/plugins/AddBaseTypePlugin.d.ts +26 -0
  11. package/dist/src/plugins/AddBaseTypePlugin.d.ts.map +1 -0
  12. package/dist/src/plugins/AlignGuidLinePlugin.d.ts +16 -0
  13. package/dist/src/plugins/AlignGuidLinePlugin.d.ts.map +1 -0
  14. package/dist/src/plugins/BarCodePlugin.d.ts +68 -0
  15. package/dist/src/plugins/BarCodePlugin.d.ts.map +1 -0
  16. package/dist/src/plugins/CenterAlignPlugin.d.ts +29 -0
  17. package/dist/src/plugins/CenterAlignPlugin.d.ts.map +1 -0
  18. package/dist/src/plugins/ControlsPlugin.d.ts +11 -0
  19. package/dist/src/plugins/ControlsPlugin.d.ts.map +1 -0
  20. package/dist/src/plugins/ControlsRotatePlugin.d.ts +11 -0
  21. package/dist/src/plugins/ControlsRotatePlugin.d.ts.map +1 -0
  22. package/dist/src/plugins/CopyPlugin.d.ts +30 -0
  23. package/dist/src/plugins/CopyPlugin.d.ts.map +1 -0
  24. package/dist/src/plugins/CreateElementPlugin.d.ts +121 -0
  25. package/dist/src/plugins/CreateElementPlugin.d.ts.map +1 -0
  26. package/dist/src/plugins/DeleteHotKeyPlugin.d.ts +25 -0
  27. package/dist/src/plugins/DeleteHotKeyPlugin.d.ts.map +1 -0
  28. package/dist/src/plugins/DrawLinePlugin.d.ts +26 -0
  29. package/dist/src/plugins/DrawLinePlugin.d.ts.map +1 -0
  30. package/dist/src/plugins/DrawPolygonPlugin.d.ts +41 -0
  31. package/dist/src/plugins/DrawPolygonPlugin.d.ts.map +1 -0
  32. package/dist/src/plugins/DringPlugin.d.ts +33 -0
  33. package/dist/src/plugins/DringPlugin.d.ts.map +1 -0
  34. package/dist/src/plugins/FlipPlugin.d.ts +26 -0
  35. package/dist/src/plugins/FlipPlugin.d.ts.map +1 -0
  36. package/dist/src/plugins/FontPlugin.d.ts +33 -0
  37. package/dist/src/plugins/FontPlugin.d.ts.map +1 -0
  38. package/dist/src/plugins/FreeDrawPlugin.d.ts +23 -0
  39. package/dist/src/plugins/FreeDrawPlugin.d.ts.map +1 -0
  40. package/dist/src/plugins/GroupAlignPlugin.d.ts +24 -0
  41. package/dist/src/plugins/GroupAlignPlugin.d.ts.map +1 -0
  42. package/dist/src/plugins/GroupPlugin.d.ts +24 -0
  43. package/dist/src/plugins/GroupPlugin.d.ts.map +1 -0
  44. package/dist/src/plugins/GroupTextEditorPlugin.d.ts +18 -0
  45. package/dist/src/plugins/GroupTextEditorPlugin.d.ts.map +1 -0
  46. package/dist/src/plugins/HistoryPlugin.d.ts +30 -0
  47. package/dist/src/plugins/HistoryPlugin.d.ts.map +1 -0
  48. package/dist/src/plugins/ImageStroke.d.ts +18 -0
  49. package/dist/src/plugins/ImageStroke.d.ts.map +1 -0
  50. package/dist/src/plugins/LayerPlugin.d.ts +31 -0
  51. package/dist/src/plugins/LayerPlugin.d.ts.map +1 -0
  52. package/dist/src/plugins/LockPlugin.d.ts +27 -0
  53. package/dist/src/plugins/LockPlugin.d.ts.map +1 -0
  54. package/dist/src/plugins/MaskPlugin.d.ts +38 -0
  55. package/dist/src/plugins/MaskPlugin.d.ts.map +1 -0
  56. package/dist/src/plugins/MaterialPlugin.d.ts +45 -0
  57. package/dist/src/plugins/MaterialPlugin.d.ts.map +1 -0
  58. package/dist/src/plugins/MiddleMousePlugin.d.ts +18 -0
  59. package/dist/src/plugins/MiddleMousePlugin.d.ts.map +1 -0
  60. package/dist/src/plugins/MoveHotKeyPlugin.d.ts +12 -0
  61. package/dist/src/plugins/MoveHotKeyPlugin.d.ts.map +1 -0
  62. package/dist/src/plugins/PathTextPlugin.d.ts +30 -0
  63. package/dist/src/plugins/PathTextPlugin.d.ts.map +1 -0
  64. package/dist/src/plugins/PolygonModifyPlugin.d.ts +28 -0
  65. package/dist/src/plugins/PolygonModifyPlugin.d.ts.map +1 -0
  66. package/dist/src/plugins/PrintPlugin.d.ts +39 -0
  67. package/dist/src/plugins/PrintPlugin.d.ts.map +1 -0
  68. package/dist/src/plugins/PsdPlugin.d.ts +17 -0
  69. package/dist/src/plugins/PsdPlugin.d.ts.map +1 -0
  70. package/dist/src/plugins/QrCodePlugin.d.ts +137 -0
  71. package/dist/src/plugins/QrCodePlugin.d.ts.map +1 -0
  72. package/dist/src/plugins/ResizePlugin.d.ts +44 -0
  73. package/dist/src/plugins/ResizePlugin.d.ts.map +1 -0
  74. package/dist/src/plugins/RulerPlugin.d.ts +24 -0
  75. package/dist/src/plugins/RulerPlugin.d.ts.map +1 -0
  76. package/dist/src/plugins/SimpleClipImagePlugin.d.ts +18 -0
  77. package/dist/src/plugins/SimpleClipImagePlugin.d.ts.map +1 -0
  78. package/dist/src/plugins/UnitPlugin.d.ts +84 -0
  79. package/dist/src/plugins/UnitPlugin.d.ts.map +1 -0
  80. package/dist/src/plugins/WaterMarkPlugin.d.ts +40 -0
  81. package/dist/src/plugins/WaterMarkPlugin.d.ts.map +1 -0
  82. package/dist/src/plugins/WorkspacePlugin.d.ts +57 -0
  83. package/dist/src/plugins/WorkspacePlugin.d.ts.map +1 -0
  84. package/dist/src/types/eventType.d.ts +11 -0
  85. package/dist/src/types/eventType.d.ts.map +1 -0
  86. package/dist/src/utils/psd.d.ts +3 -0
  87. package/dist/src/utils/psd.d.ts.map +1 -0
  88. package/dist/src/utils/ruler/guideline.d.ts +4 -0
  89. package/dist/src/utils/ruler/guideline.d.ts.map +1 -0
  90. package/dist/src/utils/ruler/index.d.ts +5 -0
  91. package/dist/src/utils/ruler/index.d.ts.map +1 -0
  92. package/dist/src/utils/ruler/ruler.d.ts +147 -0
  93. package/dist/src/utils/ruler/ruler.d.ts.map +1 -0
  94. package/dist/src/utils/ruler/utils.d.ts +50 -0
  95. package/dist/src/utils/ruler/utils.d.ts.map +1 -0
  96. package/dist/src/utils/units.d.ts +22 -0
  97. package/dist/src/utils/units.d.ts.map +1 -0
  98. package/package.json +51 -0
  99. package/src/assets/edgecontrol.svg +17 -0
  100. package/src/assets/lock.svg +7 -0
  101. package/src/assets/middlecontrol.svg +17 -0
  102. package/src/assets/middlecontrolhoz.svg +17 -0
  103. package/src/assets/rotateicon.svg +20 -0
  104. package/src/assets/style/resizePlugin.css +27 -0
  105. package/src/index.ts +121 -0
  106. package/src/objects/Arrow.js +47 -0
  107. package/src/objects/ThinTailArrow.js +50 -0
  108. package/src/plugins/AddBaseTypePlugin.ts +107 -0
  109. package/src/plugins/AlignGuidLinePlugin.ts +1141 -0
  110. package/src/plugins/BarCodePlugin.ts +860 -0
  111. package/src/plugins/CenterAlignPlugin.ts +133 -0
  112. package/src/plugins/ControlsPlugin.ts +251 -0
  113. package/src/plugins/ControlsRotatePlugin.ts +111 -0
  114. package/src/plugins/CopyPlugin.ts +255 -0
  115. package/src/plugins/CreateElementPlugin.ts +548 -0
  116. package/src/plugins/DeleteHotKeyPlugin.ts +57 -0
  117. package/src/plugins/DrawLinePlugin.ts +162 -0
  118. package/src/plugins/DrawPolygonPlugin.ts +205 -0
  119. package/src/plugins/DringPlugin.ts +125 -0
  120. package/src/plugins/FlipPlugin.ts +59 -0
  121. package/src/plugins/FontPlugin.ts +165 -0
  122. package/src/plugins/FreeDrawPlugin.ts +49 -0
  123. package/src/plugins/GroupAlignPlugin.ts +365 -0
  124. package/src/plugins/GroupPlugin.ts +82 -0
  125. package/src/plugins/GroupTextEditorPlugin.ts +198 -0
  126. package/src/plugins/HistoryPlugin.ts +181 -0
  127. package/src/plugins/ImageStroke.ts +121 -0
  128. package/src/plugins/LayerPlugin.ts +108 -0
  129. package/src/plugins/LockPlugin.ts +240 -0
  130. package/src/plugins/MaskPlugin.ts +155 -0
  131. package/src/plugins/MaterialPlugin.ts +224 -0
  132. package/src/plugins/MiddleMousePlugin.ts +45 -0
  133. package/src/plugins/MoveHotKeyPlugin.ts +46 -0
  134. package/src/plugins/PathTextPlugin.ts +89 -0
  135. package/src/plugins/PolygonModifyPlugin.ts +224 -0
  136. package/src/plugins/PrintPlugin.ts +81 -0
  137. package/src/plugins/PsdPlugin.ts +52 -0
  138. package/src/plugins/QrCodePlugin.ts +393 -0
  139. package/src/plugins/ResizePlugin.ts +274 -0
  140. package/src/plugins/RulerPlugin.ts +78 -0
  141. package/src/plugins/SimpleClipImagePlugin.ts +244 -0
  142. package/src/plugins/UnitPlugin.ts +327 -0
  143. package/src/plugins/WaterMarkPlugin.ts +257 -0
  144. package/src/plugins/WorkspacePlugin.ts +307 -0
  145. package/src/types/eventType.ts +11 -0
  146. package/src/utils/psd.js +432 -0
  147. package/src/utils/ruler/guideline.ts +145 -0
  148. package/src/utils/ruler/index.ts +91 -0
  149. package/src/utils/ruler/ruler.ts +924 -0
  150. package/src/utils/ruler/utils.ts +162 -0
  151. package/src/utils/units.ts +133 -0
  152. package/tsconfig.json +10 -0
  153. package/vite.config.ts +29 -0
@@ -0,0 +1,860 @@
1
+ import { fabric, IEditor, IPluginTempl } from '@hprint/core';
2
+ import JsBarcode from 'jsbarcode';
3
+ import { getUnit, processOptions, formatOriginValues } from '../utils/units';
4
+
5
+ type IPlugin = Pick<
6
+ BarCodePlugin,
7
+ 'addBarcode' | 'setBarcode' | 'getBarcodeTypes'
8
+ >;
9
+
10
+ declare module '@hprint/core' {
11
+ // eslint-disable-next-line @typescript-eslint/no-empty-interface
12
+ interface IEditor extends IPlugin { }
13
+ }
14
+
15
+ // 条形码生成参数
16
+ // https://github.com/lindell/JsBarcode/wiki/Options
17
+
18
+ enum CodeType {
19
+ CODE128 = 'CODE128',
20
+ EAN8 = 'EAN8',
21
+ EAN13 = 'EAN13',
22
+ ITF14 = 'ITF14',
23
+ codabar = 'codabar',
24
+ pharmacode = 'pharmacode',
25
+ }
26
+
27
+ class BarCodePlugin implements IPluginTempl {
28
+ static pluginName = 'BarCodePlugin';
29
+ static apis = ['addBarcode', 'setBarcode', 'getBarcodeTypes'];
30
+ constructor(
31
+ public canvas: fabric.Canvas,
32
+ public editor: IEditor
33
+ ) { }
34
+
35
+ async hookTransform(object: any) {
36
+ if (object.extensionType === 'barcode') {
37
+ const extension = object.extension || {};
38
+
39
+ // 恢复保存的宽高信息,如果存在则使用,否则使用默认值
40
+ const options = this._mergeBarcodeOptions(extension);
41
+
42
+ const url = await this._getBase64Str(options);
43
+ object.src = url;
44
+
45
+ // 更新 extension 以确保宽高信息被保存
46
+ object.extension = options;
47
+ }
48
+ }
49
+
50
+ // 绑定条形码对象的事件监听器
51
+ private _bindBarcodeEvents(imgEl: fabric.Image) {
52
+ // 移除旧的事件监听器(如果存在)
53
+ imgEl.off('modified');
54
+ imgEl.off('scaled');
55
+
56
+ // 移除旧的 zoom 处理器(如果存在)
57
+ if ((imgEl as any)._barcodeZoomHandler) {
58
+ this.canvas.off('mouse:wheel', (imgEl as any)._barcodeZoomHandler);
59
+ }
60
+
61
+ // 监听对象修改事件(大小变化)- 立即更新
62
+ imgEl.on('modified', async (event: any) => {
63
+ const target = (event.target as fabric.Image) || imgEl;
64
+ await this._updateBarcodeImage(target, true);
65
+ });
66
+
67
+ // 监听缩放结束事件(立即更新)
68
+ imgEl.on('scaled', async () => {
69
+ await this._updateBarcodeImage(imgEl, true);
70
+ });
71
+
72
+ // 监听 canvas zoom 变化(防抖更新)
73
+ const zoomHandler = () => {
74
+ this._updateBarcodeImage(imgEl, false);
75
+ };
76
+ this.canvas.on('mouse:wheel', zoomHandler);
77
+
78
+ // 保存事件处理器,以便在销毁时移除
79
+ (imgEl as any)._barcodeZoomHandler = zoomHandler;
80
+ }
81
+
82
+ // 加载 JSON 后恢复事件监听器
83
+ hookImportAfter() {
84
+ return new Promise<void>(async (resolve) => {
85
+ // 遍历所有对象,找到条形码对象并重新绑定事件
86
+ const barcodeObjects: fabric.Image[] = [];
87
+ this.canvas.getObjects().forEach((obj) => {
88
+ if (
89
+ obj.type === 'image' &&
90
+ (obj as any).extensionType === 'barcode'
91
+ ) {
92
+ barcodeObjects.push(obj as fabric.Image);
93
+ this._bindBarcodeEvents(obj as fabric.Image);
94
+ }
95
+ });
96
+
97
+ // 重新生成所有条形码的 src,因为 canvas zoom 可能在 hookImportAfter 中被改变了
98
+ // 使用保存的 extension 中的 boxWidth 和 height(原始值),但使用当前的 canvas zoom 重新生成
99
+ await Promise.all(
100
+ barcodeObjects.map(async (imgEl) => {
101
+ const extension = imgEl.get('extension');
102
+ if (!extension) return;
103
+
104
+ // 使用保存的宽高信息,保持原始值不变
105
+ const options = this._mergeBarcodeOptions(extension);
106
+
107
+ try {
108
+ // 使用当前的 canvas zoom 重新生成条形码图片
109
+ const url = await this._getBase64Str(options);
110
+
111
+ // 获取当前的缩放比例,用于计算正确的 scaleX 和 scaleY
112
+ const currentWidth = imgEl.getScaledWidth();
113
+ const currentHeight = imgEl.getScaledHeight();
114
+
115
+ await new Promise<void>((resolve) => {
116
+ imgEl.setSrc(url, () => {
117
+ // 设置缩放比例,使图片显示为期望的尺寸
118
+ this._setImageScale(
119
+ imgEl,
120
+ currentWidth,
121
+ currentHeight
122
+ );
123
+
124
+ // 保持 extension 不变,不更新 boxWidth 和 height
125
+ resolve();
126
+ });
127
+ });
128
+ } catch (error) {
129
+ console.error('重新生成条形码失败:', error);
130
+ }
131
+ })
132
+ );
133
+
134
+ this.canvas.renderAll();
135
+ resolve();
136
+ });
137
+ }
138
+
139
+ // 保存前处理:确保保存宽高信息到 extension
140
+ // 注意:src 不会被导出,因为 toJSON(keys) 只会保存 keys 中指定的属性,而 src 不在 getExtensionKey() 中
141
+ // 保存的是 extension 中已有的 boxWidth 和 height(缩放前的原始值),而不是渲染后的尺寸
142
+ hookSaveBefore() {
143
+ return new Promise<void>((resolve) => {
144
+ // 遍历所有对象,找到条形码对象
145
+ this.canvas.getObjects().forEach((obj) => {
146
+ if (
147
+ obj.type === 'image' &&
148
+ (obj as any).extensionType === 'barcode'
149
+ ) {
150
+ const imgEl = obj as fabric.Image;
151
+ const extension = imgEl.get('extension');
152
+
153
+ if (extension) {
154
+ // 确保 extension 中包含 boxWidth 和 height
155
+ // 这些值已经在 _updateBarcodeImage 中被更新,是缩放前的原始值
156
+ // 如果不存在,使用默认值
157
+ const defaultOption = this._defaultBarcodeOption();
158
+ const finalExtension = {
159
+ ...extension,
160
+ boxWidth:
161
+ extension.boxWidth !== undefined
162
+ ? extension.boxWidth
163
+ : defaultOption.boxWidth,
164
+ height:
165
+ extension.height !== undefined
166
+ ? extension.height
167
+ : defaultOption.height,
168
+ };
169
+
170
+ // 更新 extension,确保保存时包含这些信息
171
+ imgEl.set('extension', finalExtension);
172
+ }
173
+ }
174
+ });
175
+ resolve();
176
+ });
177
+ }
178
+ async _getBase64Str(option: any): Promise<string> {
179
+ // 获取 canvas 的缩放比例,用于提高绘制分辨率
180
+ const zoom = this.canvas.getZoom() || 1;
181
+ const devicePixelRatio = window.devicePixelRatio || 1;
182
+ // 使用 zoom 和 devicePixelRatio 的乘积作为基础缩放因子
183
+ let scale = zoom * devicePixelRatio;
184
+
185
+ // 计算条形码的最小尺寸,用于优化清晰度
186
+ const minDimension = Math.min(
187
+ option.boxWidth || 60,
188
+ option.height || 30
189
+ );
190
+
191
+ // 对于小尺寸的条形码,使用更高的 scale 来保证清晰度
192
+ // 当条形码宽度或高度小于 100px 时,增加 scale
193
+ if (minDimension < 100) {
194
+ // 小尺寸时,使用更高的 scale(至少 3 倍)
195
+ const minScale = 3;
196
+ scale = Math.max(scale, minScale);
197
+ } else if (minDimension < 200) {
198
+ // 中等尺寸时,使用适中的 scale(至少 2 倍)
199
+ const minScale = 2;
200
+ scale = Math.max(scale, minScale);
201
+ }
202
+
203
+ // 设置最大 scale 限制,避免生成过大的图片
204
+ const maxScale = 5;
205
+ scale = Math.min(scale, maxScale);
206
+
207
+ // 必须使用命名空间的svg元素才能正确生成barcode string
208
+ const svg = document.createElementNS(
209
+ 'http://www.w3.org/2000/svg',
210
+ 'svg'
211
+ );
212
+
213
+ // 排除文本相关参数,只传递条形码生成所需的参数
214
+ const {
215
+ fontSize,
216
+ textAlign,
217
+ textPosition,
218
+ displayValue,
219
+ charSpacing,
220
+ lineHeight,
221
+ fontFamily,
222
+ ...barcodeOptions
223
+ } = option;
224
+
225
+ // 生成不包含文本的条形码 SVG
226
+ // 对于小尺寸,增加 width 参数以提高分辨率(JsBarcode 的 width 是线条宽度)
227
+ const barcodeWidth = barcodeOptions.width || 1;
228
+ // 如果条形码尺寸很小,增加线条宽度以提高清晰度
229
+ const adjustedWidth =
230
+ minDimension < 100 ? Math.max(barcodeWidth, 2) : barcodeWidth;
231
+
232
+ JsBarcode(svg, option.value, {
233
+ ...barcodeOptions,
234
+ width: adjustedWidth,
235
+ displayValue: false, // 明确禁用 JsBarcode 的文本显示
236
+ });
237
+
238
+ const svgStr = new XMLSerializer().serializeToString(svg);
239
+ const svgUrl = `data:image/svg+xml;base64,` + btoa(svgStr);
240
+
241
+ // 如果不需要显示文本,直接返回 SVG URL
242
+ if (!displayValue) {
243
+ return svgUrl;
244
+ }
245
+
246
+ // 将 SVG 转换为图片,使用高分辨率
247
+ // 先加载原始 SVG 获取尺寸
248
+ const tempImg = await this._loadImage(svgUrl);
249
+ const originalBarcodeWidth = tempImg.naturalWidth || tempImg.width;
250
+ const originalBarcodeHeight = tempImg.naturalHeight || tempImg.height;
251
+
252
+ // 创建高分辨率 canvas 渲染 SVG
253
+ const svgImage = await this._loadImageToCanvas(svgUrl, scale);
254
+
255
+ // 文本应该匹配 boxWidth(期望的显示宽度),而不是条形码的原始宽度
256
+ // 这样当条形码被拉伸到 boxWidth 时,文本宽度也能匹配
257
+ const textCanvas = this._drawText(option.value, {
258
+ fontSize: fontSize || 12,
259
+ textAlign: textAlign || 'center',
260
+ boxWidth: option.boxWidth || originalBarcodeWidth, // 使用 boxWidth 而不是 originalBarcodeWidth
261
+ scale: scale,
262
+ textPosition: textPosition || 'bottom', // 传递文本位置,用于决定间距
263
+ charSpacing:
264
+ typeof charSpacing === 'number'
265
+ ? charSpacing
266
+ : (option as any).textSpacing ?? 0,
267
+ lineHeight: lineHeight || 1, // 传递行高
268
+ fontFamily: fontFamily || 'Arial', // 传递字体
269
+ });
270
+
271
+ // 合并条形码和文本,使用高分辨率
272
+ const mergedCanvas = this._mergeBarcodeAndText(
273
+ svgImage,
274
+ textCanvas,
275
+ textPosition || 'bottom',
276
+ scale,
277
+ option.height, // 传入目标高度
278
+ option.boxWidth // 传入目标宽度,确保条形码和文本宽度一致
279
+ );
280
+
281
+ // 返回合并后的 base64
282
+ return mergedCanvas.toDataURL('image/png');
283
+ }
284
+
285
+ // 加载图片的辅助方法
286
+ private _loadImage(url: string): Promise<HTMLImageElement> {
287
+ return new Promise((resolve, reject) => {
288
+ const img = new Image();
289
+ img.crossOrigin = 'anonymous';
290
+ img.onload = () => resolve(img);
291
+ img.onerror = reject;
292
+ img.src = url;
293
+ });
294
+ }
295
+
296
+ // 将 SVG 加载到高分辨率 canvas
297
+ private _loadImageToCanvas(
298
+ url: string,
299
+ scale: number
300
+ ): Promise<HTMLCanvasElement> {
301
+ return new Promise((resolve, reject) => {
302
+ const img = new Image();
303
+ img.crossOrigin = 'anonymous';
304
+ img.onload = () => {
305
+ const canvas = document.createElement('canvas');
306
+ const ctx = canvas.getContext('2d');
307
+ if (!ctx) {
308
+ reject(new Error('无法创建 canvas 上下文'));
309
+ return;
310
+ }
311
+
312
+ // 设置高分辨率尺寸
313
+ const originalWidth = img.naturalWidth || img.width;
314
+ const originalHeight = img.naturalHeight || img.height;
315
+
316
+ canvas.width = originalWidth * scale;
317
+ canvas.height = originalHeight * scale;
318
+
319
+ // 使用高质量缩放(对于小尺寸图片,禁用平滑以获得更清晰的线条)
320
+ // 条形码需要清晰的线条,所以对于小尺寸使用 nearest-neighbor 缩放
321
+ const isSmallImage = canvas.width < 300 || canvas.height < 300;
322
+ if (isSmallImage) {
323
+ ctx.imageSmoothingEnabled = false; // 禁用平滑,获得更清晰的像素边界
324
+ } else {
325
+ ctx.imageSmoothingEnabled = true;
326
+ ctx.imageSmoothingQuality = 'high';
327
+ }
328
+
329
+ // 绘制图片到高分辨率 canvas
330
+ ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
331
+
332
+ resolve(canvas);
333
+ };
334
+ img.onerror = reject;
335
+ img.src = url;
336
+ });
337
+ }
338
+
339
+ // 在 canvas 上绘制文本
340
+ private _drawText(
341
+ text: string,
342
+ options: {
343
+ fontSize: number;
344
+ textAlign: string;
345
+ boxWidth: number;
346
+ scale: number;
347
+ textPosition?: string;
348
+ fontFamily?: string;
349
+ charSpacing?: number; // 字符间距
350
+ lineHeight?: number; // 行高
351
+ }
352
+ ): HTMLCanvasElement {
353
+ const canvas = document.createElement('canvas');
354
+ const ctx = canvas.getContext('2d');
355
+ if (!ctx) {
356
+ throw new Error('无法创建 canvas 上下文');
357
+ }
358
+ // 根据缩放比例设置 canvas 的实际尺寸
359
+ const scaledWidth = options.boxWidth * options.scale;
360
+ const scaledFontSize = options.fontSize * options.scale;
361
+ const charSpacing = (options.charSpacing ?? 0) * options.scale;
362
+ const lineHeight = options.lineHeight ?? 1;
363
+ const lines = String(text).split('\n');
364
+ const scaledLineHeight = scaledFontSize * lineHeight;
365
+ const totalTextHeight = scaledLineHeight * lines.length;
366
+ const canvasHeight = totalTextHeight;
367
+
368
+ // 设置 canvas 的实际尺寸(高分辨率)
369
+ canvas.width = scaledWidth;
370
+ canvas.height = canvasHeight;
371
+
372
+ // 设置 canvas 的显示尺寸(CSS 尺寸)
373
+ canvas.style.width = `${options.boxWidth}px`;
374
+ canvas.style.height = `${canvasHeight / options.scale}px`;
375
+
376
+ // 缩放上下文以匹配高分辨率
377
+ ctx.scale(options.scale, options.scale);
378
+
379
+ // 设置字体(使用原始尺寸,因为已经通过 scale 缩放)
380
+ ctx.font = `${options.fontSize}px ${options.fontFamily || 'Arial'}`;
381
+ ctx.textAlign = 'left';
382
+ ctx.textBaseline = 'top';
383
+ ctx.fillStyle = '#000';
384
+
385
+ const leadingScaled = Math.max(0, scaledLineHeight - scaledFontSize);
386
+ const baseY = leadingScaled / 2 / options.scale;
387
+
388
+ const measureLineWidth = (line: string) => {
389
+ if (charSpacing <= 0) {
390
+ return ctx.measureText(line).width;
391
+ }
392
+ let sum = 0;
393
+ for (let i = 0; i < line.length; i++) {
394
+ sum += ctx.measureText(line[i]).width;
395
+ if (i > 0) sum += charSpacing / options.scale;
396
+ }
397
+ return sum;
398
+ };
399
+
400
+ lines.forEach((line, idx) => {
401
+ const rawLineWidth = measureLineWidth(line);
402
+ let lineX = 0;
403
+ if (options.textAlign === 'center') {
404
+ lineX = (options.boxWidth - rawLineWidth) / 2;
405
+ } else if (options.textAlign === 'right') {
406
+ lineX = options.boxWidth - rawLineWidth;
407
+ } else {
408
+ lineX = 0;
409
+ }
410
+ const lineY = baseY + idx * (scaledLineHeight / options.scale);
411
+
412
+ if (charSpacing <= 0) {
413
+ ctx.fillText(line, lineX, lineY);
414
+ } else {
415
+ let cursorX = lineX;
416
+ for (let i = 0; i < line.length; i++) {
417
+ const ch = line[i];
418
+ ctx.fillText(ch, cursorX, lineY);
419
+ const w = ctx.measureText(ch).width;
420
+ cursorX += w + charSpacing / options.scale;
421
+ }
422
+ }
423
+ });
424
+
425
+ return canvas;
426
+ }
427
+
428
+ // 合并条形码图片和文本 canvas
429
+ private _mergeBarcodeAndText(
430
+ barcodeImage: HTMLCanvasElement,
431
+ textCanvas: HTMLCanvasElement,
432
+ textPosition: string,
433
+ scale: number,
434
+ targetHeight?: number,
435
+ targetWidth?: number
436
+ ): HTMLCanvasElement {
437
+ const canvas = document.createElement('canvas');
438
+ const ctx = canvas.getContext('2d');
439
+ if (!ctx) {
440
+ throw new Error('无法创建 canvas 上下文');
441
+ }
442
+
443
+ // 计算合并后的尺寸(使用高分辨率)
444
+ const barcodeWidth = barcodeImage.width;
445
+ const barcodeHeight = barcodeImage.height;
446
+
447
+ // 文本 canvas 的实际尺寸(已经是高分辨率,包含间距)
448
+ const textCanvasHeight = textCanvas.height; // 包含文本和间距的总高度
449
+ const textWidth = textCanvas.width;
450
+
451
+ // 纯文本高度(不包含间距,由 lineHeight 控制)
452
+ const actualTextHeight = textCanvasHeight;
453
+
454
+ // 如果提供了目标宽度,使用目标宽度(确保条形码和文本宽度一致,避免拉伸)
455
+ // 否则使用较大的宽度作为最终宽度(高分辨率)
456
+ const finalWidth =
457
+ targetWidth !== undefined && targetWidth > 0
458
+ ? targetWidth * scale
459
+ : Math.max(barcodeWidth, textWidth);
460
+
461
+ // 计算目标高度(如果提供了 targetHeight)
462
+ let finalHeight: number;
463
+ let barcodeDrawHeight: number; // SVG 的实际绘制高度(可能被拉伸或裁剪)
464
+
465
+ if (targetHeight !== undefined && targetHeight > 0) {
466
+ // 根据传入的 height 和 scale 计算高分辨率目标高度
467
+ const targetHeightScaled = targetHeight * scale;
468
+
469
+ // 规则1: 如果文本 canvas 高度(包含间距)大于目标高度,以文本 canvas 高度为准(保证文本完整展示)
470
+ if (textCanvasHeight > targetHeightScaled) {
471
+ finalHeight = textCanvasHeight;
472
+ // 如果文本在上方,条形码高度为0;如果文本在下方,条形码高度也为0(因为文本占满全部高度)
473
+ barcodeDrawHeight = 0;
474
+ } else {
475
+ // 规则2: 如果 SVG + textCanvas 的高度小于目标高度,需要拉伸 SVG
476
+ const availableHeightForBarcode =
477
+ targetHeightScaled - textCanvasHeight;
478
+ if (barcodeHeight + textCanvasHeight < targetHeightScaled) {
479
+ finalHeight = targetHeightScaled;
480
+ barcodeDrawHeight = availableHeightForBarcode; // 拉伸 SVG
481
+ } else {
482
+ // 规则3: 如果 SVG 高度大于可用高度,裁剪 SVG
483
+ finalHeight = targetHeightScaled;
484
+ barcodeDrawHeight = Math.min(
485
+ barcodeHeight,
486
+ availableHeightForBarcode
487
+ ); // 裁剪 SVG
488
+ }
489
+ }
490
+ } else {
491
+ // 如果没有提供目标高度,使用实际合并后的高度
492
+ finalHeight = barcodeHeight + textCanvasHeight;
493
+ barcodeDrawHeight = barcodeHeight;
494
+ }
495
+
496
+ // 设置 canvas 的实际尺寸(高分辨率)
497
+ canvas.width = finalWidth;
498
+ canvas.height = finalHeight;
499
+
500
+ // 设置 canvas 的显示尺寸(CSS 尺寸)
501
+ canvas.style.width = `${finalWidth / scale}px`;
502
+ canvas.style.height = `${finalHeight / scale}px`;
503
+
504
+ // 设置背景色(如果需要)
505
+ ctx.fillStyle = '#fff';
506
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
507
+
508
+ // 根据 textPosition 决定文本位置
509
+ const textX = (finalWidth - textWidth) / 2;
510
+ // 如果提供了目标宽度,条形码会被拉伸到目标宽度,所以使用目标宽度计算居中位置
511
+ const barcodeDrawWidth =
512
+ targetWidth !== undefined && targetWidth > 0
513
+ ? targetWidth * scale
514
+ : barcodeWidth;
515
+ const barcodeX = (finalWidth - barcodeDrawWidth) / 2;
516
+
517
+ if (textPosition === 'top') {
518
+ // 文本在上方:只需要保留下方间距
519
+ let currentY = 0;
520
+
521
+ // 绘制文本(始终完整显示)
522
+ // 文本 canvas 已经包含了下方间距,直接绘制整个 canvas
523
+ ctx.drawImage(
524
+ textCanvas,
525
+ 0,
526
+ 0,
527
+ textWidth,
528
+ textCanvas.height,
529
+ textX,
530
+ currentY,
531
+ textWidth,
532
+ textCanvas.height
533
+ );
534
+ currentY += textCanvas.height; // 包含文本和下方间距
535
+
536
+ // 绘制条形码(可能被拉伸或裁剪)
537
+ // 如果提供了目标宽度,条形码应该被拉伸/压缩到目标宽度,避免文本被拉伸
538
+ const barcodeDrawWidth =
539
+ targetWidth !== undefined && targetWidth > 0
540
+ ? targetWidth * scale
541
+ : barcodeWidth;
542
+ if (barcodeDrawHeight > 0 && currentY < finalHeight) {
543
+ ctx.drawImage(
544
+ barcodeImage,
545
+ 0,
546
+ 0,
547
+ barcodeWidth,
548
+ barcodeHeight,
549
+ barcodeX,
550
+ currentY,
551
+ barcodeDrawWidth,
552
+ barcodeDrawHeight
553
+ );
554
+ }
555
+ } else {
556
+ // 文本在下方(默认):只需要保留上方间距
557
+ let currentY = 0;
558
+
559
+ // 绘制条形码(可能被拉伸或裁剪)
560
+ // 如果提供了目标宽度,条形码应该被拉伸/压缩到目标宽度,避免文本被拉伸
561
+ const barcodeDrawWidth =
562
+ targetWidth !== undefined && targetWidth > 0
563
+ ? targetWidth * scale
564
+ : barcodeWidth;
565
+ if (barcodeDrawHeight > 0) {
566
+ ctx.drawImage(
567
+ barcodeImage,
568
+ 0,
569
+ 0,
570
+ barcodeWidth,
571
+ barcodeHeight,
572
+ barcodeX,
573
+ currentY,
574
+ barcodeDrawWidth,
575
+ barcodeDrawHeight
576
+ );
577
+ currentY += barcodeDrawHeight;
578
+ }
579
+
580
+ // 绘制文本(始终完整显示)
581
+ // 文本 canvas 已经包含了上方间距,直接绘制整个 canvas
582
+ ctx.drawImage(
583
+ textCanvas,
584
+ 0,
585
+ 0,
586
+ textWidth,
587
+ textCanvas.height,
588
+ textX,
589
+ currentY,
590
+ textWidth,
591
+ textCanvas.height
592
+ );
593
+ }
594
+
595
+ return canvas;
596
+ }
597
+
598
+ _defaultBarcodeOption() {
599
+ return {
600
+ value: '123456',
601
+ format: CodeType.CODE128,
602
+ textAlign: 'center',
603
+ textPosition: 'bottom',
604
+ fontSize: 12,
605
+ background: '#fff',
606
+ lineColor: '#000',
607
+ displayValue: true,
608
+ margin: 0,
609
+ width: 1,
610
+ height: 30,
611
+ boxWidth: 60,
612
+ };
613
+ }
614
+
615
+ // 合并条形码选项,确保 boxWidth 和 height 存在
616
+ private _mergeBarcodeOptions(extension: any) {
617
+ const defaultOption = this._defaultBarcodeOption();
618
+ return {
619
+ ...defaultOption,
620
+ ...extension,
621
+ boxWidth:
622
+ extension.boxWidth !== undefined
623
+ ? extension.boxWidth
624
+ : defaultOption.boxWidth,
625
+ height:
626
+ extension.height !== undefined
627
+ ? extension.height
628
+ : defaultOption.height,
629
+ };
630
+ }
631
+
632
+ // 设置图片的缩放比例,使图片显示为指定的尺寸
633
+ private _setImageScale(
634
+ imgEl: fabric.Image,
635
+ targetWidth: number,
636
+ targetHeight: number
637
+ ) {
638
+ const imgWidth = imgEl.width || 0;
639
+ const imgHeight = imgEl.height || 0;
640
+
641
+ if (imgWidth > 0 && imgHeight > 0) {
642
+ // 计算缩放比例,使图片在 canvas 坐标系中显示为 targetWidth x targetHeight
643
+ const scaleX = targetWidth / imgWidth;
644
+ const scaleY = targetHeight / imgHeight;
645
+
646
+ // 设置缩放,使图片显示为期望的尺寸
647
+ imgEl.set({
648
+ scaleX: scaleX,
649
+ scaleY: scaleY,
650
+ });
651
+ }
652
+ }
653
+
654
+ // 更新条形码图片的辅助方法(带防抖)
655
+ private _updateBarcodeImageDebounced: Map<fabric.Image, NodeJS.Timeout> =
656
+ new Map();
657
+
658
+ private async _updateBarcodeImage(imgEl: fabric.Image, immediate = false) {
659
+ const extension = imgEl.get('extension');
660
+ if (!extension) return;
661
+
662
+ // 如果已经有待执行的更新,清除它
663
+ const existingTimeout = this._updateBarcodeImageDebounced.get(imgEl);
664
+ if (existingTimeout) {
665
+ clearTimeout(existingTimeout);
666
+ }
667
+
668
+ const updateFn = async () => {
669
+ const target = imgEl;
670
+
671
+ // 使用 getScaledWidth/getScaledHeight 获取 canvas 坐标系中的实际显示尺寸
672
+ // 这些方法已经考虑了 scaleX/scaleY,返回的是 canvas 坐标系中的尺寸(不考虑 zoom)
673
+ // 生成的图片应该匹配这个尺寸,这样当图片被加载后,scaleX/scaleY 为 1 时就能正确显示
674
+ const currentWidth = target.getScaledWidth();
675
+ const currentHeight = target.getScaledHeight();
676
+
677
+ // 保持 fontSize 不变,只更新宽度和高度
678
+ const options = {
679
+ ...extension,
680
+ boxWidth: currentWidth,
681
+ height: currentHeight,
682
+ // fontSize 保持不变,不随尺寸变化
683
+ };
684
+
685
+ try {
686
+ const url = await this._getBase64Str(options);
687
+ // setSrc 是异步的,需要在回调中等待图片加载完成后再渲染
688
+ imgEl.setSrc(url, () => {
689
+ // 设置缩放比例,使图片显示为期望的尺寸
690
+ this._setImageScale(imgEl, currentWidth, currentHeight);
691
+
692
+ imgEl.set('extension', options);
693
+ this.canvas.renderAll();
694
+ // 更新完成后清理防抖记录
695
+ this._updateBarcodeImageDebounced.delete(imgEl);
696
+ });
697
+ } catch (error) {
698
+ console.error('更新条形码失败:', error);
699
+ // 发生错误时也要清理防抖记录
700
+ this._updateBarcodeImageDebounced.delete(imgEl);
701
+ }
702
+ };
703
+
704
+ if (immediate) {
705
+ await updateFn();
706
+ } else {
707
+ // 防抖:300ms 后执行
708
+ const timeout = setTimeout(updateFn, 300);
709
+ this._updateBarcodeImageDebounced.set(imgEl, timeout);
710
+ }
711
+ }
712
+
713
+
714
+
715
+ async addBarcode(
716
+ value?: string,
717
+ opts?: {
718
+ left?: number;
719
+ top?: number;
720
+ height?: number;
721
+ boxWidth?: number;
722
+ fontSize?: number;
723
+ format?: string;
724
+ textAlign?: string;
725
+ textPosition?: string;
726
+ background?: string;
727
+ lineColor?: string;
728
+ displayValue?: boolean;
729
+ margin?: number;
730
+ width?: number;
731
+ },
732
+ dpi?: number
733
+ ): Promise<fabric.Image> {
734
+ const option = {
735
+ ...this._defaultBarcodeOption(),
736
+ ...(opts || {}),
737
+ ...(value ? { value } : {}),
738
+ };
739
+ if ((option as any).type && !option.format) {
740
+ (option as any).format = (option as any).type;
741
+ }
742
+ const unit = getUnit(this.editor);
743
+ const { processed, originByUnit } = processOptions(option, unit, dpi);
744
+ const finalOption = { ...option, ...processed };
745
+ const url = await this._getBase64Str(JSON.parse(JSON.stringify(finalOption)));
746
+ return new Promise<fabric.Image>((resolve) => {
747
+ fabric.Image.fromURL(
748
+ url,
749
+ (imgEl) => {
750
+ const safeLeft = (
751
+ typeof processed.left === 'number'
752
+ ? processed.left
753
+ : typeof opts?.left === 'number'
754
+ ? opts!.left!
755
+ : 0
756
+ );
757
+ const safeTop = (
758
+ typeof processed.top === 'number'
759
+ ? processed.top
760
+ : typeof opts?.top === 'number'
761
+ ? opts!.top!
762
+ : 0
763
+ );
764
+ imgEl.set({ left: safeLeft, top: safeTop });
765
+ (imgEl as any).extensionType = 'barcode';
766
+ (imgEl as any).extension = finalOption;
767
+
768
+ const targetWidth =
769
+ typeof finalOption.boxWidth === 'number'
770
+ ? finalOption.boxWidth
771
+ : (imgEl.width ?? 0);
772
+ const targetHeight =
773
+ typeof finalOption.height === 'number'
774
+ ? finalOption.height
775
+ : (imgEl.height ?? 0);
776
+ this._setImageScale(imgEl, targetWidth, targetHeight);
777
+
778
+ const unit = getUnit(this.editor);
779
+ const origin = originByUnit[unit] || {};
780
+ const originMapped: Record<string, any> = { ...origin };
781
+ if (originMapped.boxWidth !== undefined) {
782
+ originMapped.width = originMapped.boxWidth;
783
+ delete originMapped.boxWidth;
784
+ }
785
+ (imgEl as any)._originSize = { [unit]: originMapped };
786
+
787
+ (imgEl as any).setExtension = async (fields: Record<string, any>) => {
788
+ const currentExt = (imgEl.get('extension') as any) || {};
789
+ const merged = { ...currentExt, ...(fields || {}) };
790
+ imgEl.set('extension', merged);
791
+ await this._updateBarcodeImage(imgEl, true);
792
+ };
793
+
794
+ (imgEl as any).setExtensionByUnit = async (
795
+ fields: Record<string, any>,
796
+ dpi?: number
797
+ ) => {
798
+ const curUnit = getUnit(this.editor);
799
+ const { processed, originByUnit } = processOptions(fields || {}, curUnit, dpi);
800
+ const precision = (this.editor as any).getPrecision?.();
801
+ const formattedOrigin = formatOriginValues(originByUnit[curUnit] || {}, precision);
802
+
803
+ const originSize = (imgEl as any)._originSize || {};
804
+ const unitOrigin = originSize[curUnit] || {};
805
+ unitOrigin.extension = { ...(unitOrigin.extension || {}), ...formattedOrigin };
806
+ (imgEl as any)._originSize = { ...originSize, [curUnit]: unitOrigin };
807
+
808
+ const currentExt = (imgEl.get('extension') as any) || {};
809
+ const merged = { ...currentExt, ...processed };
810
+ imgEl.set('extension', merged);
811
+ await this._updateBarcodeImage(imgEl, true);
812
+ };
813
+
814
+ this._bindBarcodeEvents(imgEl);
815
+ resolve(imgEl);
816
+ },
817
+ { crossOrigin: 'anonymous' }
818
+ );
819
+ });
820
+ }
821
+
822
+ async setBarcode(option: any) {
823
+ try {
824
+ const url = await this._getBase64Str(option);
825
+ const activeObject = this.canvas.getActiveObjects()[0];
826
+ fabric.Image.fromURL(
827
+ url,
828
+ (imgEl) => {
829
+ imgEl.set({
830
+ left: activeObject.left,
831
+ top: activeObject.top,
832
+ extensionType: 'barcode',
833
+ extension: { ...option },
834
+ });
835
+ imgEl.scaleToWidth(activeObject.getScaledWidth());
836
+
837
+ // 绑定事件监听器
838
+ this._bindBarcodeEvents(imgEl);
839
+
840
+ this.editor.del();
841
+ this.canvas.add(imgEl);
842
+ this.canvas.setActiveObject(imgEl);
843
+ },
844
+ { crossOrigin: 'anonymous' }
845
+ );
846
+ } catch (error) {
847
+ console.log(error);
848
+ }
849
+ }
850
+
851
+ getBarcodeTypes() {
852
+ return Object.values(CodeType);
853
+ }
854
+
855
+ destroy() {
856
+ console.log('pluginDestroy');
857
+ }
858
+ }
859
+
860
+ export default BarCodePlugin;