diff --git a/packages/core/src/pipes/index.ts b/packages/core/src/pipes/index.ts index 5dd6307..0f37f6e 100644 --- a/packages/core/src/pipes/index.ts +++ b/packages/core/src/pipes/index.ts @@ -10,9 +10,11 @@ import { Pipe } from "@ipp/common"; import { ConvertPipe } from "./convert"; import { PassthroughPipe } from "./passthrough"; import { ResizePipe } from "./resize"; +import { RotatePipe } from "./rotate"; export const PIPES: { [index: string]: Pipe } = { convert: ConvertPipe, passthrough: PassthroughPipe, resize: ResizePipe, + rotate: RotatePipe, } as const; diff --git a/packages/core/src/pipes/rotate.test.ts b/packages/core/src/pipes/rotate.test.ts new file mode 100644 index 0000000..7f0026d --- /dev/null +++ b/packages/core/src/pipes/rotate.test.ts @@ -0,0 +1,71 @@ +/** + * Image Processing Pipeline - Copyright (c) Marcus Cemes + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { DataObject, sampleMetadata } from "@ipp/common"; + +import { randomBytes } from "crypto"; +import sharp, { OutputInfo, Sharp } from "sharp"; + +import { RotateOptions, RotatePipe } from "./rotate"; + +jest.mock("sharp"); + +type UnPromise = T extends Promise ? U : never; + +describe("built-in rotate pipe", () => { + /** The input value */ + const data: DataObject = { + buffer: randomBytes(8), + metadata: sampleMetadata(256, "jpeg"), + }; + + const rotateOptions = { angle: 45 }; + + /** The return value of the mocked sharp.toBuffer() function */ + const toBufferResult: UnPromise> = { + data: data.buffer, + info: { + width: 256, + height: 256, + channels: data.metadata.current.channels, + size: data.buffer.length, + format: data.metadata.current.format, + premultiplied: false, + } as OutputInfo, + }; + + /** The expected value */ + const newData: DataObject = { + ...data, + metadata: { + ...data.metadata, + current: { + ...data.metadata.current, + width: toBufferResult.info.width, + height: toBufferResult.info.height, + }, + }, + }; + + const toBufferMock = jest.fn(async () => toBufferResult); + const rotateMock = jest.fn(() => ({ toBuffer: toBufferMock })); + const sharpMock = sharp as unknown as jest.Mock<{ rotate: typeof rotateMock }>; + const mocks = [toBufferMock, rotateMock, sharpMock]; + + beforeAll(() => sharpMock.mockImplementation(() => ({ rotate: rotateMock }))); + afterAll(() => sharpMock.mockRestore()); + afterEach(() => mocks.forEach((m) => m.mockClear())); + + test("rotate image", async () => { + const result = RotatePipe(data, rotateOptions); + + await expect(result).resolves.toMatchObject(newData); + + expect(rotateMock).toHaveBeenCalledWith(rotateOptions.angle, rotateOptions?.rotateOptions); + expect(toBufferMock).toHaveBeenCalledWith({ resolveWithObject: true }); + }); +}); diff --git a/packages/core/src/pipes/rotate.ts b/packages/core/src/pipes/rotate.ts new file mode 100644 index 0000000..f6fab02 --- /dev/null +++ b/packages/core/src/pipes/rotate.ts @@ -0,0 +1,43 @@ +/** + * Image Processing Pipeline - Copyright (c) Marcus Cemes + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { Pipe } from "@ipp/common"; + +import produce from "immer"; +import sharp, { RotateOptions as SharpOptions } from "sharp"; + +sharp.concurrency(1); + +export interface RotateOptions { + angle?: number; + rotateOptions?: SharpOptions; +} + +/** + * A built-in pipe that lets you rotate an image + * This can be used to rotate the image properly before the EXIF data is removed from CompressPipe + */ +export const RotatePipe: Pipe = async (data, options = {}) => { + const { + data: newBuffer, + info: { width, height, format, channels }, + } = await sharp(data.buffer) + .rotate(options.angle, options.rotateOptions) + .toBuffer({ resolveWithObject: true }); + + const newMetadata = produce(data.metadata, (draft) => { + draft.current.width = width; + draft.current.height = height; + draft.current.channels = channels; + draft.current.format = format; + }); + + return { + buffer: newBuffer, + metadata: newMetadata, + }; +};