@archbase/components 4.0.17 → 4.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/archbase-components-4.0.19.tgz +0 -0
- package/dist/editors/ArchbaseImageEdit.d.ts +21 -1
- package/dist/image/editor/functions/crop-image.d.ts +32 -0
- package/dist/image/editor/index.d.ts +0 -30
- package/dist/image/editor/models/index.models.d.ts +16 -0
- package/dist/index.css +1 -1
- package/dist/index.js +8139 -7944
- package/package.json +5 -6
- package/src/editors/ArchbaseImageEdit.tsx +57 -2
- package/src/image/editor/functions/crop-image.ts +172 -0
- package/src/image/editor/index.tsx +411 -413
- package/src/image/editor/models/index.models.ts +16 -0
- package/dist/archbase-components-4.0.17.tgz +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@archbase/components",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.19",
|
|
4
4
|
"description": "UI Components for Archbase React v3 - Form editors, data visualization, and business components",
|
|
5
5
|
"author": "Edson Martins <edsonmartins2005@gmail.com>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -87,7 +87,6 @@
|
|
|
87
87
|
"react-frame-component": "^5.2.7",
|
|
88
88
|
"react-hook-form": "^7.54.2",
|
|
89
89
|
"react-i18next": "^13.5.0",
|
|
90
|
-
"react-image-picker-editor": "^1.1.1",
|
|
91
90
|
"react-imask": "^7.6.1",
|
|
92
91
|
"react-inspector": "^9.0.0",
|
|
93
92
|
"react-intersection-observer": "^9.10.3",
|
|
@@ -121,9 +120,9 @@
|
|
|
121
120
|
"vis-timeline": "^7.7.3",
|
|
122
121
|
"xlsx": "^0.18.5",
|
|
123
122
|
"yet-another-react-lightbox": "^3.21.0",
|
|
124
|
-
"@archbase/core": "4.0.
|
|
125
|
-
"@archbase/data": "4.0.
|
|
126
|
-
"@archbase/layout": "4.0.
|
|
123
|
+
"@archbase/core": "4.0.19",
|
|
124
|
+
"@archbase/data": "4.0.19",
|
|
125
|
+
"@archbase/layout": "4.0.19"
|
|
127
126
|
},
|
|
128
127
|
"devDependencies": {
|
|
129
128
|
"@types/d3": "^7.4.3",
|
|
@@ -143,7 +142,7 @@
|
|
|
143
142
|
"vitest": "^2.1.8"
|
|
144
143
|
},
|
|
145
144
|
"publishConfig": {
|
|
146
|
-
"registry": "http://192.168.
|
|
145
|
+
"registry": "http://192.168.1.110:4873"
|
|
147
146
|
},
|
|
148
147
|
"scripts": {
|
|
149
148
|
"dev": "NODE_OPTIONS=\"--max-old-space-size=16384\" vite build --watch",
|
|
@@ -60,6 +60,26 @@ export interface ArchbaseImageEditProps<T, ID> extends ImageProps {
|
|
|
60
60
|
maxHeight?: number;
|
|
61
61
|
/** Tamanho máximo da imagem em KB (recomprime se exceder) */
|
|
62
62
|
maxSizeKb?: number;
|
|
63
|
+
/**
|
|
64
|
+
* Preserva a transparência da imagem (canal alfa).
|
|
65
|
+
*
|
|
66
|
+
* Quando true:
|
|
67
|
+
* - desabilita a compressão automática (`compressInitial` é forçado para `null`),
|
|
68
|
+
* impedindo que a biblioteca subjacente reencodifique PNG/WebP como JPEG.
|
|
69
|
+
* - força a saída em PNG (ou WebP, quando a origem já é WebP) ao redimensionar
|
|
70
|
+
* via `maxWidth`/`maxHeight`/`maxSizeKb`.
|
|
71
|
+
*
|
|
72
|
+
* Útil quando o usuário precisa enviar logos com fundo transparente.
|
|
73
|
+
* Default: `false`.
|
|
74
|
+
*/
|
|
75
|
+
preserveTransparency?: boolean;
|
|
76
|
+
/**
|
|
77
|
+
* Habilita logs detalhados do fluxo de processamento da imagem no console
|
|
78
|
+
* (formato detectado, decisões de compressão, mime de saída, tamanhos, etc).
|
|
79
|
+
* Útil para diagnosticar problemas como perda de transparência.
|
|
80
|
+
* Default: `false`.
|
|
81
|
+
*/
|
|
82
|
+
debug?: boolean;
|
|
63
83
|
}
|
|
64
84
|
|
|
65
85
|
export function ArchbaseImageEdit<T, ID>({
|
|
@@ -77,7 +97,7 @@ export function ArchbaseImageEdit<T, ID>({
|
|
|
77
97
|
radius = '4px',
|
|
78
98
|
aspectRatio,
|
|
79
99
|
objectFit = 'contain',
|
|
80
|
-
compressInitial =
|
|
100
|
+
compressInitial = null,
|
|
81
101
|
onChangeImage,
|
|
82
102
|
disabledBase64Convertion,
|
|
83
103
|
innerRef,
|
|
@@ -87,8 +107,26 @@ export function ArchbaseImageEdit<T, ID>({
|
|
|
87
107
|
maxWidth,
|
|
88
108
|
maxHeight,
|
|
89
109
|
maxSizeKb,
|
|
110
|
+
preserveTransparency = false,
|
|
111
|
+
debug = false,
|
|
90
112
|
...otherProps
|
|
91
113
|
}: ArchbaseImageEditProps<T, ID>) {
|
|
114
|
+
// Log inicial das props relevantes — só quando elas realmente mudarem
|
|
115
|
+
// (evita "loop" de logs causado por re-renders do Mantine/dataSource).
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
if (!debug) return;
|
|
118
|
+
// eslint-disable-next-line no-console
|
|
119
|
+
console.log('[ArchbaseImageEdit] props', {
|
|
120
|
+
dataField,
|
|
121
|
+
preserveTransparency,
|
|
122
|
+
compressInitial,
|
|
123
|
+
maxWidth,
|
|
124
|
+
maxHeight,
|
|
125
|
+
maxSizeKb,
|
|
126
|
+
disabledBase64Convertion,
|
|
127
|
+
effectiveCompressInitial: preserveTransparency ? null : compressInitial,
|
|
128
|
+
});
|
|
129
|
+
}, [debug, dataField, preserveTransparency, compressInitial, maxWidth, maxHeight, maxSizeKb, disabledBase64Convertion]);
|
|
92
130
|
// 🔄 MIGRAÇÃO V1/V2: Hook de compatibilidade
|
|
93
131
|
const v1v2Compatibility = useArchbaseV1V2Compatibility<string | undefined>(
|
|
94
132
|
'ArchbaseImageEdit',
|
|
@@ -202,6 +240,19 @@ useEffect(() => {
|
|
|
202
240
|
}, []);
|
|
203
241
|
|
|
204
242
|
const handleChangeImage = (image: string | undefined) => {
|
|
243
|
+
if (debug) {
|
|
244
|
+
const head = typeof image === 'string' ? image.slice(0, 64) : image;
|
|
245
|
+
const mimeMatch = typeof image === 'string' ? image.match(/^data:(image\/[a-zA-Z+\-.]+);/) : null;
|
|
246
|
+
// eslint-disable-next-line no-console
|
|
247
|
+
console.log('[ArchbaseImageEdit] handleChangeImage', {
|
|
248
|
+
dataField,
|
|
249
|
+
incomingMime: mimeMatch?.[1] ?? '(no data uri)',
|
|
250
|
+
incomingHead: head,
|
|
251
|
+
lengthChars: image?.length ?? 0,
|
|
252
|
+
approxSizeKb: image ? Math.ceil(((3 / 4) * image.length) / 1024) : 0,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
205
256
|
// ✅ Limpa erro quando usuário edita o campo (tanto do estado local quanto do contexto)
|
|
206
257
|
const hasError = internalError || contextError;
|
|
207
258
|
if (hasError) {
|
|
@@ -265,7 +316,9 @@ useEffect(() => {
|
|
|
265
316
|
width,
|
|
266
317
|
height,
|
|
267
318
|
objectFit,
|
|
268
|
-
|
|
319
|
+
// preserveTransparency desabilita a compressão automática
|
|
320
|
+
// (que internamente força conversão para JPEG)
|
|
321
|
+
compressInitial: preserveTransparency ? null : compressInitial,
|
|
269
322
|
showImageSize: !isReadOnly(),
|
|
270
323
|
hideDeleteBtn: isReadOnly(),
|
|
271
324
|
hideDownloadBtn: isReadOnly(),
|
|
@@ -276,6 +329,8 @@ useEffect(() => {
|
|
|
276
329
|
maxWidth,
|
|
277
330
|
maxHeight,
|
|
278
331
|
maxSizeKb,
|
|
332
|
+
preserveTransparency,
|
|
333
|
+
debug,
|
|
279
334
|
}}
|
|
280
335
|
/>
|
|
281
336
|
</Input.Wrapper>
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilidades de crop usadas pelo modal de edição do ArchbaseImagePickerEditor.
|
|
3
|
+
* Trabalham diretamente sobre data URIs, preservando o mime original (PNG/WebP
|
|
4
|
+
* mantêm alpha; JPEG continua JPEG). Não há "fallback silencioso" para JPEG.
|
|
5
|
+
*
|
|
6
|
+
* Algoritmo de crop com rotação adaptado do exemplo oficial do react-easy-crop:
|
|
7
|
+
* https://github.com/ValentinH/react-easy-crop/blob/master/example/utils.ts
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export interface PixelCrop {
|
|
11
|
+
x: number;
|
|
12
|
+
y: number;
|
|
13
|
+
width: number;
|
|
14
|
+
height: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Detecta o mime de saída do crop respeitando alpha:
|
|
19
|
+
* - se a origem é PNG ou WebP, ou se `preserveTransparency` está ligado,
|
|
20
|
+
* retornamos um mime com suporte a alpha (PNG ou WebP);
|
|
21
|
+
* - caso contrário, JPEG.
|
|
22
|
+
*/
|
|
23
|
+
function pickOutputMime(sourceDataUri: string, preserveTransparency: boolean): string {
|
|
24
|
+
const match = sourceDataUri.match(/^data:(image\/[a-zA-Z+\-.]+);/);
|
|
25
|
+
const sourceMime = match?.[1] ?? '';
|
|
26
|
+
if (preserveTransparency || sourceMime === 'image/png' || sourceMime === 'image/webp') {
|
|
27
|
+
return sourceMime === 'image/webp' ? 'image/webp' : 'image/png';
|
|
28
|
+
}
|
|
29
|
+
return 'image/jpeg';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function loadImage(src: string): Promise<HTMLImageElement> {
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
const img = new Image();
|
|
35
|
+
img.crossOrigin = 'anonymous';
|
|
36
|
+
img.onload = () => resolve(img);
|
|
37
|
+
img.onerror = (e) => reject(e);
|
|
38
|
+
img.src = src;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function toRadians(deg: number): number {
|
|
43
|
+
return (deg * Math.PI) / 180;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Bounding box que comporta a imagem após rotação. Necessário para o canvas
|
|
48
|
+
* intermediário não cortar a imagem rotacionada.
|
|
49
|
+
*/
|
|
50
|
+
function rotateSize(width: number, height: number, rotation: number): { width: number; height: number } {
|
|
51
|
+
const rotRad = toRadians(rotation);
|
|
52
|
+
return {
|
|
53
|
+
width: Math.abs(Math.cos(rotRad) * width) + Math.abs(Math.sin(rotRad) * height),
|
|
54
|
+
height: Math.abs(Math.sin(rotRad) * width) + Math.abs(Math.cos(rotRad) * height),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Aplica rotação (em graus) + crop sobre `imageSrc` e retorna o data URI
|
|
60
|
+
* resultante.
|
|
61
|
+
*
|
|
62
|
+
* Estratégia:
|
|
63
|
+
* 1. Desenha a imagem rotacionada centralizada num canvas auxiliar do
|
|
64
|
+
* tamanho do bounding box rotacionado.
|
|
65
|
+
* 2. Faz drawImage do recorte (pixelCrop) desse canvas auxiliar para o
|
|
66
|
+
* canvas final do tamanho do recorte.
|
|
67
|
+
*
|
|
68
|
+
* Output mime respeita `preserveTransparency` e a origem (ver pickOutputMime).
|
|
69
|
+
*/
|
|
70
|
+
export async function getCroppedImage(
|
|
71
|
+
imageSrc: string,
|
|
72
|
+
pixelCrop: PixelCrop,
|
|
73
|
+
rotation: number = 0,
|
|
74
|
+
preserveTransparency: boolean = false,
|
|
75
|
+
quality: number = 92,
|
|
76
|
+
): Promise<string> {
|
|
77
|
+
const image = await loadImage(imageSrc);
|
|
78
|
+
const mime = pickOutputMime(imageSrc, preserveTransparency);
|
|
79
|
+
|
|
80
|
+
// 1) Canvas intermediário: imagem rotacionada centralizada.
|
|
81
|
+
const { width: bBoxW, height: bBoxH } = rotateSize(image.width, image.height, rotation);
|
|
82
|
+
const work = document.createElement('canvas');
|
|
83
|
+
work.width = Math.round(bBoxW);
|
|
84
|
+
work.height = Math.round(bBoxH);
|
|
85
|
+
const workCtx = work.getContext('2d');
|
|
86
|
+
if (!workCtx) throw new Error('Canvas 2D context not available');
|
|
87
|
+
|
|
88
|
+
workCtx.translate(bBoxW / 2, bBoxH / 2);
|
|
89
|
+
workCtx.rotate(toRadians(rotation));
|
|
90
|
+
workCtx.translate(-image.width / 2, -image.height / 2);
|
|
91
|
+
workCtx.drawImage(image, 0, 0);
|
|
92
|
+
|
|
93
|
+
// 2) Canvas final do tamanho do recorte (em pixels reais).
|
|
94
|
+
const out = document.createElement('canvas');
|
|
95
|
+
out.width = Math.round(pixelCrop.width);
|
|
96
|
+
out.height = Math.round(pixelCrop.height);
|
|
97
|
+
const outCtx = out.getContext('2d');
|
|
98
|
+
if (!outCtx) throw new Error('Canvas 2D context not available');
|
|
99
|
+
|
|
100
|
+
outCtx.drawImage(
|
|
101
|
+
work,
|
|
102
|
+
Math.round(pixelCrop.x),
|
|
103
|
+
Math.round(pixelCrop.y),
|
|
104
|
+
Math.round(pixelCrop.width),
|
|
105
|
+
Math.round(pixelCrop.height),
|
|
106
|
+
0,
|
|
107
|
+
0,
|
|
108
|
+
Math.round(pixelCrop.width),
|
|
109
|
+
Math.round(pixelCrop.height),
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// PNG ignora o parâmetro de qualidade; passamos mesmo assim para JPEG/WebP.
|
|
113
|
+
return out.toDataURL(mime, quality / 100);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Redimensiona um data URI para respeitar maxWidth/maxHeight, preservando
|
|
118
|
+
* proporção e mime apropriado. Usado fora do fluxo de crop, ao subir um arquivo.
|
|
119
|
+
*/
|
|
120
|
+
export async function resizeDataUri(
|
|
121
|
+
dataUri: string,
|
|
122
|
+
maxWidth?: number,
|
|
123
|
+
maxHeight?: number,
|
|
124
|
+
maxSizeKb?: number,
|
|
125
|
+
quality: number = 92,
|
|
126
|
+
preserveTransparency: boolean = false,
|
|
127
|
+
): Promise<string> {
|
|
128
|
+
if (!maxWidth && !maxHeight && !maxSizeKb) return dataUri;
|
|
129
|
+
|
|
130
|
+
const img = await loadImage(dataUri);
|
|
131
|
+
let targetWidth = img.width;
|
|
132
|
+
let targetHeight = img.height;
|
|
133
|
+
|
|
134
|
+
if (maxWidth && img.width > maxWidth) {
|
|
135
|
+
const ratio = maxWidth / img.width;
|
|
136
|
+
targetWidth = maxWidth;
|
|
137
|
+
targetHeight = Math.round(img.height * ratio);
|
|
138
|
+
}
|
|
139
|
+
if (maxHeight && targetHeight > maxHeight) {
|
|
140
|
+
const ratio = maxHeight / targetHeight;
|
|
141
|
+
targetHeight = maxHeight;
|
|
142
|
+
targetWidth = Math.round(targetWidth * ratio);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (targetWidth === img.width && targetHeight === img.height && !maxSizeKb) {
|
|
146
|
+
return dataUri;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const canvas = document.createElement('canvas');
|
|
150
|
+
canvas.width = targetWidth;
|
|
151
|
+
canvas.height = targetHeight;
|
|
152
|
+
const ctx = canvas.getContext('2d');
|
|
153
|
+
if (!ctx) return dataUri;
|
|
154
|
+
ctx.drawImage(img, 0, 0, targetWidth, targetHeight);
|
|
155
|
+
|
|
156
|
+
const mime = pickOutputMime(dataUri, preserveTransparency);
|
|
157
|
+
|
|
158
|
+
const compressToSize = (currentQuality: number): string => {
|
|
159
|
+
const result = canvas.toDataURL(mime, currentQuality / 100);
|
|
160
|
+
if (maxSizeKb) {
|
|
161
|
+
const base64 = result.split(',')[1] ?? '';
|
|
162
|
+
const sizeKb = Math.ceil(((3 / 4) * base64.length) / 1024);
|
|
163
|
+
if (sizeKb > maxSizeKb && currentQuality > 20 && mime !== 'image/png') {
|
|
164
|
+
// PNG é lossless; reduzir quality não diminui o tamanho.
|
|
165
|
+
return compressToSize(currentQuality - 10);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return result;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
return compressToSize(quality);
|
|
172
|
+
}
|