From 9fab0e6368e64e7ad0ad06bb826b6052c18c9029 Mon Sep 17 00:00:00 2001 From: Cristobal Contreras Rubio Date: Sun, 3 Nov 2024 11:00:18 +0100 Subject: [PATCH] backup --- package-lock.json | 18 ++++ package.json | 1 + src/ejemplo.js | 22 +++++ src/image-sharp.ts | 161 ++++++++++++++++++++++++++++++++++ src/imageSharp.ts | 101 ---------------------- src/sharp-phash.d.ts | 15 ++++ tests/image-sharp.test.ts | 177 ++++++++++++++++++++++++++++++++++++++ tests/splitea.test.ts | 32 ++++--- 8 files changed, 412 insertions(+), 115 deletions(-) create mode 100644 src/ejemplo.js create mode 100644 src/image-sharp.ts delete mode 100644 src/imageSharp.ts create mode 100644 src/sharp-phash.d.ts create mode 100644 tests/image-sharp.test.ts diff --git a/package-lock.json b/package-lock.json index 2b8a98c..f15d29e 100755 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@schemasjs/validator": "^1.0.1", "jimp": "^0.22.12", "sharp": "^0.33.4", + "sharp-phash": "^2.1.0", "valibot": "^0.36.0", "valid-filename": "^4.0.0" }, @@ -6089,6 +6090,17 @@ "@img/sharp-win32-x64": "0.33.4" } }, + "node_modules/sharp-phash": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/sharp-phash/-/sharp-phash-2.1.0.tgz", + "integrity": "sha512-9JYWr4tiKpjRA5Mi0qHn6LP2evS+GjdRVGjDFOSnO761m5Pavkpm83SyzauO2Ntt7znVqTn7J3XTUwHjRPAonw==", + "engines": { + "node": ">= 10" + }, + "peerDependencies": { + "sharp": ">= 0.25.4" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -12172,6 +12184,12 @@ "semver": "^7.6.0" } }, + "sharp-phash": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/sharp-phash/-/sharp-phash-2.1.0.tgz", + "integrity": "sha512-9JYWr4tiKpjRA5Mi0qHn6LP2evS+GjdRVGjDFOSnO761m5Pavkpm83SyzauO2Ntt7znVqTn7J3XTUwHjRPAonw==", + "requires": {} + }, "shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", diff --git a/package.json b/package.json index e7ec31d..994ba56 100755 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "@schemasjs/validator": "^1.0.1", "jimp": "^0.22.12", "sharp": "^0.33.4", + "sharp-phash": "^2.1.0", "valibot": "^0.36.0", "valid-filename": "^4.0.0" }, diff --git a/src/ejemplo.js b/src/ejemplo.js new file mode 100644 index 0000000..9f70bd0 --- /dev/null +++ b/src/ejemplo.js @@ -0,0 +1,22 @@ +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import sharp from 'sharp' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) + +const IMG_FOLDER = path.join(__dirname, '..', 'tests') +const horizontal = { + path: path.join(IMG_FOLDER, 'chess_horizontal.png'), + width: 720, + height: 90 +} + +const img = sharp(horizontal.path) +const tile = img.clone().extract({ top: 0, left: 180, width: 90, height: 90 }) +const metadata = await tile.metadata() +console.log(`width = ${metadata.width} - height = ${metadata.height}`) +const filename = path.join(__dirname, 'tile.png') +await tile.toFile(filename) + +const saved = await sharp(filename).metadata() +console.log(`width = ${saved.width} - height = ${saved.height}`) diff --git a/src/image-sharp.ts b/src/image-sharp.ts new file mode 100644 index 0000000..26968f0 --- /dev/null +++ b/src/image-sharp.ts @@ -0,0 +1,161 @@ +import * as Path from 'node:path' +import sharp, { type Sharp } from 'sharp' +import phash from 'sharp-phash' +import dist from 'sharp-phash/distance' +import { SpliteaError, ThrowSpliteaError } from './errors' +import type { CropData, ImageSource, Natural, Size, StoreOptions, UniqueImagesOptions, WriteOptions } from './types' + +// @ts-expect-error +export const getImage = async (image: ImageSource): Promise => { + try { + const img = sharp(image) + await getSize(img) + return await Promise.resolve(img) + } catch (error) { + ThrowSpliteaError(error, `Error reading image ${image.toString()}`) + } +} + +export const getSize = async (image: Sharp): Promise => { + const metadata = await image.metadata() + return { width: metadata.width as number, height: metadata.height as number } +} + +const areEqualImages = async (img1: Sharp, img2: Sharp, options: UniqueImagesOptions): Promise => { + const { distance } = options + try { + const [buff1, buff2] = await Promise.all([img1.toBuffer(), img2.toBuffer()]) + const [hash1, hash2] = await Promise.all([phash(buff1), phash(buff2)]) + const d = dist(hash1, hash2) + return d < distance + } catch (error) { + console.log(error) + ThrowSpliteaError(error, 'Error comparing images') + } + return false +} + +const getUniqueTiles = async (images: Sharp[], options: UniqueImagesOptions): Promise => { + if (images.length < 2) return images + let array = [...images] + const uniques: Sharp[] = [] + do { + const image: Sharp = array.shift() as Sharp + uniques.push(image) + array = await Promise.all( + array.filter(async (elem) => { + const equal = await areEqualImages(image, elem, options) + return !equal + }) + ) + } while (array.length > 0) + return uniques +} + +// @ts-expect-error +const getTile = (image: Sharp, { x, y, w, h }: CropData): Sharp => { + try { + return image.clone().extract({ left: x, top: y, width: w, height: h }) + } catch (error) { + console.error(`Error with getTile\n${(error as Error).message}`) + } +} + +export const getHorizontalTiles = (image: Sharp, size: Size, width: Natural): Sharp[] => { + if (size.width === width) return [image] + const tiles = [] + const y = 0 + const w = width + const h = size.height + for (let x = 0; x < size.width; x += width) { + try { + const tile = getTile(image, { x, y, w, h }) + tiles.push(tile) + } catch (error) { + ThrowSpliteaError(error, 'Cannot get Horizontal tiles') + } + } + return tiles +} + +export const getUniqueHorizontalTiles = async (image: Sharp, size: Size, width: Natural, options: UniqueImagesOptions): Promise => { + const tiles = getHorizontalTiles(image, size, width) + return await getUniqueTiles(tiles, options) +} + +export const getVerticalTiles = (image: Sharp, size: Size, height: Natural): Sharp[] => { + if (size.height === height) return [image] + const tiles = [] + const x = 0 + const w = size.width + const h = height + for (let y = 0; y < size.height; y += height) { + try { + tiles.push(getTile(image, { x, y, w, h })) + } catch (error) { + ThrowSpliteaError(error, 'Cannot get Vertical tiles') + } + } + return tiles +} + +export const getUniqueVerticalTiles = async (image: Sharp, size: Size, height: Natural, options: UniqueImagesOptions): Promise => { + const tiles = getVerticalTiles(image, size, height) + return await getUniqueTiles(tiles, options) +} + +export const getGridTiles = (image: Sharp, size: Size, width: Natural, height: Natural): Sharp[] => { + if (size.width === width) return [image] + const tiles = [] + const w = width + const h = height + for (let x = 0; x < size.width; x += width) { + for (let y = 0; y < size.height; y += height) { + try { + tiles.push(getTile(image, { x, y, w, h })) + } catch (error) { + ThrowSpliteaError(error, 'Cannot get Grid tiles') + } + } + } + return tiles +} + +export const getUniqueGridTiles = async (image: Sharp, size: Size, width: Natural, height: Natural, options: UniqueImagesOptions): Promise => { + const tiles = getGridTiles(image, size, width, height) + return await getUniqueTiles(tiles, options) +} + +const writeImage = async (image: Sharp, options: WriteOptions): Promise => { + const { path, filename: name, extension, index, pad } = options + const filename = `${name}_${(index).toString().padStart(pad, '0')}.${extension}` + const file = Path.join(path, filename) + await image.toFile(file) + return file +} + +export const writeImages = async (images: Sharp[], storeOptions: StoreOptions): Promise => { + if (images.length < 1) throw new SpliteaError('Impossible to write no images') + const { path, filename, extension } = storeOptions + const pad = Math.floor(Math.log10(images.length)) + 1 + if (images.length === 1) { + const filenames = await writeImage(images[0], { path, filename, extension, index: '', pad }) + return [filenames] + } + return await Promise.all( + images.map( + async (image: Sharp, index: number) => await writeImage(image, { path, filename, extension, index: index.toString(), pad }) + ) + ) +} + +// @ts-expect-error +export const getBufferImages = async (images: Sharp[]): Promise => { + try { + return await Promise.all( + images.map(async (image: Sharp) => await image.toBuffer()) + ) + } catch (error) { + ThrowSpliteaError(error, 'Impossible to get buffer from images') + } +} diff --git a/src/imageSharp.ts b/src/imageSharp.ts deleted file mode 100644 index 536db77..0000000 --- a/src/imageSharp.ts +++ /dev/null @@ -1,101 +0,0 @@ -// phash with sharp -> https://www.npmjs.com/package/sharp-phash -// phash -> https://www.npmjs.com/search?q=phash - -// import * as Path from 'node:path' -// import Jimp from "jimp" -// import sharp, { Sharp } from 'sharp' -// import { SpliteaError, ThrowSpliteaError } from "./errors" -// import { Image, ImageSchema, Size, SizeSchema, TileCoordinates } from "./types" - -// export const getSize = async (image: Sharp): Promise => { -// const { width, height } = await image.metadata() -// return SizeSchema.parse({ width, height }) -// } - -// export const getImage = async (image: Image): Promise<[Sharp, Size]> => { -// try { -// ImageSchema.parse(image) -// const img = sharp(image) -// const { width, height } = await img.metadata() -// const size: Size = SizeSchema.parse({ width, height }) -// return [img, size] -// } catch (error) { -// throw ThrowSpliteaError(error, `Error reading image ${image}`) -// } -// } - -// const getSplitImage = (image: Sharp, size: Size, tileCoordinates: TileCoordinates): Sharp => { -// try { -// const { width, height } = size -// const { x, y, width: w, height: h } = tileCoordinates -// if (x === 0 && w === width && y === 0 && h === height) return image -// if ((x + w) > width) throw new SpliteaError(`Can't have an image of ${w}x${h}px from (${x}, ${y}) because max x value is ${width - 1}`) -// if ((y + h) > height) throw new SpliteaError(`Can't have an image of ${w}x${h}px from (${x}, ${y}) because max y value is ${height - 1}`) -// return image.clone().extract({ left: x, top: y, width: w, height: h }) -// } catch (error) { -// throw ThrowSpliteaError(error, 'Problem spliting image') -// } -// } - -// export const getSplitImages = (image: Sharp, size: Size, tilesCoordinate: TileCoordinates[], unique: boolean = false): Sharp[] => { -// const images = tilesCoordinate.map(tileCoordinates => getSplitImage(image, size, tileCoordinates)) -// if (unique && images.length > 1) { return getUniqueImages(images) } -// return images -// } - -// export const areEqualImages = (img1: Sharp, img2: Sharp): boolean => { -// try { -// const distance = Jimp.distance(img1, img2) -// const diff = Jimp.diff(img1, img2) -// if (distance < 0.15 && diff.percent < 0.15) { -// console.debug(`distance = ${distance} | diff = ${diff.percent}`) -// return true -// } -// } catch (error) { -// console.log(error) -// ThrowSpliteaError(error, 'Error comparing images') -// } -// return false -// } - -// export const getUniqueImages = (images: Sharp[]): Sharp[] => { -// let array = [...images] -// let image: Sharp -// let uniqueArray: Sharp[] = [] -// while (array.length) { -// image = array[0] -// uniqueArray.push(image) -// array = array.filter(elem => !areEqualImages(image, elem)) -// } -// return uniqueArray -// } - -// const writeImage = async (image: Jimp, path: string, name: string, index: number | string, extension: string): Promise => { -// const filename = `${name}_${index}_${new Date().getTime()}.${extension}` -// const file = Path.join(path, filename) -// await image.writeAsync(file) -// return file -// } - -// export const writeImages = async (images: Jimp[], path: string, name: string, extension: string): Promise => { -// if (images.length < 1) throw new SpliteaError('Impossible to write no images') -// if (images.length === 1) { -// const filenames = await writeImage(images[0], path, name, '', extension) -// return [filenames] -// } -// return Promise.all( -// images.map( -// async (image: Jimp, index: number) => await writeImage(image, path, name, index, extension) -// ) -// ) -// } - -// export const getBufferImages = async (images: Jimp[]): Promise => { -// try { -// const buffers = await Promise.all(images.map(async (image: Jimp) => await image.getBufferAsync(image.getMIME()))) -// return buffers -// } catch (error) { -// ThrowSpliteaError(error, 'Impossible to get buffer from images') -// } -// return Promise.resolve([]) -// } diff --git a/src/sharp-phash.d.ts b/src/sharp-phash.d.ts new file mode 100644 index 0000000..aed1923 --- /dev/null +++ b/src/sharp-phash.d.ts @@ -0,0 +1,15 @@ +declare module 'sharp-phash' { + import type sharp from 'sharp' + + type SharpImage = Parameters[0] + type SharpOptions = Parameters[1] + + export default function phash ( + image: SharpImage, + options?: SharpOptions, + ): Promise +} + +declare module 'sharp-phash/distance' { + export default function distance (a: string, b: string): number +} diff --git a/tests/image-sharp.test.ts b/tests/image-sharp.test.ts new file mode 100644 index 0000000..d8d2b5d --- /dev/null +++ b/tests/image-sharp.test.ts @@ -0,0 +1,177 @@ +import { Buffer } from 'node:buffer' +import fs from 'node:fs' +import path from 'node:path' +import { describe, test, expect } from 'vitest' +import { SpliteaError } from '../src/errors' +import { getBufferImages, getGridTiles, getHorizontalTiles, getImage, getSize, getUniqueGridTiles, getUniqueHorizontalTiles, getUniqueVerticalTiles, getVerticalTiles, writeImages } from '../src/image-sharp' +import { StoreOptions, UniqueImagesOptions } from '../src/types' +import { MAX_DIFFERENCE, MAX_DISTANCE } from '../src/constants' +import sharp from 'sharp' + +const IMG_FOLDER = path.join(__dirname) + +const forest = { + path: path.join(IMG_FOLDER, 'forestmap.png'), + width: 320, + height: 224, + bad: path.join(IMG_FOLDER, 'forestmapp.png'), +} + +const bad = { + path: path.join(IMG_FOLDER, 'forestmapp.png'), + width: 320, + height: 224, +} + +const satie = { + path: path.join(IMG_FOLDER, 'Ericsatie.jpg'), + width: 2651, + height: 3711, +} + +const chess = { + path: path.join(IMG_FOLDER, 'chess.png'), + width: 720, + height: 720, +} + +const horizontal = { + path: path.join(IMG_FOLDER, 'chess_horizontal.png'), + width: 720, + height: 90, +} + +const vertical = { + path: path.join(IMG_FOLDER, 'chess_vertical.png'), + width: 90, + height: 720, +} + +describe.skip.concurrent('Test getImage + getSize', () => { + test('Correct local jpg', async () => { + Promise.all([forest, satie, chess].map(async(img) => { + const image = await getImage(img.path) + const size = await getSize(image) + expect(size.width).toBe(img.width) + expect(size.height).toBe(img.height) + })) + }) + + test('Incorrect local jpg', async () => { + await expect(getImage(bad.path)).rejects.toThrowError() + }) +}) + +test.skip.concurrent('getHorizontalTiles', async () => { + const img = await getImage(horizontal.path) + const size = await getSize(img) + const width = size.width / 8 + const tiles = getHorizontalTiles(img, size, width) + expect(tiles).toHaveLength(8) + await Promise.all(tiles.map(async (tile, idx) => { + const filename = path.join(IMG_FOLDER, `test-sharp-${idx}.png`) + await tile.toFile(filename) + const { width: w, height: h } = await sharp(filename).metadata() + expect(w).toBe(width) + expect(h).toBe(size.height) + fs.rmSync(filename) + })) +}) + +test.concurrent('getUniqueHorizontalTiles', async () => { + const img = await getImage(horizontal.path) + const size = await getSize(img) + const width = size.width / 8 + const options: UniqueImagesOptions = { + requirement: 'all', + distance: MAX_DISTANCE, + difference: MAX_DIFFERENCE + } + const tiles = await getUniqueHorizontalTiles(img, size, width, options) + Promise.all(tiles.map(async (tile, idx) => { + const extension = 'png' + const filename = path.join(__dirname, `horizontal_${idx}.${extension}`) + await tile.toFile(filename) + })) + expect(tiles).toHaveLength(2) +}) + +test.skip.concurrent('getVerticalTiles', async () => { + const img = await getImage(vertical.path) + const size = await getSize(img) + const height = size.height / 8 + const tiles = getVerticalTiles(img, size, height) + expect(tiles).toHaveLength(8) +}) + +test.skip.concurrent('getUniqueVerticalTiles', async () => { + const img = await getImage(vertical.path) + const size = await getSize(img) + const height = size.height / 8 + const options: UniqueImagesOptions = { + requirement: 'all', + distance: MAX_DISTANCE, + difference: MAX_DIFFERENCE + } + const tiles = getUniqueVerticalTiles(img, size, height, options) + expect(tiles).toHaveLength(6) +}) + +test.skip.concurrent('getGridTiles', async () => { + const img = await getImage(chess.path) + const size = await getSize(img) + const width = size.width / 8 + const height = size.height / 8 + const tiles = getGridTiles(img, size, width, height) + expect(tiles).toHaveLength(8 * 8) +}) + +test.skip.concurrent('getUniqueGridTiles', async () => { + const img = await getImage(chess.path) + const size = await getSize(img) + const width = size.width / 8 + const height = size.height / 8 + const options: UniqueImagesOptions = { + requirement: 'all', + distance: MAX_DISTANCE, + difference: MAX_DIFFERENCE + } + const tiles = getUniqueGridTiles(img, size, width, height, options) + // Promise.all(tiles.map(async (tile, idx) => { + // const extension = tile.getExtension() + // const filename = path.join(__dirname, `horizontal_${idx}.${extension}`) + // const buffer = await tile.getBufferAsync(tile.getMIME()) + // fs.writeFileSync(filename, buffer) + // })) + expect(tiles).toHaveLength(22) +}) + +test.skip.concurrent('writeImages', { timeout: 50000 }, async () => { + const options: StoreOptions = { + path: __dirname, + filename: 'horizontal_test', + extension: 'jpg' + } + await expect(writeImages([], options)).rejects.toThrow(SpliteaError) + const [img1, img2, img3] = await Promise.all([getImage(chess.path), getImage(satie.path), getImage(forest.path)]) + await expect(writeImages([img1], options)).resolves.toHaveLength(1) + await expect(writeImages([img1, img2, img3], options)).resolves.toHaveLength(3) + fs.readdirSync(__dirname).forEach(item => { + if (item.includes(options.filename)) { + const file = path.join(__dirname, item) + fs.rmSync(file) + } + } ) +}) + +describe.skip.concurrent('Test getBufferImages()', { timeout: 50000 }, () => { + test('checking buffers', async () => { + const image1 = forest.path + const image2 = satie.path + const [jimp1, jimp2] = await Promise.all([getImage(image1), getImage(image2)]) + const buffers = await getBufferImages([jimp1, jimp2]) + buffers.forEach(async (buffer) => { + expect(buffer).toBeInstanceOf(Buffer) + }) + }) +}) diff --git a/tests/splitea.test.ts b/tests/splitea.test.ts index cf39595..e9c6ff2 100755 --- a/tests/splitea.test.ts +++ b/tests/splitea.test.ts @@ -1,4 +1,5 @@ import fs from 'node:fs' +import fspromises from 'node:fs/promises' import path from 'node:path' import { describe, test, expect } from 'vitest' import { HorizontalOptions, VerticalOptions, verticalTiles, horizontalTiles, GridOptions, gridTiles } from '../src' @@ -43,7 +44,7 @@ const vertical = { height: 720, } -describe.concurrent('Horizontal', () => { +describe.skip.concurrent('Horizontal', () => { test('Not unique tiles', async () => { const image = horizontal.path @@ -94,7 +95,7 @@ describe.concurrent('Horizontal', () => { }) }) -describe.concurrent('Vertical', () => { +describe.skip.concurrent('Vertical', () => { test('Not unique tiles', async () => { const image = vertical.path @@ -147,7 +148,7 @@ describe.concurrent('Vertical', () => { describe.concurrent('Grid', { timeout: 50000 }, () => { - test('Not unique tiles', async () => { + test.skip('Not unique tiles', async () => { const image = chess.path const options: GridOptions = { rows: 8, columns: 8 } await expect(gridTiles(image, options)).resolves.toHaveLength(8 * 8) @@ -159,17 +160,20 @@ describe.concurrent('Grid', { timeout: 50000 }, () => { }) test('Unique tiles', async () => { - const image = chess.path - const options: GridOptions = { rows: 8, columns: 8, unique: true } - await expect(gridTiles(image, options)).resolves.toHaveLength(22) - delete options.rows - delete options.columns - options.width = horizontal.width / 8 - options.height = vertical.height / 8 - await expect(gridTiles(image, options)).resolves.toHaveLength(22) + const image = forest.path + const options: GridOptions = { width: 8, height: 8, unique: true } + const tiles = await gridTiles(image, options) + tiles.map( (tile, idx) => fs.writeFileSync(`zerasul_${idx}.png`, tile)) + await expect(tiles).resolves.toHaveLength(22) + + // delete options.rows + // delete options.columns + // options.width = horizontal.width / 8 + // options.height = vertical.height / 8 + // await expect(gridTiles(image, options)).resolves.toHaveLength(22) }) - test('rows-columns or width-height are not submultiple of image.width-image.size', async () => { + test.skip('rows-columns or width-height are not submultiple of image.width-image.size', async () => { const image = chess.path const options: GridOptions = { rows: 7, columns: 8 } await expect(gridTiles(image, options)).rejects.toThrow(SpliteaError) @@ -186,7 +190,7 @@ describe.concurrent('Grid', { timeout: 50000 }, () => { await expect(gridTiles(image, options)).rejects.toThrow(SpliteaError) }) - test('store and return buffer', async () => { + test.skip('store and return buffer', async () => { const image = chess.path const directory = path.join(__dirname, 'grid-test-buffer') // const filename = 'gridTest' @@ -197,7 +201,7 @@ describe.concurrent('Grid', { timeout: 50000 }, () => { fs.rmSync(directory, { recursive: true }) }) - test('store and return file', async () => { + test.skip('store and return file', async () => { const image = chess.path const directory = path.join(__dirname, 'grid-test-file') const options: GridOptions = { rows: 8, columns: 8, response: 'file', path: directory }