From 1b68ec5d68d37bf769142317dcdcdb5624c339c9 Mon Sep 17 00:00:00 2001 From: Dirk Lemstra Date: Thu, 1 Aug 2024 22:20:04 +0200 Subject: [PATCH] Added extra overload to the compare method to get access to the compare image. --- src/index.ts | 1 + src/internal/pointer/double-pointer.ts | 27 ++++++++ src/magick-image.ts | 68 +++++++++++++++++-- src/types/compare-result.ts | 28 ++++++++ tests/magick-image/compare.spec.ts | 90 ++++++++++++++++++++++++++ 5 files changed, 210 insertions(+), 4 deletions(-) create mode 100644 src/internal/pointer/double-pointer.ts create mode 100644 src/types/compare-result.ts diff --git a/src/index.ts b/src/index.ts index c2b2dff6..777efeb1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -93,6 +93,7 @@ export * from './settings/quantize-settings'; export * from './statistics/channel-statistics'; export * from './statistics/statistics'; export * from './types/chromaticity-info'; +export * from './types/compare-result'; export * from './types/connected-component'; export * from './types/density'; export * from './types/magick-geometry'; diff --git a/src/internal/pointer/double-pointer.ts b/src/internal/pointer/double-pointer.ts new file mode 100644 index 00000000..33198c2e --- /dev/null +++ b/src/internal/pointer/double-pointer.ts @@ -0,0 +1,27 @@ +// Copyright Dirk Lemstra https://github.com/dlemstra/magick-wasm. +// Licensed under the Apache License, Version 2.0. + +import { ImageMagick } from '../../image-magick'; + +/** @internal */ +export class DoublePointer { + private readonly instance: number; + + private constructor() { + this.instance = ImageMagick._api._malloc(8); + ImageMagick._api.setValue(this.instance, 0, 'double'); + } + + get ptr(): number { return this.instance; } + + get value(): number { return ImageMagick._api.getValue(this.instance, 'double'); } + + static use(func: (pointer: DoublePointer) => TReturnType): TReturnType { + const pointer = new DoublePointer(); + try { + return func(pointer); + } finally { + ImageMagick._api._free(pointer.instance); + } + } +} diff --git a/src/magick-image.ts b/src/magick-image.ts index 9f5fb827..22112437 100644 --- a/src/magick-image.ts +++ b/src/magick-image.ts @@ -9,6 +9,7 @@ import { ChromaticityInfo } from './types/chromaticity-info'; import { ClassType } from './enums/class-type'; import { ColorSpace } from './enums/color-space'; import { ColorType } from './enums/color-type'; +import { CompareResult } from './types/compare-result'; import { CompositeOperator } from './enums/composite-operator'; import { CompressionMethod } from './enums/compression-method'; import { ConnectedComponent } from './types/connected-component'; @@ -20,6 +21,7 @@ import { DisposableArray } from './internal/disposable-array'; import { DistortMethod } from './enums/distort-method'; import { DistortSettings } from './settings/distort-settings'; import { DrawingWand } from './drawing/drawing-wand'; +import { DoublePointer } from './internal/pointer/double-pointer'; import { Endian } from './enums/endian'; import { ErrorMetric } from './enums/error-metric'; import { EvaluateOperator } from './enums/evaluate-operator'; @@ -503,6 +505,20 @@ export interface IMagickImage extends IDisposable { */ compare(image: IMagickImage, metric: ErrorMetric): number; + /** + * Returns the distortion based on the specified metric. + * @param image - The other image to compare with this image. + * @param metric - The metric to use. + */ + compare(image: IMagickImage, metric: ErrorMetric, func: (compareResult: CompareResult) => TReturnType): TReturnType; + + /** + * Returns the distortion based on the specified metric. + * @param image - The other image to compare with this image. + * @param metric - The metric to use. + */ + compare(image: IMagickImage, metric: ErrorMetric, func: (compareResult: CompareResult) => Promise): Promise; + /** * Returns the distortion based on the specified metric. * @param image - The other image to compare with this image. @@ -511,6 +527,20 @@ export interface IMagickImage extends IDisposable { */ compare(image: IMagickImage, metric: ErrorMetric, channels: Channels): number; + /** + * Returns the distortion based on the specified metric. + * @param image - The other image to compare with this image. + * @param metric - The metric to use. + */ + compare(image: IMagickImage, metric: ErrorMetric, channels: Channels, func: (compareResult: CompareResult) => TReturnType): TReturnType; + + /** + * Returns the distortion based on the specified metric. + * @param image - The other image to compare with this image. + * @param metric - The metric to use. + */ + compare(image: IMagickImage, metric: ErrorMetric, channels: Channels, func: (compareResult: CompareResult) => Promise): Promise; + /** * Compose an image onto another at specified offset using the 'In' operator. * @param image - The image to composite with this image. @@ -2109,10 +2139,40 @@ export class MagickImage extends NativeInstance implements IMagickImage { compare(image: IMagickImage, metric: ErrorMetric): number; compare(image: IMagickImage, metric: ErrorMetric, channels: Channels): number; - compare(image: IMagickImage, metric: ErrorMetric, channelsOrUndefined?: Channels): number { - return this.useExceptionPointer(exception => { - const channels = this.valueOrDefault(channelsOrUndefined, Channels.Undefined); - return ImageMagick._api._MagickImage_CompareDistortion(this._instance, image._instance, metric, channels, exception); + compare(image: IMagickImage, metric: ErrorMetric, func: (image: CompareResult) => TReturnType): TReturnType; + compare(image: IMagickImage, metric: ErrorMetric, func: (image: CompareResult) => Promise): Promise; + compare(image: IMagickImage, metric: ErrorMetric, channels: Channels, func: (image: CompareResult) => TReturnType): TReturnType; + compare(image: IMagickImage, metric: ErrorMetric, channels: Channels, func: (image: CompareResult) => Promise): Promise; + compare(image: IMagickImage, metric: ErrorMetric, channelsFuncOrUndefined?: Channels | ((image: CompareResult) => TReturnType | Promise), funcOrUndefined?: (image: CompareResult) => TReturnType | Promise): number | TReturnType | Promise { + let func = channelsFuncOrUndefined; + if (funcOrUndefined !== undefined) + func = funcOrUndefined; + + let channels = Channels.Undefined; + if (typeof func !== 'function') { + if (func !== undefined) + channels = func; + + return this.useExceptionPointer(exception => { + return ImageMagick._api._MagickImage_CompareDistortion(this._instance, image._instance, metric, channels, exception); + }); + } + + if (channelsFuncOrUndefined !== undefined && typeof channelsFuncOrUndefined !== 'function') + channels = channelsFuncOrUndefined; + + const compareResult = DoublePointer.use((pointer) => { + const instance = this.useExceptionPointer(exception => { + return ImageMagick._api._MagickImage_Compare(this._instance, image._instance, metric, channels, pointer.ptr, exception); + }); + + const distortion = pointer.value; + const difference = MagickImage._createFromImage(instance, this._settings); + return CompareResult._create(distortion, difference); + }); + + return compareResult.difference._use(() => { + return func(compareResult); }); } diff --git a/src/types/compare-result.ts b/src/types/compare-result.ts new file mode 100644 index 00000000..18259380 --- /dev/null +++ b/src/types/compare-result.ts @@ -0,0 +1,28 @@ +// Copyright Dirk Lemstra https://github.com/dlemstra/magick-wasm. +// Licensed under the Apache License, Version 2.0. + +import { IMagickImage } from '../magick-image'; + +/** + * Compare result. + */ +export class CompareResult { + private constructor(distortion: number, difference: IMagickImage) { + this.distortion = distortion; + this.difference = difference; + } + + /** + * Gets the difference image. + */ + readonly difference; + /** + * Gets the distortion. + */ + readonly distortion; + + /** @internal */ + static _create(distortion: number, difference: IMagickImage): CompareResult { + return new CompareResult(distortion, difference); + } +} diff --git a/tests/magick-image/compare.spec.ts b/tests/magick-image/compare.spec.ts index e93bac9f..765c060e 100644 --- a/tests/magick-image/compare.spec.ts +++ b/tests/magick-image/compare.spec.ts @@ -3,8 +3,10 @@ import { Channels } from '@src/enums/channels'; import { ErrorMetric } from '@src/enums/error-metric'; +import { MagickColor } from '@src/magick-color'; import { MagickColors } from '@src/magick-colors'; import { TestImages } from '@test/test-images'; +import { bogusAsyncMethod } from '@test/bogus-async'; describe('MagickImage#compare', () => { it('should return 0 for same image', () => { @@ -24,6 +26,49 @@ describe('MagickImage#compare', () => { }); }); + it('should call function with compare result', () => { + TestImages.empty.use(image => { + TestImages.empty.use(other => { + image.read(MagickColors.Red, 1, 1); + other.read(MagickColors.RosyBrown, 1, 1); + const result = image.compare(other, ErrorMetric.RootMeanSquared, compareResult => { + expect(compareResult.difference).not.toBeNull(); + expect(compareResult.difference.width).toBeCloseTo(1); + expect(compareResult.difference.height).toBeCloseTo(1); + expect(compareResult.difference).toHavePixelWithColor(0, 0, new MagickColor('#f40018')); + + return compareResult; + }); + + expect(() => { result.difference._instance }).toThrowError('instance is disposed'); + expect(result.distortion).toBeCloseTo(0.48); + }); + }); + }); + + it('should call function with compare result async', async () => { + await TestImages.empty.use(async image => { + await TestImages.empty.use(async other => { + image.read(MagickColors.Red, 1, 1); + other.read(MagickColors.RosyBrown, 1, 1); + const result = await image.compare(other, ErrorMetric.RootMeanSquared, async compareResult => { + expect(compareResult.difference).not.toBeNull(); + expect(compareResult.difference.width).toBeCloseTo(1); + expect(compareResult.difference.height).toBeCloseTo(1); + + await bogusAsyncMethod(); + + expect(compareResult.difference).toHavePixelWithColor(0, 0, new MagickColor('#f40018')); + + return compareResult; + }); + + expect(() => { result.difference._instance }).toThrowError('instance is disposed'); + expect(result.distortion).toBeCloseTo(0.48); + }); + }); + }); + it('should compare the specified channels', () => { TestImages.empty.use(image => { TestImages.empty.use(other => { @@ -33,4 +78,49 @@ describe('MagickImage#compare', () => { }); }); }); + + it('should compare the specified channels and call function with compare result', () => { + TestImages.empty.use(image => { + TestImages.empty.use(other => { + image.read(MagickColors.Red, 1, 1); + other.read(MagickColors.RosyBrown, 1, 1); + + const result = image.compare(other, ErrorMetric.RootMeanSquared, Channels.Red, compareResult => { + expect(compareResult.difference).not.toBeNull(); + expect(compareResult.difference.width).toBeCloseTo(1); + expect(compareResult.difference.height).toBeCloseTo(1); + expect(compareResult.difference).toHavePixelWithColor(0, 0, new MagickColor('#f40018')); + + return compareResult; + }); + + expect(() => { result.difference._instance }).toThrowError('instance is disposed'); + expect(result.distortion).toBeCloseTo(0.15); + }); + }); + }); + + it('should compare the specified channels and call function with compare result async', async () => { + await TestImages.empty.use(async image => { + await TestImages.empty.use(async other => { + image.read(MagickColors.Red, 1, 1); + other.read(MagickColors.RosyBrown, 1, 1); + + const result = await image.compare(other, ErrorMetric.RootMeanSquared, Channels.Red, async compareResult => { + expect(compareResult.difference).not.toBeNull(); + expect(compareResult.difference.width).toBeCloseTo(1); + expect(compareResult.difference.height).toBeCloseTo(1); + + await bogusAsyncMethod(); + + expect(compareResult.difference).toHavePixelWithColor(0, 0, new MagickColor('#f40018')); + + return compareResult; + }); + + expect(() => { result.difference._instance }).toThrowError('instance is disposed'); + expect(result.distortion).toBeCloseTo(0.15); + }); + }); + }); });