refactor: ImageConversionException

This commit is contained in:
Koji Wakamiya 2025-12-12 14:09:05 +09:00
parent 75aef49477
commit 2de2d34c48
No known key found for this signature in database
6 changed files with 97 additions and 29 deletions

View File

@ -10,6 +10,7 @@ import 'package:platform_image_converter/src/output_format.dart';
import 'package:platform_image_converter/src/output_resize.dart'; import 'package:platform_image_converter/src/output_resize.dart';
import 'package:platform_image_converter/src/web/shared.dart'; import 'package:platform_image_converter/src/web/shared.dart';
export 'src/image_conversion_exception.dart';
export 'src/output_format.dart'; export 'src/output_format.dart';
export 'src/output_resize.dart'; export 'src/output_resize.dart';
@ -42,8 +43,9 @@ class ImageConverter {
/// ///
/// **Throws:** /// **Throws:**
/// - [UnsupportedError]: If the platform or output format is not supported. /// - [UnsupportedError]: If the platform or output format is not supported.
/// - [Exception]: If the image decoding or encoding fails. /// - [ImageDecodingException]: If the input image data cannot be decoded.
/// /// - [ImageEncodingException]: If the image cannot be encoded to the target format.
/// - [ImageConversionException]: For other general errors during the conversion process. ///
/// **Example - Convert HEIC to JPEG:** /// **Example - Convert HEIC to JPEG:**
/// ```dart /// ```dart
/// final jpegData = await ImageConverter.convert( /// final jpegData = await ImageConverter.convert(

View File

@ -2,6 +2,7 @@ import 'dart:typed_data';
import 'package:jni/jni.dart'; import 'package:jni/jni.dart';
import 'package:platform_image_converter/src/android/bindings.g.dart'; import 'package:platform_image_converter/src/android/bindings.g.dart';
import 'package:platform_image_converter/src/image_conversion_exception.dart';
import 'package:platform_image_converter/src/image_converter_platform_interface.dart'; import 'package:platform_image_converter/src/image_converter_platform_interface.dart';
import 'package:platform_image_converter/src/output_format.dart'; import 'package:platform_image_converter/src/output_format.dart';
import 'package:platform_image_converter/src/output_resize.dart'; import 'package:platform_image_converter/src/output_resize.dart';
@ -54,7 +55,7 @@ final class ImageConverterAndroid implements ImageConverterPlatform {
inputData.length, inputData.length,
); );
if (originalBitmap == null) { if (originalBitmap == null) {
throw Exception('Failed to decode image. Invalid image data.'); throw const ImageDecodingException('Invalid image data.');
} }
final originalWidth = originalBitmap.getWidth(); final originalWidth = originalBitmap.getWidth();
@ -78,7 +79,9 @@ final class ImageConverterAndroid implements ImageConverterPlatform {
if (bitmapToCompress == null) { if (bitmapToCompress == null) {
// This should not happen if originalBitmap is valid // This should not happen if originalBitmap is valid
throw Exception('Bitmap could not be prepared for compression.'); throw const ImageConversionException(
'Bitmap could not be prepared for compression.',
);
} }
compressFormat = switch (format) { compressFormat = switch (format) {
@ -98,12 +101,15 @@ final class ImageConverterAndroid implements ImageConverterPlatform {
outputStream, outputStream,
); );
if (!success) { if (!success) {
throw Exception('Failed to compress bitmap.'); throw ImageEncodingException(format, 'Failed to compress bitmap.');
} }
outputJBytes = outputStream.toByteArray(); outputJBytes = outputStream.toByteArray();
if (outputJBytes == null) { if (outputJBytes == null) {
throw Exception('Failed to get byte array from output stream.'); throw ImageEncodingException(
format,
'Failed to get byte array from output stream.',
);
} }
return Uint8List.fromList(outputJBytes.toList()); return Uint8List.fromList(outputJBytes.toList());

View File

@ -4,6 +4,7 @@ import 'dart:typed_data';
import 'package:ffi/ffi.dart'; import 'package:ffi/ffi.dart';
import 'package:objective_c/objective_c.dart'; import 'package:objective_c/objective_c.dart';
import 'package:platform_image_converter/src/darwin/bindings.g.dart'; import 'package:platform_image_converter/src/darwin/bindings.g.dart';
import 'package:platform_image_converter/src/image_conversion_exception.dart';
import 'package:platform_image_converter/src/image_converter_platform_interface.dart'; import 'package:platform_image_converter/src/image_converter_platform_interface.dart';
import 'package:platform_image_converter/src/output_format.dart'; import 'package:platform_image_converter/src/output_format.dart';
import 'package:platform_image_converter/src/output_resize.dart'; import 'package:platform_image_converter/src/output_resize.dart';
@ -60,17 +61,19 @@ final class ImageConverterDarwin implements ImageConverterPlatform {
inputData.length, inputData.length,
); );
if (cfData == nullptr) { if (cfData == nullptr) {
throw Exception('Failed to create CFData from input data.'); throw const ImageConversionException(
'Failed to create CFData from input data.',
);
} }
imageSource = CGImageSourceCreateWithData(cfData, nullptr); imageSource = CGImageSourceCreateWithData(cfData, nullptr);
if (imageSource == nullptr) { if (imageSource == nullptr) {
throw Exception('Failed to create CGImageSource. Invalid image data.'); throw const ImageDecodingException('Invalid image data.');
} }
originalImage = CGImageSourceCreateImageAtIndex(imageSource, 0, nullptr); originalImage = CGImageSourceCreateImageAtIndex(imageSource, 0, nullptr);
if (originalImage == nullptr) { if (originalImage == nullptr) {
throw Exception('Failed to decode image.'); throw const ImageDecodingException();
} }
final originalWidth = CGImageGetWidth(originalImage); final originalWidth = CGImageGetWidth(originalImage);
@ -87,12 +90,14 @@ final class ImageConverterDarwin implements ImageConverterPlatform {
} }
if (imageToEncode == nullptr) { if (imageToEncode == nullptr) {
throw Exception('Failed to prepare image for encoding.'); throw const ImageConversionException(
'Failed to prepare image for encoding. Resizing may have failed.',
);
} }
outputData = CFDataCreateMutable(kCFAllocatorDefault, 0); outputData = CFDataCreateMutable(kCFAllocatorDefault, 0);
if (outputData == nullptr) { if (outputData == nullptr) {
throw Exception('Failed to create output CFData.'); throw ImageEncodingException(format, 'Failed to create output CFData.');
} }
final utiStr = switch (format) { final utiStr = switch (format) {
@ -120,7 +125,10 @@ final class ImageConverterDarwin implements ImageConverterPlatform {
nullptr, nullptr,
); );
if (destination == nullptr) { if (destination == nullptr) {
throw Exception('Failed to create CGImageDestination.'); throw ImageEncodingException(
format,
'Failed to create CGImageDestination.',
);
} }
properties = _createPropertiesForFormat(format, quality); properties = _createPropertiesForFormat(format, quality);
@ -132,13 +140,19 @@ final class ImageConverterDarwin implements ImageConverterPlatform {
final success = CGImageDestinationFinalize(destination); final success = CGImageDestinationFinalize(destination);
if (!success) { if (!success) {
throw Exception('Failed to finalize image encoding.'); throw ImageEncodingException(
format,
'Failed to finalize image encoding.',
);
} }
final length = CFDataGetLength(outputData); final length = CFDataGetLength(outputData);
final bytePtr = CFDataGetBytePtr(outputData); final bytePtr = CFDataGetBytePtr(outputData);
if (bytePtr == nullptr) { if (bytePtr == nullptr) {
throw Exception('Failed to get output data bytes.'); throw ImageEncodingException(
format,
'Failed to get output data bytes.',
);
} }
return Uint8List.fromList(bytePtr.cast<Uint8>().asTypedList(length)); return Uint8List.fromList(bytePtr.cast<Uint8>().asTypedList(length));
@ -193,7 +207,9 @@ final class ImageConverterDarwin implements ImageConverterPlatform {
try { try {
final colorSpace = CGImageGetColorSpace(originalImage); final colorSpace = CGImageGetColorSpace(originalImage);
if (colorSpace == nullptr) { if (colorSpace == nullptr) {
throw Exception('Failed to get color space from image.'); throw const ImageConversionException(
'Failed to get color space from image for resizing.',
);
} }
final bitsPerComponent = CGImageGetBitsPerComponent(originalImage); final bitsPerComponent = CGImageGetBitsPerComponent(originalImage);
@ -209,7 +225,9 @@ final class ImageConverterDarwin implements ImageConverterPlatform {
bitmapInfo, bitmapInfo,
); );
if (context == nullptr) { if (context == nullptr) {
throw Exception('Failed to create bitmap context for resizing.'); throw const ImageConversionException(
'Failed to create bitmap context for resizing.',
);
} }
CGContextSetInterpolationQuality( CGContextSetInterpolationQuality(
@ -227,7 +245,9 @@ final class ImageConverterDarwin implements ImageConverterPlatform {
final resizedImage = CGBitmapContextCreateImage(context); final resizedImage = CGBitmapContextCreateImage(context);
if (resizedImage == nullptr) { if (resizedImage == nullptr) {
throw Exception('Failed to create resized image from context.'); throw const ImageConversionException(
'Failed to create resized image from context.',
);
} }
return resizedImage; return resizedImage;
} finally { } finally {

View File

@ -0,0 +1,30 @@
import 'package:platform_image_converter/src/output_format.dart';
/// Base exception for image conversion errors.
class ImageConversionException implements Exception {
/// Creates an [ImageConversionException] with the given [message].
const ImageConversionException(this.message);
/// Error message describing the exception.
final String message;
@override
String toString() => 'ImageConversionException: $message';
}
/// Thrown when the input image data cannot be decoded.
class ImageDecodingException extends ImageConversionException {
/// Creates an [ImageDecodingException] with the given [message].
const ImageDecodingException([
super.message = 'Failed to decode image data.',
]);
}
/// Thrown when the image cannot be encoded to the target format.
class ImageEncodingException extends ImageConversionException {
/// Creates an [ImageEncodingException] with the given [message].
ImageEncodingException(this.format, [String? message])
: super(message ?? 'Failed to encode image to ${format.name}');
final OutputFormat format;
}

View File

@ -1,7 +1,7 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'output_format.dart'; import 'package:platform_image_converter/src/output_format.dart';
import 'output_resize.dart'; import 'package:platform_image_converter/src/output_resize.dart';
/// Platform-specific image converter interface. /// Platform-specific image converter interface.
/// ///
@ -27,7 +27,9 @@ abstract interface class ImageConverterPlatform {
/// **Throws:** /// **Throws:**
/// - [UnimplementedError]: If not implemented by platform subclass /// - [UnimplementedError]: If not implemented by platform subclass
/// - [UnsupportedError]: If format is not supported /// - [UnsupportedError]: If format is not supported
/// - [Exception]: If conversion fails /// - [ImageDecodingException]: If the input image data cannot be decoded.
/// - [ImageEncodingException]: If the image cannot be encoded to the target format.
/// - [ImageConversionException]: For other general errors during the conversion process.
Future<Uint8List> convert({ Future<Uint8List> convert({
required Uint8List inputData, required Uint8List inputData,
OutputFormat format = OutputFormat.jpeg, OutputFormat format = OutputFormat.jpeg,

View File

@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:js_interop'; import 'dart:js_interop';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:platform_image_converter/src/image_conversion_exception.dart';
import 'package:platform_image_converter/src/image_converter_platform_interface.dart'; import 'package:platform_image_converter/src/image_converter_platform_interface.dart';
import 'package:platform_image_converter/src/output_format.dart'; import 'package:platform_image_converter/src/output_format.dart';
import 'package:platform_image_converter/src/output_resize.dart'; import 'package:platform_image_converter/src/output_resize.dart';
@ -45,17 +46,24 @@ final class ImageConverterWeb implements ImageConverterPlatform {
}) async { }) async {
final img = HTMLImageElement(); final img = HTMLImageElement();
final decodeCompleter = Completer<void>(); final decodeCompleter = Completer<void>();
final blob = Blob([inputData.toJS].toJS); final blob = Blob([inputData.toJS].toJS);
final url = URL.createObjectURL(blob); final url = URL.createObjectURL(blob);
img.onLoad.listen((_) => decodeCompleter.complete()); img
img.onError.listen((e) { ..onLoad.listen((_) {
URL.revokeObjectURL(url); decodeCompleter.complete();
decodeCompleter.completeError('Failed to load image: $e'); })
..onError.listen((event) {
decodeCompleter.completeError(
const ImageDecodingException('Failed to load image from data.'),
);
}); });
img.src = url; img.src = url;
try {
await decodeCompleter.future; await decodeCompleter.future;
} finally {
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
}
final canvas = HTMLCanvasElement(); final canvas = HTMLCanvasElement();
@ -86,7 +94,7 @@ final class ImageConverterWeb implements ImageConverterPlatform {
encodeCompleter.complete(blob); encodeCompleter.complete(blob);
} else { } else {
encodeCompleter.completeError( encodeCompleter.completeError(
'Failed to convert canvas to JPEG Blob.', ImageEncodingException(format, 'Canvas toBlob returned null.'),
); );
} }
}.toJS, }.toJS,