mjpic 1.0.9 → 1.0.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/assets/index-C6nMBMvY.css +1 -0
- package/dist/client/assets/index-Tbz-to9P.js +197 -0
- package/dist/client/index.html +2 -2
- package/package.json +1 -1
- package/src/components/layout/CanvasArea.tsx +145 -50
- package/src/components/layout/Header.tsx +82 -32
- package/dist/client/assets/index--OgUxz8X.js +0 -197
- package/dist/client/assets/index-BoiS81Ei.css +0 -1
package/dist/client/index.html
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
<meta name="description" content="敏捷图片(mjpic)是一个轻量级网页版图片处理工具,设计灵感来源于光影魔术手。" />
|
|
8
8
|
<title>敏捷图片 (mjpic) - 轻量级网页版图片处理工具</title>
|
|
9
|
-
<script type="module" crossorigin src="/assets/index
|
|
10
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-Tbz-to9P.js"></script>
|
|
10
|
+
<link rel="stylesheet" crossorigin href="/assets/index-C6nMBMvY.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
13
13
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -75,8 +75,9 @@ export const CanvasArea = ({ stageRef }: CanvasAreaProps) => {
|
|
|
75
75
|
// Use default values to prevent undefined issues
|
|
76
76
|
const { size = 0, applyHorizontal = true, applyVertical = false } = config.border;
|
|
77
77
|
if (size > 0) {
|
|
78
|
-
|
|
79
|
-
if (
|
|
78
|
+
// Ensure border dimensions are integers to avoid sub-pixel black lines during rendering
|
|
79
|
+
if (applyHorizontal) borderW = Math.round(imgWidth * (size / 100));
|
|
80
|
+
if (applyVertical) borderH = Math.round(imgHeight * (size / 100));
|
|
80
81
|
}
|
|
81
82
|
}
|
|
82
83
|
|
|
@@ -231,7 +232,13 @@ export const CanvasArea = ({ stageRef }: CanvasAreaProps) => {
|
|
|
231
232
|
filters.push(sharpnessFilter);
|
|
232
233
|
}
|
|
233
234
|
|
|
234
|
-
// Auto Enhance -
|
|
235
|
+
// Auto Enhance - 智能自适应图片美化
|
|
236
|
+
// 算法特点:
|
|
237
|
+
// 1. 基于图像直方图分析,动态生成优化参数
|
|
238
|
+
// 2. 自适应亮度控制:保护高光,提升暗部
|
|
239
|
+
// 3. 智能对比度:根据动态范围自适应
|
|
240
|
+
// 4. 自然饱和度:避免过度饱和
|
|
241
|
+
// 5. 保持色温不变
|
|
235
242
|
if (config.enhancements?.autoEnhance) {
|
|
236
243
|
// @ts-ignore
|
|
237
244
|
const autoEnhanceFilter = function(imageData) {
|
|
@@ -240,71 +247,138 @@ export const CanvasArea = ({ stageRef }: CanvasAreaProps) => {
|
|
|
240
247
|
const height = imageData.height;
|
|
241
248
|
const pixelCount = width * height;
|
|
242
249
|
|
|
250
|
+
// === 第一阶段:图像统计分析 ===
|
|
243
251
|
// 使用采样来加速统计(每8个像素采样一次)
|
|
244
252
|
const sampleStep = 8;
|
|
245
|
-
let sumR = 0, sumG = 0, sumB = 0;
|
|
246
253
|
let minVal = 255, maxVal = 0;
|
|
247
254
|
let totalLuminance = 0;
|
|
255
|
+
let darkPixels = 0, brightPixels = 0; // 暗部和亮部像素统计
|
|
248
256
|
let sampleCount = 0;
|
|
249
257
|
|
|
258
|
+
// 用于计算色彩分布
|
|
259
|
+
let sumR = 0, sumG = 0, sumB = 0;
|
|
260
|
+
|
|
250
261
|
for (let i = 0; i < data.length; i += 4 * sampleStep) {
|
|
251
262
|
const r = data[i], g = data[i+1], b = data[i+2];
|
|
252
|
-
sumR += r; sumG += g; sumB += b;
|
|
253
263
|
const lum = 0.299 * r + 0.587 * g + 0.114 * b;
|
|
264
|
+
|
|
254
265
|
if (lum < minVal) minVal = lum;
|
|
255
266
|
if (lum > maxVal) maxVal = lum;
|
|
256
267
|
totalLuminance += lum;
|
|
268
|
+
|
|
269
|
+
// 统计暗部(<50)和亮部(>200)像素
|
|
270
|
+
if (lum < 50) darkPixels++;
|
|
271
|
+
if (lum > 200) brightPixels++;
|
|
272
|
+
|
|
273
|
+
sumR += r; sumG += g; sumB += b;
|
|
257
274
|
sampleCount++;
|
|
258
275
|
}
|
|
259
276
|
|
|
277
|
+
const avgLuminance = totalLuminance / sampleCount;
|
|
278
|
+
const dynamicRange = maxVal - minVal;
|
|
279
|
+
const darkRatio = darkPixels / sampleCount;
|
|
280
|
+
const brightRatio = brightPixels / sampleCount;
|
|
281
|
+
|
|
282
|
+
// 计算平均色彩(用于饱和度判断)
|
|
260
283
|
const avgR = sumR / sampleCount;
|
|
261
284
|
const avgG = sumG / sampleCount;
|
|
262
285
|
const avgB = sumB / sampleCount;
|
|
263
|
-
const
|
|
286
|
+
const avgColorLum = 0.299 * avgR + 0.587 * avgG + 0.114 * avgB;
|
|
287
|
+
const colorSaturation = (Math.abs(avgR - avgColorLum) + Math.abs(avgG - avgColorLum) + Math.abs(avgB - avgColorLum)) / 3 / 255;
|
|
264
288
|
|
|
265
|
-
//
|
|
266
|
-
const wbR = avgG / (avgR || 1);
|
|
267
|
-
const wbB = avgG / (avgB || 1);
|
|
289
|
+
// === 第二阶段:动态参数生成 ===
|
|
268
290
|
|
|
269
|
-
//
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
291
|
+
// 1. 智能亮度调整(基于图像亮度分布)
|
|
292
|
+
// 使用曲线映射而非固定增量,保护高光
|
|
293
|
+
let shadowBoost = 0; // 暗部提升
|
|
294
|
+
let highlightCompress = 0; // 高光压缩
|
|
273
295
|
|
|
274
|
-
|
|
275
|
-
|
|
296
|
+
if (avgLuminance < 100) {
|
|
297
|
+
// 较暗图片:适度提升暗部,保护高光
|
|
298
|
+
shadowBoost = 25 + (100 - avgLuminance) * 0.15;
|
|
299
|
+
highlightCompress = 0;
|
|
300
|
+
} else if (avgLuminance > 180) {
|
|
301
|
+
// 较亮图片:减少提升,避免过曝
|
|
302
|
+
shadowBoost = 10;
|
|
303
|
+
highlightCompress = (avgLuminance - 180) * 0.3;
|
|
304
|
+
} else {
|
|
305
|
+
// 正常亮度:温和提升
|
|
306
|
+
shadowBoost = 20;
|
|
307
|
+
highlightCompress = 0;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// 2. 智能对比度(基于动态范围)
|
|
311
|
+
// 动态范围小的图片需要更多对比度
|
|
312
|
+
let contrastAdjust = 0;
|
|
313
|
+
if (dynamicRange < 100) {
|
|
314
|
+
contrastAdjust = 8;
|
|
315
|
+
} else if (dynamicRange < 150) {
|
|
316
|
+
contrastAdjust = 5;
|
|
317
|
+
} else if (dynamicRange > 200) {
|
|
318
|
+
contrastAdjust = -3; // 高动态范围图片,轻微降低对比度
|
|
319
|
+
}
|
|
320
|
+
const contrastFactor = contrastAdjust !== 0
|
|
321
|
+
? (259 * (contrastAdjust + 255)) / (255 * (259 - contrastAdjust))
|
|
322
|
+
: 1;
|
|
276
323
|
|
|
277
|
-
//
|
|
278
|
-
|
|
324
|
+
// 3. 智能饱和度(基于当前饱和度)
|
|
325
|
+
// 低饱和度图片需要更多提升,高饱和度图片减少提升
|
|
326
|
+
let saturationBoost = 1.08; // 基础提升8%
|
|
327
|
+
if (colorSaturation < 0.15) {
|
|
328
|
+
saturationBoost = 1.12; // 低饱和度图片提升12%
|
|
329
|
+
} else if (colorSaturation > 0.4) {
|
|
330
|
+
saturationBoost = 1.04; // 高饱和度图片仅提升4%
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// === 第三阶段:应用处理 ===
|
|
279
334
|
for (let i = 0; i < data.length; i += 4) {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
data[i+2] = Math.min(255, data[i+2] * wbB);
|
|
335
|
+
let r = data[i], g = data[i+1], b = data[i+2];
|
|
336
|
+
const lum = 0.299 * r + 0.587 * g + 0.114 * b;
|
|
283
337
|
|
|
284
|
-
//
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
338
|
+
// 步骤1:自适应亮度调整(使用曲线映射)
|
|
339
|
+
// 暗部提升多,亮部提升少,保护高光
|
|
340
|
+
let brightnessCurve = 0;
|
|
341
|
+
if (lum < 80) {
|
|
342
|
+
// 暗部:线性提升
|
|
343
|
+
brightnessCurve = shadowBoost * (1 - lum / 80 * 0.3);
|
|
344
|
+
} else if (lum < 180) {
|
|
345
|
+
// 中间调:渐变减少提升
|
|
346
|
+
brightnessCurve = shadowBoost * 0.7 * (1 - (lum - 80) / 100 * 0.5);
|
|
347
|
+
} else {
|
|
348
|
+
// 高光:轻微提升或压缩
|
|
349
|
+
brightnessCurve = shadowBoost * 0.35 - highlightCompress * ((lum - 180) / 75);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
r = Math.min(255, Math.max(0, r + brightnessCurve));
|
|
353
|
+
g = Math.min(255, Math.max(0, g + brightnessCurve));
|
|
354
|
+
b = Math.min(255, Math.max(0, b + brightnessCurve));
|
|
289
355
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
356
|
+
// 步骤2:对比度调整(以128为中心)
|
|
357
|
+
if (contrastFactor !== 1) {
|
|
358
|
+
r = Math.min(255, Math.max(0, contrastFactor * (r - 128) + 128));
|
|
359
|
+
g = Math.min(255, Math.max(0, contrastFactor * (g - 128) + 128));
|
|
360
|
+
b = Math.min(255, Math.max(0, contrastFactor * (b - 128) + 128));
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// 步骤3:饱和度提升(保持色温)
|
|
364
|
+
const gray = 0.299 * r + 0.587 * g + 0.114 * b;
|
|
365
|
+
r = Math.min(255, Math.max(0, gray + (r - gray) * saturationBoost));
|
|
366
|
+
g = Math.min(255, Math.max(0, gray + (g - gray) * saturationBoost));
|
|
367
|
+
b = Math.min(255, Math.max(0, gray + (b - gray) * saturationBoost));
|
|
293
368
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
data[i] = Math.min(255, gray + (data[i] - gray) * sat);
|
|
298
|
-
data[i+1] = Math.min(255, gray + (data[i+1] - gray) * sat);
|
|
299
|
-
data[i+2] = Math.min(255, gray + (data[i+2] - gray) * sat);
|
|
369
|
+
data[i] = r;
|
|
370
|
+
data[i+1] = g;
|
|
371
|
+
data[i+2] = b;
|
|
300
372
|
}
|
|
301
373
|
|
|
302
|
-
//
|
|
374
|
+
// === 第四阶段:智能锐化 ===
|
|
375
|
+
// 根据图像内容自适应锐化强度
|
|
376
|
+
const sharpnessAmount = dynamicRange < 100 ? 0.25 : 0.2;
|
|
303
377
|
const oldData = new Uint8ClampedArray(data);
|
|
304
|
-
const
|
|
305
|
-
const
|
|
306
|
-
const neighbor = -sharpenAmount;
|
|
378
|
+
const center = 1 + 4 * sharpnessAmount;
|
|
379
|
+
const neighbor = -sharpnessAmount;
|
|
307
380
|
|
|
381
|
+
// 隔行隔列采样处理,提升性能
|
|
308
382
|
for (let y = 1; y < height - 1; y += 2) {
|
|
309
383
|
for (let x = 1; x < width - 1; x += 2) {
|
|
310
384
|
const idx = (y * width + x) * 4;
|
|
@@ -614,16 +688,18 @@ export const CanvasArea = ({ stageRef }: CanvasAreaProps) => {
|
|
|
614
688
|
{image && (
|
|
615
689
|
<Group
|
|
616
690
|
id="content-group"
|
|
617
|
-
|
|
618
|
-
|
|
691
|
+
// Use Math.round to avoid sub-pixel positioning which causes antialiasing black lines on borders
|
|
692
|
+
x={Math.round(stageSize.width / 2)}
|
|
693
|
+
y={Math.round(stageSize.height / 2)}
|
|
619
694
|
rotation={config.rotation}
|
|
620
695
|
>
|
|
621
696
|
{(displayBorderW > 0 || displayBorderH > 0) && (
|
|
622
697
|
<Rect
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
698
|
+
// Round up border dimensions to ensure full coverage
|
|
699
|
+
width={Math.ceil(displayWidth + displayBorderW * 2)}
|
|
700
|
+
height={Math.ceil(displayHeight + displayBorderH * 2)}
|
|
701
|
+
offsetX={Math.round((displayWidth + displayBorderW * 2) / 2)}
|
|
702
|
+
offsetY={Math.round((displayHeight + displayBorderH * 2) / 2)}
|
|
627
703
|
fill={config.border?.color || '#ffffff'}
|
|
628
704
|
listening={false}
|
|
629
705
|
/>
|
|
@@ -638,8 +714,8 @@ export const CanvasArea = ({ stageRef }: CanvasAreaProps) => {
|
|
|
638
714
|
height={imgHeight}
|
|
639
715
|
scaleX={scale}
|
|
640
716
|
scaleY={scale}
|
|
641
|
-
offsetX={imgWidth / 2}
|
|
642
|
-
offsetY={imgHeight / 2}
|
|
717
|
+
offsetX={Math.round(imgWidth / 2)}
|
|
718
|
+
offsetY={Math.round(imgHeight / 2)}
|
|
643
719
|
/>
|
|
644
720
|
</Group>
|
|
645
721
|
)}
|
|
@@ -818,10 +894,11 @@ export const CanvasArea = ({ stageRef }: CanvasAreaProps) => {
|
|
|
818
894
|
anchorStroke="#3b82f6"
|
|
819
895
|
anchorFill="#3b82f6"
|
|
820
896
|
anchorSize={8}
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
897
|
+
// 所有裁剪模式都支持 8 个方向的手柄,方便对称裁剪
|
|
898
|
+
enabledAnchors={[
|
|
899
|
+
'top-left', 'top-right', 'bottom-left', 'bottom-right',
|
|
900
|
+
'top-center', 'bottom-center', 'middle-left', 'middle-right'
|
|
901
|
+
]}
|
|
825
902
|
boundBoxFunc={(oldBox, newBox) => {
|
|
826
903
|
if (newBox.width < 20 || newBox.height < 20) {
|
|
827
904
|
return oldBox;
|
|
@@ -840,6 +917,24 @@ export const CanvasArea = ({ stageRef }: CanvasAreaProps) => {
|
|
|
840
917
|
}
|
|
841
918
|
}
|
|
842
919
|
|
|
920
|
+
// 检测正在使用的手柄,保持中心位置不变
|
|
921
|
+
const transformerInstance = transformerRef.current;
|
|
922
|
+
if (transformerInstance) {
|
|
923
|
+
const activeAnchor = transformerInstance.getActiveAnchor();
|
|
924
|
+
|
|
925
|
+
if (activeAnchor === 'top-center' || activeAnchor === 'bottom-center') {
|
|
926
|
+
// 使用上/下手柄时,保持水平中心位置不变
|
|
927
|
+
const oldCenterX = oldBox.x + oldBox.width / 2;
|
|
928
|
+
const newCenterX = newBox.x + newBox.width / 2;
|
|
929
|
+
newBox.x += oldCenterX - newCenterX;
|
|
930
|
+
} else if (activeAnchor === 'middle-left' || activeAnchor === 'middle-right') {
|
|
931
|
+
// 使用左/右手柄时,保持垂直中心位置不变
|
|
932
|
+
const oldCenterY = oldBox.y + oldBox.height / 2;
|
|
933
|
+
const newCenterY = newBox.y + newBox.height / 2;
|
|
934
|
+
newBox.y += oldCenterY - newCenterY;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
843
938
|
const relX = newBox.x - contentX;
|
|
844
939
|
const relY = newBox.y - contentY;
|
|
845
940
|
if (relX < -5 || relY < -5 ||
|
|
@@ -40,53 +40,103 @@ export const Header = ({ stageRef }: HeaderProps) => {
|
|
|
40
40
|
|
|
41
41
|
try {
|
|
42
42
|
// Get the current state from the image store
|
|
43
|
-
const {
|
|
43
|
+
const { config, originalWidth, originalHeight } = useImageStore.getState();
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
// Find the image node to get actual dimensions
|
|
46
|
+
const imageNode = stageRef.current.findOne('Image') as Konva.Image;
|
|
47
|
+
if (!imageNode) {
|
|
46
48
|
console.error('No image found to save');
|
|
47
49
|
return;
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
// Determine the final dimensions based on resize config
|
|
51
|
-
let finalWidth = originalWidth ||
|
|
52
|
-
let finalHeight = originalHeight ||
|
|
53
|
+
let finalWidth = originalWidth || imageNode.width();
|
|
54
|
+
let finalHeight = originalHeight || imageNode.height();
|
|
53
55
|
|
|
54
56
|
if (config.resize && config.resize.width > 0 && config.resize.height > 0) {
|
|
55
57
|
finalWidth = config.resize.width;
|
|
56
58
|
finalHeight = config.resize.height;
|
|
57
59
|
}
|
|
58
60
|
|
|
59
|
-
//
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
canvas.height = finalHeight;
|
|
63
|
-
const ctx = canvas.getContext('2d');
|
|
61
|
+
// Ensure finalWidth and finalHeight are integers to prevent sub-pixel rendering issues (black lines)
|
|
62
|
+
finalWidth = Math.round(finalWidth);
|
|
63
|
+
finalHeight = Math.round(finalHeight);
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
// Calculate the pixel ratio needed to get the correct output size
|
|
66
|
+
const pixelRatio = finalWidth / imageNode.width();
|
|
67
|
+
|
|
68
|
+
// Hide other elements temporarily to only capture the image
|
|
69
|
+
const transformer = stageRef.current.findOne('Transformer');
|
|
70
|
+
const cropGroup = stageRef.current.findOne('#crop-group');
|
|
71
|
+
const rotationGrid = stageRef.current.findOne('#rotation-grid');
|
|
72
|
+
|
|
73
|
+
const transformerVisible = transformer?.visible();
|
|
74
|
+
const cropGroupVisible = cropGroup?.visible();
|
|
75
|
+
const rotationGridVisible = rotationGrid?.visible();
|
|
76
|
+
|
|
77
|
+
if (transformer) transformer.visible(false);
|
|
78
|
+
if (cropGroup) cropGroup.visible(false);
|
|
79
|
+
if (rotationGrid) rotationGrid.visible(false);
|
|
80
|
+
|
|
81
|
+
// Force a redraw
|
|
82
|
+
stageRef.current.batchDraw();
|
|
83
|
+
|
|
84
|
+
// Wait a bit for the render to complete
|
|
85
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
86
|
+
|
|
87
|
+
// Ensure finalWidth and finalHeight are integers to prevent sub-pixel rendering issues (black lines)
|
|
88
|
+
finalWidth = Math.round(finalWidth);
|
|
89
|
+
finalHeight = Math.round(finalHeight);
|
|
90
|
+
|
|
91
|
+
// Get the cached canvas from the image node
|
|
92
|
+
// This gives us the filtered image without group transformations
|
|
93
|
+
const cachedCanvas = (imageNode as any)._cacheCanvas;
|
|
94
|
+
|
|
95
|
+
let dataUrl: string;
|
|
96
|
+
|
|
97
|
+
if (cachedCanvas) {
|
|
98
|
+
// Create a temporary canvas with the correct output size
|
|
99
|
+
const tempCanvas = document.createElement('canvas');
|
|
100
|
+
tempCanvas.width = finalWidth;
|
|
101
|
+
tempCanvas.height = finalHeight;
|
|
102
|
+
const ctx = tempCanvas.getContext('2d');
|
|
103
|
+
|
|
104
|
+
if (ctx) {
|
|
105
|
+
// Set high quality rendering
|
|
106
|
+
ctx.imageSmoothingEnabled = true;
|
|
107
|
+
ctx.imageSmoothingQuality = 'high';
|
|
108
|
+
|
|
109
|
+
// Ensure dimensions are integers when drawing to avoid sub-pixel interpolation artifacts
|
|
110
|
+
ctx.drawImage(
|
|
111
|
+
cachedCanvas,
|
|
112
|
+
0, 0, Math.round(cachedCanvas.width), Math.round(cachedCanvas.height),
|
|
113
|
+
0, 0, finalWidth, finalHeight
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
// Get the data URL from the temporary canvas
|
|
117
|
+
dataUrl = tempCanvas.toDataURL(format, quality / 100);
|
|
118
|
+
} else {
|
|
119
|
+
// Fallback: use toCanvas if context creation fails
|
|
120
|
+
const fallbackCanvas = imageNode.toCanvas({
|
|
121
|
+
pixelRatio: pixelRatio
|
|
122
|
+
});
|
|
123
|
+
dataUrl = fallbackCanvas.toDataURL(format, quality / 100);
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
// Fallback: use toCanvas if no cache
|
|
127
|
+
const fallbackCanvas = imageNode.toCanvas({
|
|
128
|
+
pixelRatio: pixelRatio
|
|
129
|
+
});
|
|
130
|
+
dataUrl = fallbackCanvas.toDataURL(format, quality / 100);
|
|
68
131
|
}
|
|
69
132
|
|
|
70
|
-
//
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
await new Promise<void>((resolve, reject) => {
|
|
79
|
-
img.onload = () => {
|
|
80
|
-
// Draw the image with the correct dimensions
|
|
81
|
-
ctx.drawImage(img, 0, 0, finalWidth, finalHeight);
|
|
82
|
-
resolve();
|
|
83
|
-
};
|
|
84
|
-
img.onerror = reject;
|
|
85
|
-
img.src = previewImage;
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
// Convert canvas to data URL with correct quality
|
|
89
|
-
const dataUrl = canvas.toDataURL(format, quality / 100);
|
|
133
|
+
// Restore visibility
|
|
134
|
+
if (transformer) transformer.visible(transformerVisible);
|
|
135
|
+
if (cropGroup) cropGroup.visible(cropGroupVisible);
|
|
136
|
+
if (rotationGrid) rotationGrid.visible(rotationGridVisible);
|
|
137
|
+
|
|
138
|
+
// Force another redraw
|
|
139
|
+
stageRef.current.batchDraw();
|
|
90
140
|
|
|
91
141
|
// Check if we are in CLI mode with an opened file
|
|
92
142
|
// If savePath is provided (from dialog), use it
|