| 133 | * @returns A new PNG File, or null if the rect is empty or cropping failed. |
| 134 | */ |
| 135 | export const cropImageFile = async ( |
| 136 | file: File, |
| 137 | rect: CropRect, |
| 138 | ): Promise<File | null> => { |
| 139 | if (rect.width < 1 || rect.height < 1) { |
| 140 | return null; |
| 141 | } |
| 142 | |
| 143 | const url = URL.createObjectURL(file); |
| 144 | |
| 145 | try { |
| 146 | const image = new Image(); |
| 147 | await new Promise<void>((resolve, reject) => { |
| 148 | image.onload = () => resolve(); |
| 149 | image.onerror = () => reject(new Error('Image load failed')); |
| 150 | image.src = url; |
| 151 | }); |
| 152 | |
| 153 | const canvas = document.createElement('canvas'); |
| 154 | canvas.width = Math.round(rect.width); |
| 155 | canvas.height = Math.round(rect.height); |
| 156 | |
| 157 | const ctx = canvas.getContext('2d'); |
| 158 | if (!ctx) { |
| 159 | return null; |
| 160 | } |
| 161 | |
| 162 | ctx.drawImage( |
| 163 | image, |
| 164 | rect.x, |
| 165 | rect.y, |
| 166 | rect.width, |
| 167 | rect.height, |
| 168 | 0, |
| 169 | 0, |
| 170 | canvas.width, |
| 171 | canvas.height, |
| 172 | ); |
| 173 | |
| 174 | const blob = await new Promise<Blob | null>((resolve) => { |
| 175 | canvas.toBlob(resolve, 'image/png'); |
| 176 | }); |
| 177 | |
| 178 | if (!blob) { |
| 179 | return null; |
| 180 | } |
| 181 | |
| 182 | return new File([blob], `screenshot-${Date.now()}.png`, { |
| 183 | type: 'image/png', |
| 184 | }); |
| 185 | } finally { |
| 186 | URL.revokeObjectURL(url); |
| 187 | } |
| 188 | }; |
| 189 | |
| 190 | /** |
| 191 | * Create an object URL for a File object to display a preview. |