diff --git a/.env b/.env index 8b4a6fa..bdcce1a 100644 --- a/.env +++ b/.env @@ -1,3 +1,7 @@ +# Whether to support WASM filters + +VITE_ENABLE_WASM_FILTERS=false + # --------------------------------- # Third party storage configuration # --------------------------------- diff --git a/.gitignore b/.gitignore index 7e2a946..6b4b504 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,8 @@ node_modules # local env files .env.local .env.*.local +.env.production +.env.*.production # Log files npm-debug.log* @@ -21,3 +23,4 @@ pnpm-debug.log* *.njsproj *.sln *.sw? +build.sh \ No newline at end of file diff --git a/README.md b/README.md index ccb16e5..e7523fb 100644 --- a/README.md +++ b/README.md @@ -163,10 +163,12 @@ Once the container is started, you can access BitMappery at `http://localhost:51 ## WebAssembly -BitMappery can also use WebAssembly to increase performance of image manipulation. The source -code is C based and compiled to WASM using [Emscripten](https://github.com/emscripten-core/emscripten). Because this setup is a little more cumbersome, the repository contains precompiled binaries -in the _./src/wasm/bin/_-folder meaning you can omit this setup if you don't intend to make changes -to these sources. +BitMappery can also use WebAssembly to _potentially_ increase performance of image manipulation. +WebAssembly filtering is a user controllable feature in the preferences pane, as long as the `.env` file has set +support for `VITE_ENABLE_WASM_FILTERS` to true. + +The source code is C based and compiled to WASM using [Emscripten](https://github.com/emscripten-core/emscripten). Because this setup is a little more cumbersome, the repository contains precompiled binaries in the _./src/wasm/bin/_-folder meaning you can +omit this setup if you don't intend to make changes to these sources. If you do wish to make contributions on this end, to compile the source (_/src/wasm/_) C-code to WASM, you will first need to prepare your environment (note the last _source_ call does not permanently update your paths): @@ -187,7 +189,7 @@ npm run wasm ### Benchmarks -On a particular (low powered) configuration, running all filters on a particular source takes: +On a particular (deliberately low powered) configuration, running all filters at their heaviest setting on a particular source takes: * 7000+ ms in JavaScript * 558 ms in WebAssembly @@ -195,7 +197,8 @@ On a particular (low powered) configuration, running all filters on a particular * 603 ms in WebAssembly inside a Web Worker Note that the WebAssembly Web Worker takes a performance hit from converting the ImageData buffer -to float32 prior to allocating the buffer in the WASM instance's memory. This can benefit from +to float32 prior to allocating the buffer in the WASM instance's memory. This could benefit from further tweaking to see if it gets closer to the JavaScript Web Worker performance. -WebAssembly filtering is a user controllable feature in the preferences pane. +However, as in the current setup the JS solution alone is performant enough _and you would need to write the +filter code twice_, the default for WASM is disabled. diff --git a/src/components/layer-filters/layer-filters.vue b/src/components/layer-filters/layer-filters.vue index fb30a15..7c0a34d 100644 --- a/src/components/layer-filters/layer-filters.vue +++ b/src/components/layer-filters/layer-filters.vue @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Igor Zinken 2021-2023 - https://www.igorski.nl + * Igor Zinken 2021-2024 - https://www.igorski.nl * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in @@ -84,6 +84,15 @@ :tooltip="'none'" /> +
+ + +
= { // curried, so not reactive ! // @ts-expect-error using string as key getPreference: ( state: PreferencesState ): ( n: string ) => string => ( name: string ): boolean => state.preferences[ name ], + supportWASM: (): boolean => SUPPORT_WASM, }, mutations: { setPreferences( state: PreferencesState, preferences: Preferences ): void { @@ -74,7 +78,7 @@ const PreferencesModule: Module = { if ( typeof preferences.antiAlias === "boolean" ) { commit( "setAntiAlias", preferences.antiAlias ); } - setWasmFilters( !!preferences.wasmFilters ); + setWasmFilters( SUPPORT_WASM && !!preferences.wasmFilters ); } catch { // non-blocking } diff --git a/src/wasm/filters.cpp b/src/wasm/filters.cpp index 6c28f69..2ea59df 100644 --- a/src/wasm/filters.cpp +++ b/src/wasm/filters.cpp @@ -81,8 +81,22 @@ inline void vibrance( float vibrance, float& r, float& g, float& b ) { } } +inline void threshold( float threshold, float& r, float& g, float& b ) { + float luma = r * 0.3 + g * 0.59 + b * 0.11; + + luma = luma < threshold ? 0 : 255; + + r = luma; + g = luma; + b = luma; +} + extern "C" { - void filter( float* pixels, int length, float vGamma, float vBrightness, float vContrast, float vVibrance, bool doGamma, bool doDesaturate, bool doBrightness, bool doContrast, bool doVibrance ) { + void filter( + float* pixels, int length, + float vGamma, float vBrightness, float vContrast, float vVibrance, /*float vThreshold,*/ + bool doGamma, bool doDesaturate, bool doBrightness, bool doContrast, bool doVibrance/*, bool doThreshold*/ + ) { float r, g, b, a; float gammaSquared = vGamma * vGamma; @@ -90,6 +104,7 @@ extern "C" { r = pixels[ i ]; g = pixels[ i + 1 ]; b = pixels[ i + 2 ]; + a = pixels[ i + 3 ]; if ( doGamma ) gamma( gammaSquared, r, g, b ); @@ -106,6 +121,9 @@ extern "C" { if ( doVibrance ) vibrance( vVibrance, r, g, b ); + // if ( doThreshold && a > 0 ) + // threshold( vThreshold, r, g, b ); + pixels[ i ] = r; pixels[ i + 1 ] = g; pixels[ i + 2 ] = b; diff --git a/src/workers/filter.worker.ts b/src/workers/filter.worker.ts index 2a24328..437d0c8 100644 --- a/src/workers/filter.worker.ts +++ b/src/workers/filter.worker.ts @@ -84,10 +84,12 @@ function renderFilters( imageData: ImageData, filters: any ): Uint8ClampedArray const contrast = Math.pow((( filters.contrast * 100 ) + 100 ) / 100, 2 ); // -100 to 100 range const gamma = ( filters.gamma * 2 ); // 0 to 2 range const vibrance = -(( filters.vibrance * 200 ) - 100 ); // -100 to 100 range - const { desaturate } = filters; // boolean + + const { desaturate, threshold } = filters; const pixels = imageData.data; - let r, g, b;//, a; + + let r, g, b, a; let grayScale, max, avg, amt; const gammaSquared = gamma * gamma; @@ -95,6 +97,7 @@ function renderFilters( imageData: ImageData, filters: any ): Uint8ClampedArray const doContrast = filters.contrast !== defaultFilters.contrast; const doGamma = filters.gamma !== defaultFilters.gamma; const doVibrance = filters.vibrance !== defaultFilters.vibrance; + const doThreshold = filters.threshold !== defaultFilters.threshold; // loop through the pixels, note we increment the iterator by four // as each pixel is defined by four channel values : red, green, blue and the alpha channel @@ -105,7 +108,7 @@ function renderFilters( imageData: ImageData, filters: any ): Uint8ClampedArray r = pixels[ i ]; g = pixels[ i + 1 ]; b = pixels[ i + 2 ]; - //a = pixels[ i + 3 ]; // currently no filter uses alpha channel + a = pixels[ i + 3 ]; // 1. adjust gamma if ( doGamma ) { @@ -153,6 +156,16 @@ function renderFilters( imageData: ImageData, filters: any ): Uint8ClampedArray } } + // 6. adjust threshold + + if ( doThreshold && a > 0 ) { + let luma = r * 0.3 + g * 0.59 + b * 0.11; + + luma = luma < threshold ? 0 : 255; + + r = g = b = luma; + } + // commit the changes pixels[ i ] = r; pixels[ i + 1 ] = g; @@ -169,20 +182,21 @@ function renderFiltersWasm( imageData: ImageData, filters: any ): Uint8ClampedAr const contrast = Math.pow((( filters.contrast * 100 ) + 100 ) / 100, 2 ); // -100 to 100 range const gamma = ( filters.gamma * 2 ); // 0 to 2 range const vibrance = -(( filters.vibrance * 200 ) - 100 ); // -100 to 100 range - const { desaturate } = filters; // boolean + const { desaturate, threshold } = filters; const doBrightness = filters.brightness !== defaultFilters.brightness; const doContrast = filters.contrast !== defaultFilters.contrast; const doGamma = filters.gamma !== defaultFilters.gamma; const doVibrance = filters.vibrance !== defaultFilters.vibrance; + const doThreshold = filters.threshold !== defaultFilters.threshold; // run WASM operations return imageDataAsFloat( imageData, wasmInstance, ( memory, length ) => { wasmInstance._filter( memory, length, - gamma, brightness, contrast, vibrance, - doGamma, desaturate, doBrightness, doContrast, doVibrance + gamma, brightness, contrast, vibrance,/* threshold,*/ + doGamma, desaturate, doBrightness, doContrast, doVibrance/*, doThreshold*/ ); }); } diff --git a/tests/unit/factories/filters-factory.spec.ts b/tests/unit/factories/filters-factory.spec.ts index 021c4e4..cfff1c6 100644 --- a/tests/unit/factories/filters-factory.spec.ts +++ b/tests/unit/factories/filters-factory.spec.ts @@ -14,6 +14,7 @@ describe( "Filters factory", () => { brightness: .5, contrast: 0, vibrance: .5, + threshold: -1, desaturate: false, }); }); @@ -27,6 +28,7 @@ describe( "Filters factory", () => { brightness: .6, contrast: .3, vibrance: .2, + threshold: 127, desaturate: true, }); expect( filters ).toEqual({ @@ -37,6 +39,7 @@ describe( "Filters factory", () => { brightness: .6, contrast: .3, vibrance: .2, + threshold: 127, desaturate: true, }); }); @@ -52,6 +55,7 @@ describe( "Filters factory", () => { brightness: .6, contrast: .3, vibrance: .2, + threshold: 255, desaturate: true, }); const serialized = FiltersFactory.serialize( filters ); @@ -86,6 +90,9 @@ describe( "Filters factory", () => { filter = FiltersFactory.create({ vibrance: .4 }); expect( hasFilters( filter )).toBe( true ); + filter = FiltersFactory.create({ threshold: 0 }); + expect( hasFilters( filter )).toBe( true ); + filter = FiltersFactory.create({ desaturate: true }); expect( hasFilters( filter )).toBe( true ); }); @@ -93,7 +100,7 @@ describe( "Filters factory", () => { it( "should know when two filters instances are equal", () => { const defaultFilter = FiltersFactory.create(); - [ "enabled", "blendMode", "opacity", "gamma", "brightness", "contrast", "vibrance", "desaturate" ].forEach( property => { + [ "enabled", "blendMode", "opacity", "gamma", "brightness", "contrast", "vibrance", "threshold", "desaturate" ].forEach( property => { const filters = FiltersFactory.create({ [ property ]: .88 }); expect( isEqual( filters, defaultFilter )).toBe( false ); });