feat: server support for filters

This commit is contained in:
bwees
2026-01-25 15:26:55 -06:00
parent 8653e20cc5
commit 2e4cfa80a9
13 changed files with 613 additions and 12 deletions

View File

@@ -358,6 +358,7 @@ Class | Method | HTTP request | Description
- [AssetDeltaSyncResponseDto](doc//AssetDeltaSyncResponseDto.md)
- [AssetEditAction](doc//AssetEditAction.md)
- [AssetEditActionCrop](doc//AssetEditActionCrop.md)
- [AssetEditActionFilter](doc//AssetEditActionFilter.md)
- [AssetEditActionListDto](doc//AssetEditActionListDto.md)
- [AssetEditActionListDtoEditsInner](doc//AssetEditActionListDtoEditsInner.md)
- [AssetEditActionMirror](doc//AssetEditActionMirror.md)
@@ -427,6 +428,7 @@ Class | Method | HTTP request | Description
- [ExifResponseDto](doc//ExifResponseDto.md)
- [FaceDto](doc//FaceDto.md)
- [FacialRecognitionConfig](doc//FacialRecognitionConfig.md)
- [FilterParameters](doc//FilterParameters.md)
- [FoldersResponse](doc//FoldersResponse.md)
- [FoldersUpdate](doc//FoldersUpdate.md)
- [ImageFormat](doc//ImageFormat.md)

View File

@@ -98,6 +98,7 @@ part 'model/asset_delta_sync_dto.dart';
part 'model/asset_delta_sync_response_dto.dart';
part 'model/asset_edit_action.dart';
part 'model/asset_edit_action_crop.dart';
part 'model/asset_edit_action_filter.dart';
part 'model/asset_edit_action_list_dto.dart';
part 'model/asset_edit_action_list_dto_edits_inner.dart';
part 'model/asset_edit_action_mirror.dart';
@@ -167,6 +168,7 @@ part 'model/email_notifications_update.dart';
part 'model/exif_response_dto.dart';
part 'model/face_dto.dart';
part 'model/facial_recognition_config.dart';
part 'model/filter_parameters.dart';
part 'model/folders_response.dart';
part 'model/folders_update.dart';
part 'model/image_format.dart';

View File

@@ -242,6 +242,8 @@ class ApiClient {
return AssetEditActionTypeTransformer().decode(value);
case 'AssetEditActionCrop':
return AssetEditActionCrop.fromJson(value);
case 'AssetEditActionFilter':
return AssetEditActionFilter.fromJson(value);
case 'AssetEditActionListDto':
return AssetEditActionListDto.fromJson(value);
case 'AssetEditActionListDtoEditsInner':
@@ -380,6 +382,8 @@ class ApiClient {
return FaceDto.fromJson(value);
case 'FacialRecognitionConfig':
return FacialRecognitionConfig.fromJson(value);
case 'FilterParameters':
return FilterParameters.fromJson(value);
case 'FoldersResponse':
return FoldersResponse.fromJson(value);
case 'FoldersUpdate':

View File

@@ -26,12 +26,14 @@ class AssetEditAction {
static const crop = AssetEditAction._(r'crop');
static const rotate = AssetEditAction._(r'rotate');
static const mirror = AssetEditAction._(r'mirror');
static const filter = AssetEditAction._(r'filter');
/// List of all possible values in this [enum][AssetEditAction].
static const values = <AssetEditAction>[
crop,
rotate,
mirror,
filter,
];
static AssetEditAction? fromJson(dynamic value) => AssetEditActionTypeTransformer().decode(value);
@@ -73,6 +75,7 @@ class AssetEditActionTypeTransformer {
case r'crop': return AssetEditAction.crop;
case r'rotate': return AssetEditAction.rotate;
case r'mirror': return AssetEditAction.mirror;
case r'filter': return AssetEditAction.filter;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');

View File

@@ -0,0 +1,107 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class AssetEditActionFilter {
/// Returns a new [AssetEditActionFilter] instance.
AssetEditActionFilter({
required this.action,
required this.parameters,
});
AssetEditAction action;
FilterParameters parameters;
@override
bool operator ==(Object other) => identical(this, other) || other is AssetEditActionFilter &&
other.action == action &&
other.parameters == parameters;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(action.hashCode) +
(parameters.hashCode);
@override
String toString() => 'AssetEditActionFilter[action=$action, parameters=$parameters]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'action'] = this.action;
json[r'parameters'] = this.parameters;
return json;
}
/// Returns a new [AssetEditActionFilter] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static AssetEditActionFilter? fromJson(dynamic value) {
upgradeDto(value, "AssetEditActionFilter");
if (value is Map) {
final json = value.cast<String, dynamic>();
return AssetEditActionFilter(
action: AssetEditAction.fromJson(json[r'action'])!,
parameters: FilterParameters.fromJson(json[r'parameters'])!,
);
}
return null;
}
static List<AssetEditActionFilter> listFromJson(dynamic json, {bool growable = false,}) {
final result = <AssetEditActionFilter>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = AssetEditActionFilter.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, AssetEditActionFilter> mapFromJson(dynamic json) {
final map = <String, AssetEditActionFilter>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = AssetEditActionFilter.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of AssetEditActionFilter-objects as value to a dart map
static Map<String, List<AssetEditActionFilter>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<AssetEditActionFilter>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = AssetEditActionFilter.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'action',
'parameters',
};
}

View File

@@ -0,0 +1,208 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class FilterParameters {
/// Returns a new [FilterParameters] instance.
FilterParameters({
required this.bOffset,
required this.bbBias,
required this.bgBias,
required this.brBias,
required this.gOffset,
required this.gbBias,
required this.ggBias,
required this.grBias,
required this.rOffset,
required this.rbBias,
required this.rgBias,
required this.rrBias,
});
/// B Offset (-255 -> 255)
///
/// Minimum value: -255
/// Maximum value: 255
num bOffset;
/// BB Bias
num bbBias;
/// BG Bias
num bgBias;
/// BR Bias
num brBias;
/// G Offset (-255 -> 255)
///
/// Minimum value: -255
/// Maximum value: 255
num gOffset;
/// GB Bias
num gbBias;
/// GG Bias
num ggBias;
/// GR Bias
num grBias;
/// R Offset (-255 -> 255)
///
/// Minimum value: -255
/// Maximum value: 255
num rOffset;
/// RB Bias
num rbBias;
/// RG Bias
num rgBias;
/// RR Bias
num rrBias;
@override
bool operator ==(Object other) => identical(this, other) || other is FilterParameters &&
other.bOffset == bOffset &&
other.bbBias == bbBias &&
other.bgBias == bgBias &&
other.brBias == brBias &&
other.gOffset == gOffset &&
other.gbBias == gbBias &&
other.ggBias == ggBias &&
other.grBias == grBias &&
other.rOffset == rOffset &&
other.rbBias == rbBias &&
other.rgBias == rgBias &&
other.rrBias == rrBias;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(bOffset.hashCode) +
(bbBias.hashCode) +
(bgBias.hashCode) +
(brBias.hashCode) +
(gOffset.hashCode) +
(gbBias.hashCode) +
(ggBias.hashCode) +
(grBias.hashCode) +
(rOffset.hashCode) +
(rbBias.hashCode) +
(rgBias.hashCode) +
(rrBias.hashCode);
@override
String toString() => 'FilterParameters[bOffset=$bOffset, bbBias=$bbBias, bgBias=$bgBias, brBias=$brBias, gOffset=$gOffset, gbBias=$gbBias, ggBias=$ggBias, grBias=$grBias, rOffset=$rOffset, rbBias=$rbBias, rgBias=$rgBias, rrBias=$rrBias]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'bOffset'] = this.bOffset;
json[r'bbBias'] = this.bbBias;
json[r'bgBias'] = this.bgBias;
json[r'brBias'] = this.brBias;
json[r'gOffset'] = this.gOffset;
json[r'gbBias'] = this.gbBias;
json[r'ggBias'] = this.ggBias;
json[r'grBias'] = this.grBias;
json[r'rOffset'] = this.rOffset;
json[r'rbBias'] = this.rbBias;
json[r'rgBias'] = this.rgBias;
json[r'rrBias'] = this.rrBias;
return json;
}
/// Returns a new [FilterParameters] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static FilterParameters? fromJson(dynamic value) {
upgradeDto(value, "FilterParameters");
if (value is Map) {
final json = value.cast<String, dynamic>();
return FilterParameters(
bOffset: num.parse('${json[r'bOffset']}'),
bbBias: num.parse('${json[r'bbBias']}'),
bgBias: num.parse('${json[r'bgBias']}'),
brBias: num.parse('${json[r'brBias']}'),
gOffset: num.parse('${json[r'gOffset']}'),
gbBias: num.parse('${json[r'gbBias']}'),
ggBias: num.parse('${json[r'ggBias']}'),
grBias: num.parse('${json[r'grBias']}'),
rOffset: num.parse('${json[r'rOffset']}'),
rbBias: num.parse('${json[r'rbBias']}'),
rgBias: num.parse('${json[r'rgBias']}'),
rrBias: num.parse('${json[r'rrBias']}'),
);
}
return null;
}
static List<FilterParameters> listFromJson(dynamic json, {bool growable = false,}) {
final result = <FilterParameters>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = FilterParameters.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, FilterParameters> mapFromJson(dynamic json) {
final map = <String, FilterParameters>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = FilterParameters.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of FilterParameters-objects as value to a dart map
static Map<String, List<FilterParameters>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<FilterParameters>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = FilterParameters.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'bOffset',
'bbBias',
'bgBias',
'brBias',
'gOffset',
'gbBias',
'ggBias',
'grBias',
'rOffset',
'rbBias',
'rgBias',
'rrBias',
};
}

View File

@@ -15797,7 +15797,8 @@
"enum": [
"crop",
"rotate",
"mirror"
"mirror",
"filter"
],
"type": "string"
},
@@ -15820,6 +15821,25 @@
],
"type": "object"
},
"AssetEditActionFilter": {
"properties": {
"action": {
"allOf": [
{
"$ref": "#/components/schemas/AssetEditAction"
}
]
},
"parameters": {
"$ref": "#/components/schemas/FilterParameters"
}
},
"required": [
"action",
"parameters"
],
"type": "object"
},
"AssetEditActionListDto": {
"properties": {
"edits": {
@@ -15834,6 +15854,9 @@
},
{
"$ref": "#/components/schemas/AssetEditActionMirror"
},
{
"$ref": "#/components/schemas/AssetEditActionFilter"
}
]
},
@@ -15902,6 +15925,9 @@
},
{
"$ref": "#/components/schemas/AssetEditActionMirror"
},
{
"$ref": "#/components/schemas/AssetEditActionFilter"
}
]
},
@@ -17545,6 +17571,79 @@
],
"type": "object"
},
"FilterParameters": {
"properties": {
"bOffset": {
"description": "B Offset (-255 -> 255)",
"maximum": 255,
"minimum": -255,
"type": "number"
},
"bbBias": {
"description": "BB Bias",
"type": "number"
},
"bgBias": {
"description": "BG Bias",
"type": "number"
},
"brBias": {
"description": "BR Bias",
"type": "number"
},
"gOffset": {
"description": "G Offset (-255 -> 255)",
"maximum": 255,
"minimum": -255,
"type": "number"
},
"gbBias": {
"description": "GB Bias",
"type": "number"
},
"ggBias": {
"description": "GG Bias",
"type": "number"
},
"grBias": {
"description": "GR Bias",
"type": "number"
},
"rOffset": {
"description": "R Offset (-255 -> 255)",
"maximum": 255,
"minimum": -255,
"type": "number"
},
"rbBias": {
"description": "RB Bias",
"type": "number"
},
"rgBias": {
"description": "RG Bias",
"type": "number"
},
"rrBias": {
"description": "RR Bias",
"type": "number"
}
},
"required": [
"bOffset",
"bbBias",
"bgBias",
"brBias",
"gOffset",
"gbBias",
"ggBias",
"grBias",
"rOffset",
"rbBias",
"rgBias",
"rrBias"
],
"type": "object"
},
"FoldersResponse": {
"properties": {
"enabled": {

View File

@@ -4,7 +4,7 @@
AssetEditAction action;
- MirrorParameters parameters;
- FilterParameters parameters;
+ Map<String, dynamic> parameters;
@override
@@ -13,7 +13,7 @@
return AssetEditActionListDtoEditsInner(
action: AssetEditAction.fromJson(json[r'action'])!,
- parameters: MirrorParameters.fromJson(json[r'parameters'])!,
- parameters: FilterParameters.fromJson(json[r'parameters'])!,
+ parameters: json[r'parameters'],
);
}

View File

@@ -637,14 +637,44 @@ export type AssetEditActionMirror = {
action: AssetEditAction;
parameters: MirrorParameters;
};
export type FilterParameters = {
/** B Offset (-255 -> 255) */
bOffset: number;
/** BB Bias */
bbBias: number;
/** BG Bias */
bgBias: number;
/** BR Bias */
brBias: number;
/** G Offset (-255 -> 255) */
gOffset: number;
/** GB Bias */
gbBias: number;
/** GG Bias */
ggBias: number;
/** GR Bias */
grBias: number;
/** R Offset (-255 -> 255) */
rOffset: number;
/** RB Bias */
rbBias: number;
/** RG Bias */
rgBias: number;
/** RR Bias */
rrBias: number;
};
export type AssetEditActionFilter = {
action: AssetEditAction;
parameters: FilterParameters;
};
export type AssetEditsDto = {
assetId: string;
/** list of edits */
edits: (AssetEditActionCrop | AssetEditActionRotate | AssetEditActionMirror)[];
edits: (AssetEditActionCrop | AssetEditActionRotate | AssetEditActionMirror | AssetEditActionFilter)[];
};
export type AssetEditActionListDto = {
/** list of edits */
edits: (AssetEditActionCrop | AssetEditActionRotate | AssetEditActionMirror)[];
edits: (AssetEditActionCrop | AssetEditActionRotate | AssetEditActionMirror | AssetEditActionFilter)[];
};
export type AssetMetadataResponseDto = {
key: string;
@@ -5870,7 +5900,8 @@ export enum AssetJobName {
export enum AssetEditAction {
Crop = "crop",
Rotate = "rotate",
Mirror = "mirror"
Mirror = "mirror",
Filter = "filter"
}
export enum MirrorAxis {
Horizontal = "horizontal",

View File

@@ -1,12 +1,13 @@
import { ApiExtraModels, ApiProperty, getSchemaPath } from '@nestjs/swagger';
import { ClassConstructor, plainToInstance, Transform, Type } from 'class-transformer';
import { ArrayMinSize, IsEnum, IsInt, Min, ValidateNested } from 'class-validator';
import { ArrayMinSize, IsEnum, IsInt, IsNumber, Max, Min, ValidateNested } from 'class-validator';
import { IsAxisAlignedRotation, IsUniqueEditActions, ValidateUUID } from 'src/validation';
export enum AssetEditAction {
Crop = 'crop',
Rotate = 'rotate',
Mirror = 'mirror',
Filter = 'filter',
}
export enum MirrorAxis {
@@ -48,6 +49,68 @@ export class MirrorParameters {
axis!: MirrorAxis;
}
// Sharp supports a 3x3 matrix for color manipulation and rgb offsets
// The matrix representation of a filter is as follows:
// | rrBias rgBias rbBias | | r_offset |
// Image x | grBias ggBias gbBias | + | g_offset |
// | brBias bgBias bbBias | | b_offset |
export class FilterParameters {
@IsNumber()
@ApiProperty({ description: 'RR Bias' })
rrBias!: number;
@IsNumber()
@ApiProperty({ description: 'RG Bias' })
rgBias!: number;
@IsNumber()
@ApiProperty({ description: 'RB Bias' })
rbBias!: number;
@IsNumber()
@ApiProperty({ description: 'GR Bias' })
grBias!: number;
@IsNumber()
@ApiProperty({ description: 'GG Bias' })
ggBias!: number;
@IsNumber()
@ApiProperty({ description: 'GB Bias' })
gbBias!: number;
@IsNumber()
@ApiProperty({ description: 'BR Bias' })
brBias!: number;
@IsNumber()
@ApiProperty({ description: 'BG Bias' })
bgBias!: number;
@IsNumber()
@ApiProperty({ description: 'BB Bias' })
bbBias!: number;
@IsInt()
@Min(-255)
@Max(255)
@ApiProperty({ description: 'R Offset (-255 -> 255)' })
rOffset!: number;
@IsInt()
@Min(-255)
@Max(255)
@ApiProperty({ description: 'G Offset (-255 -> 255)' })
gOffset!: number;
@IsInt()
@Min(-255)
@Max(255)
@ApiProperty({ description: 'B Offset (-255 -> 255)' })
bOffset!: number;
}
class AssetEditActionBase {
@IsEnum(AssetEditAction)
@ApiProperty({ enum: AssetEditAction, enumName: 'AssetEditAction' })
@@ -74,6 +137,12 @@ export class AssetEditActionMirror extends AssetEditActionBase {
@ApiProperty({ type: MirrorParameters })
parameters!: MirrorParameters;
}
export class AssetEditActionFilter extends AssetEditActionBase {
@ValidateNested()
@Type(() => FilterParameters)
@ApiProperty({ type: FilterParameters })
parameters!: FilterParameters;
}
export type AssetEditActionItem =
| {
@@ -87,25 +156,31 @@ export type AssetEditActionItem =
| {
action: AssetEditAction.Mirror;
parameters: MirrorParameters;
}
| {
action: AssetEditAction.Filter;
parameters: FilterParameters;
};
export type AssetEditActionParameter = {
[AssetEditAction.Crop]: CropParameters;
[AssetEditAction.Rotate]: RotateParameters;
[AssetEditAction.Mirror]: MirrorParameters;
[AssetEditAction.Filter]: FilterParameters;
};
type AssetEditActions = AssetEditActionCrop | AssetEditActionRotate | AssetEditActionMirror;
type AssetEditActions = AssetEditActionCrop | AssetEditActionRotate | AssetEditActionMirror | AssetEditActionFilter;
const actionToClass: Record<AssetEditAction, ClassConstructor<AssetEditActions>> = {
[AssetEditAction.Crop]: AssetEditActionCrop,
[AssetEditAction.Rotate]: AssetEditActionRotate,
[AssetEditAction.Mirror]: AssetEditActionMirror,
[AssetEditAction.Filter]: AssetEditActionFilter,
} as const;
const getActionClass = (item: { action: AssetEditAction }): ClassConstructor<AssetEditActions> =>
actionToClass[item.action];
@ApiExtraModels(AssetEditActionRotate, AssetEditActionMirror, AssetEditActionCrop)
@ApiExtraModels(AssetEditActionRotate, AssetEditActionMirror, AssetEditActionCrop, AssetEditActionFilter)
export class AssetEditActionListDto {
/** list of edits */
@ArrayMinSize(1)

View File

@@ -165,6 +165,38 @@ describe(MediaRepository.name, () => {
// bottom-right should now be top-right (blue)
expect(await getPixelColor(bufferVertical, 990, 990)).toEqual({ r: 0, g: 255, b: 0 });
});
it('should apply filter edit correctly', async () => {
const resultHorizontal = await sut['applyEdits'](sharp(await buildTestQuadImage()), [
{
action: AssetEditAction.Filter,
parameters: {
rrBias: 1,
rgBias: 0.5,
rbBias: 0.5,
grBias: 0.5,
ggBias: 1,
gbBias: 0.5,
brBias: 0.5,
bgBias: 0.5,
bbBias: 1,
rOffset: 5,
gOffset: 10,
bOffset: 15,
},
},
]);
const bufferHorizontal = await resultHorizontal.toBuffer();
const metadataHorizontal = await resultHorizontal.metadata();
expect(metadataHorizontal.width).toBe(1000);
expect(metadataHorizontal.height).toBe(1000);
expect(await getPixelColor(bufferHorizontal, 10, 10)).toEqual({ r: 255, g: 137, b: 142 });
expect(await getPixelColor(bufferHorizontal, 990, 10)).toEqual({ r: 132, g: 255, b: 142 });
expect(await getPixelColor(bufferHorizontal, 10, 990)).toEqual({ r: 132, g: 137, b: 255 });
expect(await getPixelColor(bufferHorizontal, 990, 990)).toEqual({ r: 255, g: 255, b: 255 });
});
});
describe('applyEdits (multiple sequential edits)', () => {
@@ -307,12 +339,29 @@ describe(MediaRepository.name, () => {
expect(await getPixelColor(buffer, 490, 490)).toEqual({ r: 255, g: 0, b: 0 });
});
it('should apply all operations: crop, rotate, mirror', async () => {
it('should apply all operations: crop, rotate, mirror, filter', async () => {
const imageBuffer = await buildTestQuadImage();
const result = await sut['applyEdits'](sharp(imageBuffer), [
{ action: AssetEditAction.Crop, parameters: { x: 0, y: 0, width: 500, height: 1000 } },
{ action: AssetEditAction.Rotate, parameters: { angle: 90 } },
{ action: AssetEditAction.Mirror, parameters: { axis: MirrorAxis.Horizontal } },
{
action: AssetEditAction.Filter,
parameters: {
rrBias: 1,
rgBias: 0,
rbBias: 0,
grBias: 0,
ggBias: 1,
gbBias: 0,
brBias: 0,
bgBias: 0,
bbBias: 1,
rOffset: -10,
gOffset: 20,
bOffset: -30,
},
},
]);
const buffer = await result.png().toBuffer();
@@ -320,8 +369,8 @@ describe(MediaRepository.name, () => {
expect(metadata.width).toBe(1000);
expect(metadata.height).toBe(500);
expect(await getPixelColor(buffer, 10, 10)).toEqual({ r: 255, g: 0, b: 0 });
expect(await getPixelColor(buffer, 990, 10)).toEqual({ r: 0, g: 0, b: 255 });
expect(await getPixelColor(buffer, 10, 10)).toEqual({ r: 245, g: 20, b: 0 });
expect(await getPixelColor(buffer, 990, 10)).toEqual({ r: 0, g: 20, b: 225 });
});
});

View File

@@ -19,6 +19,7 @@ import {
TranscodeCommand,
VideoInfo,
} from 'src/types';
import { convertColorFilterToMatricies } from 'src/utils/color_filter';
import { handlePromiseError } from 'src/utils/misc';
import { createAffineMatrix } from 'src/utils/transform';
@@ -167,6 +168,12 @@ export class MediaRepository {
[c, d],
]);
const filter = edits.find((edit) => edit.action === 'filter');
if (filter) {
const { biasMatrix, offsetMatrix } = convertColorFilterToMatricies(filter.parameters);
pipeline = pipeline.recomb(biasMatrix).linear([1, 1, 1], offsetMatrix);
}
return pipeline;
}

View File

@@ -0,0 +1,14 @@
import { Matrix3x3 } from 'sharp';
import { FilterParameters } from 'src/dtos/editing.dto';
export function convertColorFilterToMatricies(filter: FilterParameters) {
const biasMatrix: Matrix3x3 = [
[filter.rrBias, filter.rgBias, filter.rbBias],
[filter.grBias, filter.ggBias, filter.gbBias],
[filter.brBias, filter.bgBias, filter.bbBias],
];
const offsetMatrix = [filter.rOffset, filter.gOffset, filter.bOffset];
return { biasMatrix, offsetMatrix };
}