@umituz/react-native-image 1.1.5 → 1.2.1
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/package.json +25 -10
- package/src/domain/entities/EditorTypes.ts +180 -0
- package/src/domain/entities/ImageFilterTypes.ts +70 -0
- package/src/index.ts +54 -0
- package/src/infrastructure/services/ImageAIEnhancementService.ts +136 -0
- package/src/infrastructure/services/ImageAnnotationService.ts +189 -0
- package/src/infrastructure/services/ImageBatchService.ts +199 -0
- package/src/infrastructure/services/ImageEditorService.ts +274 -0
- package/src/infrastructure/services/ImageFilterService.ts +168 -0
- package/src/infrastructure/services/ImageMetadataService.ts +187 -0
- package/src/infrastructure/services/ImageSpecializedEnhancementService.ts +57 -0
- package/src/infrastructure/utils/AIImageAnalysisUtils.ts +122 -0
- package/src/infrastructure/utils/CanvasRenderingService.ts +134 -0
- package/src/infrastructure/utils/CropTool.ts +260 -0
- package/src/infrastructure/utils/DrawingEngine.ts +210 -0
- package/src/infrastructure/utils/FilterEffects.ts +51 -0
- package/src/infrastructure/utils/FilterProcessor.ts +361 -0
- package/src/infrastructure/utils/ImageQualityPresets.ts +110 -0
- package/src/infrastructure/utils/LayerManager.ts +158 -0
- package/src/infrastructure/utils/ShapeRenderer.ts +168 -0
- package/src/infrastructure/utils/TextEditor.ts +273 -0
- package/src/presentation/components/CropComponent.tsx +183 -0
- package/src/presentation/components/Editor.tsx +261 -0
- package/src/presentation/components/EditorCanvas.tsx +135 -0
- package/src/presentation/components/EditorPanel.tsx +321 -0
- package/src/presentation/components/EditorToolbar.tsx +180 -0
- package/src/presentation/components/FilterSlider.tsx +123 -0
- package/src/presentation/components/GalleryHeader.tsx +87 -25
- package/src/presentation/components/ImageGallery.tsx +97 -48
- package/src/presentation/hooks/useEditorTools.ts +188 -0
- package/src/presentation/hooks/useImage.ts +33 -2
- package/src/presentation/hooks/useImageAIEnhancement.ts +33 -0
- package/src/presentation/hooks/useImageAnnotation.ts +32 -0
- package/src/presentation/hooks/useImageBatch.ts +33 -0
- package/src/presentation/hooks/useImageEditor.ts +165 -38
- package/src/presentation/hooks/useImageFilter.ts +38 -0
- package/src/presentation/hooks/useImageMetadata.ts +28 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Infrastructure - Annotation Service
|
|
3
|
+
*
|
|
4
|
+
* Handles text overlay, drawing, and annotation features
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ImageManipulationResult } from '../../domain/entities/ImageTypes';
|
|
8
|
+
import { ImageValidator } from '../utils/ImageValidator';
|
|
9
|
+
import { ImageErrorHandler, IMAGE_ERROR_CODES } from '../utils/ImageErrorHandler';
|
|
10
|
+
import { CanvasRenderingService } from '../utils/CanvasRenderingService';
|
|
11
|
+
|
|
12
|
+
export interface TextOverlay {
|
|
13
|
+
text: string;
|
|
14
|
+
x: number;
|
|
15
|
+
y: number;
|
|
16
|
+
fontSize?: number;
|
|
17
|
+
fontFamily?: string;
|
|
18
|
+
color?: string;
|
|
19
|
+
backgroundColor?: string;
|
|
20
|
+
maxWidth?: number;
|
|
21
|
+
rotation?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface DrawingElement {
|
|
25
|
+
type: 'line' | 'rectangle' | 'circle' | 'arrow' | 'freehand';
|
|
26
|
+
points: Array<{ x: number; y: number }>;
|
|
27
|
+
color?: string;
|
|
28
|
+
strokeWidth?: number;
|
|
29
|
+
fillColor?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface WatermarkOptions {
|
|
33
|
+
text?: string;
|
|
34
|
+
imageUri?: string;
|
|
35
|
+
position: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'center';
|
|
36
|
+
opacity?: number;
|
|
37
|
+
size?: number;
|
|
38
|
+
margin?: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ImageAnnotation {
|
|
42
|
+
textOverlays?: TextOverlay[];
|
|
43
|
+
drawings?: DrawingElement[];
|
|
44
|
+
watermark?: WatermarkOptions;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class ImageAnnotationService {
|
|
48
|
+
private static getPositionCoordinates(
|
|
49
|
+
position: string,
|
|
50
|
+
imageWidth: number,
|
|
51
|
+
imageHeight: number,
|
|
52
|
+
elementWidth: number,
|
|
53
|
+
elementHeight: number,
|
|
54
|
+
margin: number = 10
|
|
55
|
+
): { x: number; y: number } {
|
|
56
|
+
switch (position) {
|
|
57
|
+
case 'top-left':
|
|
58
|
+
return { x: margin, y: margin };
|
|
59
|
+
case 'top-right':
|
|
60
|
+
return { x: imageWidth - elementWidth - margin, y: margin };
|
|
61
|
+
case 'bottom-left':
|
|
62
|
+
return { x: margin, y: imageHeight - elementHeight - margin };
|
|
63
|
+
case 'bottom-right':
|
|
64
|
+
return { x: imageWidth - elementWidth - margin, y: imageHeight - elementHeight - margin };
|
|
65
|
+
case 'center':
|
|
66
|
+
return {
|
|
67
|
+
x: (imageWidth - elementWidth) / 2,
|
|
68
|
+
y: (imageHeight - elementHeight) / 2
|
|
69
|
+
};
|
|
70
|
+
default:
|
|
71
|
+
return { x: margin, y: margin };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
static async addTextOverlay(
|
|
76
|
+
uri: string,
|
|
77
|
+
overlay: TextOverlay
|
|
78
|
+
): Promise<ImageManipulationResult> {
|
|
79
|
+
try {
|
|
80
|
+
const uriValidation = ImageValidator.validateUri(uri);
|
|
81
|
+
if (!uriValidation.isValid) {
|
|
82
|
+
throw ImageErrorHandler.createError(uriValidation.error!, IMAGE_ERROR_CODES.INVALID_URI, 'addTextOverlay');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// In a real implementation, we would:
|
|
86
|
+
// 1. Load image into canvas
|
|
87
|
+
// 2. Apply text overlay using canvas rendering
|
|
88
|
+
// 3. Export canvas to new URI
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
uri, // Would be processed URI
|
|
92
|
+
width: 0,
|
|
93
|
+
height: 0,
|
|
94
|
+
};
|
|
95
|
+
} catch (error) {
|
|
96
|
+
throw ImageErrorHandler.handleUnknownError(error, 'addTextOverlay');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
static async addDrawingElements(
|
|
101
|
+
uri: string,
|
|
102
|
+
elements: DrawingElement[]
|
|
103
|
+
): Promise<ImageManipulationResult> {
|
|
104
|
+
try {
|
|
105
|
+
const uriValidation = ImageValidator.validateUri(uri);
|
|
106
|
+
if (!uriValidation.isValid) {
|
|
107
|
+
throw ImageErrorHandler.createError(uriValidation.error!, IMAGE_ERROR_CODES.INVALID_URI, 'addDrawingElements');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Mock implementation
|
|
111
|
+
return {
|
|
112
|
+
uri,
|
|
113
|
+
width: 0,
|
|
114
|
+
height: 0,
|
|
115
|
+
};
|
|
116
|
+
} catch (error) {
|
|
117
|
+
throw ImageErrorHandler.handleUnknownError(error, 'addDrawingElements');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
static async addWatermark(
|
|
122
|
+
uri: string,
|
|
123
|
+
options: WatermarkOptions
|
|
124
|
+
): Promise<ImageManipulationResult> {
|
|
125
|
+
try {
|
|
126
|
+
const uriValidation = ImageValidator.validateUri(uri);
|
|
127
|
+
if (!uriValidation.isValid) {
|
|
128
|
+
throw ImageErrorHandler.createError(uriValidation.error!, IMAGE_ERROR_CODES.INVALID_URI, 'addWatermark');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (!options.text && !options.imageUri) {
|
|
132
|
+
throw ImageErrorHandler.createError(
|
|
133
|
+
'Either text or imageUri must be provided for watermark',
|
|
134
|
+
IMAGE_ERROR_CODES.VALIDATION_ERROR,
|
|
135
|
+
'addWatermark'
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Mock implementation
|
|
140
|
+
return {
|
|
141
|
+
uri,
|
|
142
|
+
width: 0,
|
|
143
|
+
height: 0,
|
|
144
|
+
};
|
|
145
|
+
} catch (error) {
|
|
146
|
+
throw ImageErrorHandler.handleUnknownError(error, 'addWatermark');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
static async applyAnnotation(
|
|
151
|
+
uri: string,
|
|
152
|
+
annotation: ImageAnnotation
|
|
153
|
+
): Promise<ImageManipulationResult> {
|
|
154
|
+
try {
|
|
155
|
+
const uriValidation = ImageValidator.validateUri(uri);
|
|
156
|
+
if (!uriValidation.isValid) {
|
|
157
|
+
throw ImageErrorHandler.createError(uriValidation.error!, IMAGE_ERROR_CODES.INVALID_URI, 'applyAnnotation');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Apply all annotations in order
|
|
161
|
+
let resultUri = uri;
|
|
162
|
+
|
|
163
|
+
if (annotation.textOverlays) {
|
|
164
|
+
for (const overlay of annotation.textOverlays) {
|
|
165
|
+
const result = await ImageAnnotationService.addTextOverlay(resultUri, overlay);
|
|
166
|
+
resultUri = result.uri;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (annotation.drawings) {
|
|
171
|
+
const result = await ImageAnnotationService.addDrawingElements(resultUri, annotation.drawings);
|
|
172
|
+
resultUri = result.uri;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (annotation.watermark) {
|
|
176
|
+
const result = await ImageAnnotationService.addWatermark(resultUri, annotation.watermark);
|
|
177
|
+
resultUri = result.uri;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
uri: resultUri,
|
|
182
|
+
width: 0,
|
|
183
|
+
height: 0,
|
|
184
|
+
};
|
|
185
|
+
} catch (error) {
|
|
186
|
+
throw ImageErrorHandler.handleUnknownError(error, 'applyAnnotation');
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Infrastructure - Batch Processing Service
|
|
3
|
+
*
|
|
4
|
+
* Handles processing multiple images concurrently with progress tracking
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ImageManipulationResult } from '../../domain/entities/ImageTypes';
|
|
8
|
+
import type { ImageFilter } from '../../domain/entities/ImageFilterTypes';
|
|
9
|
+
import { ImageTransformService } from './ImageTransformService';
|
|
10
|
+
import { ImageConversionService } from './ImageConversionService';
|
|
11
|
+
import { ImageFilterService } from './ImageFilterService';
|
|
12
|
+
import { ImageValidator } from '../utils/ImageValidator';
|
|
13
|
+
import { ImageErrorHandler, IMAGE_ERROR_CODES } from '../utils/ImageErrorHandler';
|
|
14
|
+
|
|
15
|
+
export interface BatchProcessingOptions {
|
|
16
|
+
concurrency?: number;
|
|
17
|
+
onProgress?: (completed: number, total: number, currentUri?: string) => void;
|
|
18
|
+
onError?: (error: Error, uri: string) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface BatchProcessingResult {
|
|
22
|
+
successful: Array<{
|
|
23
|
+
uri: string;
|
|
24
|
+
result: ImageManipulationResult;
|
|
25
|
+
}>;
|
|
26
|
+
failed: Array<{
|
|
27
|
+
uri: string;
|
|
28
|
+
error: Error;
|
|
29
|
+
}>;
|
|
30
|
+
totalProcessed: number;
|
|
31
|
+
successCount: number;
|
|
32
|
+
failureCount: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface BatchOperation {
|
|
36
|
+
uri: string;
|
|
37
|
+
type: 'resize' | 'crop' | 'filter' | 'compress' | 'convert';
|
|
38
|
+
params: any;
|
|
39
|
+
options?: any;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class ImageBatchService {
|
|
43
|
+
private static async processBatchItem(
|
|
44
|
+
operation: BatchOperation,
|
|
45
|
+
options: BatchProcessingOptions = {}
|
|
46
|
+
): Promise<{ uri: string; result: ImageManipulationResult | null; error?: Error }> {
|
|
47
|
+
try {
|
|
48
|
+
const uriValidation = ImageValidator.validateUri(operation.uri);
|
|
49
|
+
if (!uriValidation.isValid) {
|
|
50
|
+
throw ImageErrorHandler.createError(uriValidation.error!, IMAGE_ERROR_CODES.INVALID_URI, 'batchProcess');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let result: ImageManipulationResult;
|
|
54
|
+
|
|
55
|
+
switch (operation.type) {
|
|
56
|
+
case 'resize':
|
|
57
|
+
result = await ImageTransformService.resize(
|
|
58
|
+
operation.uri,
|
|
59
|
+
operation.params.width,
|
|
60
|
+
operation.params.height,
|
|
61
|
+
operation.options
|
|
62
|
+
);
|
|
63
|
+
break;
|
|
64
|
+
|
|
65
|
+
case 'crop':
|
|
66
|
+
result = await ImageTransformService.crop(
|
|
67
|
+
operation.uri,
|
|
68
|
+
operation.params,
|
|
69
|
+
operation.options
|
|
70
|
+
);
|
|
71
|
+
break;
|
|
72
|
+
|
|
73
|
+
case 'filter':
|
|
74
|
+
result = await ImageFilterService.applyFilter(
|
|
75
|
+
operation.uri,
|
|
76
|
+
operation.params
|
|
77
|
+
);
|
|
78
|
+
break;
|
|
79
|
+
|
|
80
|
+
case 'compress':
|
|
81
|
+
result = await ImageConversionService.compress(
|
|
82
|
+
operation.uri,
|
|
83
|
+
operation.params.quality
|
|
84
|
+
);
|
|
85
|
+
break;
|
|
86
|
+
|
|
87
|
+
case 'convert':
|
|
88
|
+
result = await ImageConversionService.convertFormat(
|
|
89
|
+
operation.uri,
|
|
90
|
+
operation.params.format,
|
|
91
|
+
operation.params.quality
|
|
92
|
+
);
|
|
93
|
+
break;
|
|
94
|
+
|
|
95
|
+
default:
|
|
96
|
+
throw ImageErrorHandler.createError(
|
|
97
|
+
`Unknown operation type: ${operation.type}`,
|
|
98
|
+
IMAGE_ERROR_CODES.VALIDATION_ERROR,
|
|
99
|
+
'batchProcess'
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return { uri: operation.uri, result };
|
|
104
|
+
} catch (error) {
|
|
105
|
+
return {
|
|
106
|
+
uri: operation.uri,
|
|
107
|
+
result: null,
|
|
108
|
+
error: error instanceof Error ? error : new Error('Unknown error')
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
static async processBatch(
|
|
114
|
+
operations: BatchOperation[],
|
|
115
|
+
options: BatchProcessingOptions = {}
|
|
116
|
+
): Promise<BatchProcessingResult> {
|
|
117
|
+
const concurrency = options.concurrency || 3;
|
|
118
|
+
const successful: Array<{ uri: string; result: ImageManipulationResult }> = [];
|
|
119
|
+
const failed: Array<{ uri: string; error: Error }> = [];
|
|
120
|
+
|
|
121
|
+
let completed = 0;
|
|
122
|
+
const total = operations.length;
|
|
123
|
+
|
|
124
|
+
// Process operations in chunks based on concurrency
|
|
125
|
+
for (let i = 0; i < operations.length; i += concurrency) {
|
|
126
|
+
const chunk = operations.slice(i, i + concurrency);
|
|
127
|
+
|
|
128
|
+
const chunkResults = await Promise.all(
|
|
129
|
+
chunk.map(operation => this.processBatchItem(operation, options))
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
// Process results
|
|
133
|
+
for (const result of chunkResults) {
|
|
134
|
+
completed++;
|
|
135
|
+
|
|
136
|
+
options.onProgress?.(completed, total, result.uri);
|
|
137
|
+
|
|
138
|
+
if (result.error) {
|
|
139
|
+
failed.push({ uri: result.uri, error: result.error });
|
|
140
|
+
options.onError?.(result.error, result.uri);
|
|
141
|
+
} else if (result.result) {
|
|
142
|
+
successful.push({ uri: result.uri, result: result.result });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
successful,
|
|
149
|
+
failed,
|
|
150
|
+
totalProcessed: total,
|
|
151
|
+
successCount: successful.length,
|
|
152
|
+
failureCount: failed.length,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
static async resizeBatch(
|
|
157
|
+
uris: string[],
|
|
158
|
+
width?: number,
|
|
159
|
+
height?: number,
|
|
160
|
+
options: BatchProcessingOptions & { saveOptions?: any } = {}
|
|
161
|
+
): Promise<BatchProcessingResult> {
|
|
162
|
+
const operations: BatchOperation[] = uris.map(uri => ({
|
|
163
|
+
uri,
|
|
164
|
+
type: 'resize' as const,
|
|
165
|
+
params: { width, height },
|
|
166
|
+
options: options.saveOptions,
|
|
167
|
+
}));
|
|
168
|
+
|
|
169
|
+
return this.processBatch(operations, options);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
static async compressBatch(
|
|
173
|
+
uris: string[],
|
|
174
|
+
quality: number = 0.8,
|
|
175
|
+
options: BatchProcessingOptions = {}
|
|
176
|
+
): Promise<BatchProcessingResult> {
|
|
177
|
+
const operations: BatchOperation[] = uris.map(uri => ({
|
|
178
|
+
uri,
|
|
179
|
+
type: 'compress' as const,
|
|
180
|
+
params: { quality },
|
|
181
|
+
}));
|
|
182
|
+
|
|
183
|
+
return this.processBatch(operations, options);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
static async filterBatch(
|
|
187
|
+
uris: string[],
|
|
188
|
+
filter: ImageFilter,
|
|
189
|
+
options: BatchProcessingOptions = {}
|
|
190
|
+
): Promise<BatchProcessingResult> {
|
|
191
|
+
const operations: BatchOperation[] = uris.map(uri => ({
|
|
192
|
+
uri,
|
|
193
|
+
type: 'filter' as const,
|
|
194
|
+
params: filter,
|
|
195
|
+
}));
|
|
196
|
+
|
|
197
|
+
return this.processBatch(operations, options);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Infrastructure - Advanced Editor Service
|
|
3
|
+
*
|
|
4
|
+
* Core editing functionality with history management
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { EditorTool, type EditorState, type EditorLayer, type EditorHistory, type EditorOptions } from '../../domain/entities/EditorTypes';
|
|
8
|
+
import type { ImageManipulationResult } from '../../domain/entities/ImageTypes';
|
|
9
|
+
import { ImageValidator } from '../utils/ImageValidator';
|
|
10
|
+
import { ImageErrorHandler, IMAGE_ERROR_CODES } from '../utils/ImageErrorHandler';
|
|
11
|
+
|
|
12
|
+
export class ImageEditorService {
|
|
13
|
+
private static generateId(): string {
|
|
14
|
+
return Math.random().toString(36).substr(2, 9);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
static createInitialState(
|
|
18
|
+
uri: string,
|
|
19
|
+
dimensions: { width: number; height: number },
|
|
20
|
+
options: EditorOptions = {}
|
|
21
|
+
): EditorState {
|
|
22
|
+
const defaultLayer: EditorLayer = {
|
|
23
|
+
id: 'background',
|
|
24
|
+
name: 'Background',
|
|
25
|
+
visible: true,
|
|
26
|
+
opacity: 1,
|
|
27
|
+
locked: true,
|
|
28
|
+
elements: [],
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
originalUri: uri,
|
|
33
|
+
tool: EditorTool.MOVE,
|
|
34
|
+
layers: [defaultLayer],
|
|
35
|
+
history: [{
|
|
36
|
+
id: ImageEditorService.generateId(),
|
|
37
|
+
timestamp: new Date(),
|
|
38
|
+
layers: [defaultLayer],
|
|
39
|
+
}],
|
|
40
|
+
historyIndex: 0,
|
|
41
|
+
isDirty: false,
|
|
42
|
+
dimensions,
|
|
43
|
+
zoom: 1,
|
|
44
|
+
pan: { x: 0, y: 0 },
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
static addLayer(
|
|
49
|
+
state: EditorState,
|
|
50
|
+
name?: string
|
|
51
|
+
): EditorState {
|
|
52
|
+
const newLayer: EditorLayer = {
|
|
53
|
+
id: ImageEditorService.generateId(),
|
|
54
|
+
name: name || `Layer ${state.layers.length}`,
|
|
55
|
+
visible: true,
|
|
56
|
+
opacity: 1,
|
|
57
|
+
locked: false,
|
|
58
|
+
elements: [],
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const newHistory: EditorHistory = {
|
|
62
|
+
id: ImageEditorService.generateId(),
|
|
63
|
+
timestamp: new Date(),
|
|
64
|
+
layers: [...state.layers, newLayer],
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const newHistoryState = ImageEditorService.addToHistory(state, newHistory);
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
...newHistoryState,
|
|
71
|
+
layers: [...state.layers, newLayer],
|
|
72
|
+
selectedLayer: newLayer.id,
|
|
73
|
+
isDirty: true,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
static removeLayer(
|
|
78
|
+
state: EditorState,
|
|
79
|
+
layerId: string
|
|
80
|
+
): EditorState {
|
|
81
|
+
if (state.layers.length <= 1) {
|
|
82
|
+
throw ImageErrorHandler.createError(
|
|
83
|
+
'Cannot remove the last layer',
|
|
84
|
+
IMAGE_ERROR_CODES.VALIDATION_ERROR,
|
|
85
|
+
'removeLayer'
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const newLayers = state.layers.filter(layer => layer.id !== layerId);
|
|
90
|
+
const newHistory: EditorHistory = {
|
|
91
|
+
id: ImageEditorService.generateId(),
|
|
92
|
+
timestamp: new Date(),
|
|
93
|
+
layers: newLayers,
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const newHistoryState = ImageEditorService.addToHistory(state, newHistory);
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
...newHistoryState,
|
|
100
|
+
layers: newLayers,
|
|
101
|
+
selectedLayer: newLayers[0].id,
|
|
102
|
+
isDirty: true,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
static updateLayer(
|
|
107
|
+
state: EditorState,
|
|
108
|
+
layerId: string,
|
|
109
|
+
updates: Partial<EditorLayer>
|
|
110
|
+
): EditorState {
|
|
111
|
+
const newLayers = state.layers.map(layer =>
|
|
112
|
+
layer.id === layerId ? { ...layer, ...updates } : layer
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const newHistory: EditorHistory = {
|
|
116
|
+
id: ImageEditorService.generateId(),
|
|
117
|
+
timestamp: new Date(),
|
|
118
|
+
layers: newLayers,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const newHistoryState = ImageEditorService.addToHistory(state, newHistory);
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
...newHistoryState,
|
|
125
|
+
layers: newLayers,
|
|
126
|
+
isDirty: true,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
static addElementToLayer(
|
|
131
|
+
state: EditorState,
|
|
132
|
+
layerId: string,
|
|
133
|
+
element: any
|
|
134
|
+
): EditorState {
|
|
135
|
+
const targetLayer = state.layers.find(layer => layer.id === layerId);
|
|
136
|
+
if (!targetLayer) {
|
|
137
|
+
throw ImageErrorHandler.createError(
|
|
138
|
+
'Layer not found',
|
|
139
|
+
IMAGE_ERROR_CODES.VALIDATION_ERROR,
|
|
140
|
+
'addElementToLayer'
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (targetLayer.locked) {
|
|
145
|
+
throw ImageErrorHandler.createError(
|
|
146
|
+
'Cannot add element to locked layer',
|
|
147
|
+
IMAGE_ERROR_CODES.VALIDATION_ERROR,
|
|
148
|
+
'addElementToLayer'
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const newLayers = state.layers.map(layer =>
|
|
153
|
+
layer.id === layerId
|
|
154
|
+
? { ...layer, elements: [...layer.elements, element] }
|
|
155
|
+
: layer
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
const newHistory: EditorHistory = {
|
|
159
|
+
id: ImageEditorService.generateId(),
|
|
160
|
+
timestamp: new Date(),
|
|
161
|
+
layers: newLayers,
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const newHistoryState = ImageEditorService.addToHistory(state, newHistory);
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
...newHistoryState,
|
|
168
|
+
layers: newLayers,
|
|
169
|
+
isDirty: true,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
static undo(state: EditorState): EditorState {
|
|
174
|
+
if (state.historyIndex <= 0) {
|
|
175
|
+
return state;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const newIndex = state.historyIndex - 1;
|
|
179
|
+
const historyState = state.history[newIndex];
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
...state,
|
|
183
|
+
layers: historyState.layers,
|
|
184
|
+
historyIndex: newIndex,
|
|
185
|
+
isDirty: true,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
static redo(state: EditorState): EditorState {
|
|
190
|
+
if (state.historyIndex >= state.history.length - 1) {
|
|
191
|
+
return state;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const newIndex = state.historyIndex + 1;
|
|
195
|
+
const historyState = state.history[newIndex];
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
...state,
|
|
199
|
+
layers: historyState.layers,
|
|
200
|
+
historyIndex: newIndex,
|
|
201
|
+
isDirty: true,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private static addToHistory(
|
|
206
|
+
state: EditorState,
|
|
207
|
+
newHistory: EditorHistory,
|
|
208
|
+
maxHistory: number = 50
|
|
209
|
+
): EditorState {
|
|
210
|
+
const newHistoryArray = [...state.history.slice(0, state.historyIndex + 1), newHistory];
|
|
211
|
+
|
|
212
|
+
// Keep only the last maxHistory states
|
|
213
|
+
if (newHistoryArray.length > maxHistory) {
|
|
214
|
+
newHistoryArray.shift();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
...state,
|
|
219
|
+
history: newHistoryArray,
|
|
220
|
+
historyIndex: newHistoryArray.length - 1,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
static setTool(state: EditorState, tool: EditorTool): EditorState {
|
|
225
|
+
return { ...state, tool };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
static setSelectedLayer(state: EditorState, layerId?: string): EditorState {
|
|
229
|
+
return { ...state, selectedLayer: layerId };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
static setZoom(state: EditorState, zoom: number): EditorState {
|
|
233
|
+
return { ...state, zoom: Math.max(0.1, Math.min(5, zoom)) };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
static setPan(state: EditorState, pan: { x: number; y: number }): EditorState {
|
|
237
|
+
return { ...state, pan };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
static canUndo(state: EditorState): boolean {
|
|
241
|
+
return state.historyIndex > 0;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
static canRedo(state: EditorState): boolean {
|
|
245
|
+
return state.historyIndex < state.history.length - 1;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
static getVisibleLayers(state: EditorState): EditorLayer[] {
|
|
249
|
+
return state.layers.filter(layer => layer.visible);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
static getActiveLayers(state: EditorState): EditorLayer[] {
|
|
253
|
+
return state.layers.filter(layer => layer.visible && !layer.locked);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
static exportState(state: EditorState): EditorState {
|
|
257
|
+
return {
|
|
258
|
+
...state,
|
|
259
|
+
currentUri: undefined,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
static importState(data: any, uri: string): EditorState {
|
|
264
|
+
try {
|
|
265
|
+
return {
|
|
266
|
+
...data,
|
|
267
|
+
originalUri: uri,
|
|
268
|
+
currentUri: undefined,
|
|
269
|
+
};
|
|
270
|
+
} catch (error) {
|
|
271
|
+
throw ImageErrorHandler.handleUnknownError(error, 'importState');
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|