From c79515e6a74f0e98890060e1c307fdd8be1d138b Mon Sep 17 00:00:00 2001 From: Sebastian Gurin Date: Sat, 17 Nov 2018 01:35:30 -0300 Subject: [PATCH] execute pass only command when output files not neccesary and executeAndReturnOutputFile --- .../interactive-execute-context/package.json | 3 +- .../src/commandExamples.ts | 142 +++++++++++++++++- spec/executeSpec.ts | 65 +++++++- spec/testUtil.ts | 12 ++ spec/util/htmlSpec.ts | 33 ++-- src/execute.ts | 37 ++++- src/imageHome.ts | 3 +- src/util/cli.ts | 5 +- src/util/imageCompare.ts | 2 +- src/util/misc.ts | 10 ++ 10 files changed, 272 insertions(+), 40 deletions(-) create mode 100644 spec/testUtil.ts create mode 100644 src/util/misc.ts diff --git a/samples/interactive-execute-context/package.json b/samples/interactive-execute-context/package.json index e1e0574..0bb1e6b 100644 --- a/samples/interactive-execute-context/package.json +++ b/samples/interactive-execute-context/package.json @@ -4,11 +4,12 @@ "description": "html page that transform images in different formats (not understandable by the browser), transform each of them to png and show a table with all these transformations.", "main": "dist/index.js", "scripts": { + "all": "npm run clean && npm run build && npm run copy", "build": "tsc && npm run bundle", "bundle": "browserify dist/index.js -o static/bundle.js", "copy": "cp -r ../../spec/assets/fn.png ../../spec/assets/magick.* src/static/* static", "clean": "rm -rf dist static", - "start": "npm run clean && npm run build && npm run copy && npm run watch-all", + "start": "npm run all && npm run watch-all", "server": "http-server static", "watch-all": "concurrently 'npm run watch-build' 'npm run watch-server' ", "watch-build": "onchange -v 'src/**/*' -- npm run build", diff --git a/samples/interactive-execute-context/src/commandExamples.ts b/samples/interactive-execute-context/src/commandExamples.ts index d6119b0..33680d5 100644 --- a/samples/interactive-execute-context/src/commandExamples.ts +++ b/samples/interactive-execute-context/src/commandExamples.ts @@ -1,5 +1,5 @@ -import { ExecuteCommand, asCommand, Command, MagickInputFile, extractInfo } from "wasm-imagemagick"; -import { sampleCommandTemplates } from "imagemagick-browser"; +import { ExecuteCommand, asCommand, Command, MagickInputFile, extractInfo } from 'wasm-imagemagick' +import { sampleCommandTemplates } from 'imagemagick-browser' export interface Example { name: string @@ -8,6 +8,19 @@ export interface Example { } export const commandExamples: Example[] = [ + + + { + name: 'simple append', + description: `simple append+ command that joins two images`, + command: ` +convert -size 100x100 xc:red \\ + \( rose: -rotate -90 \) \\ + +append output.png + `.trim(), + }, + + { name: 'stars spiral and inwards', description: `By Polar Distorting the image we can make the comets flying or spiraling into a point!`, @@ -24,24 +37,139 @@ convert -size 250x100 xc: +noise Random -channel R -threshold .4% \\ \( +clone \) -compose multiply -flatten \\ -virtual-pixel Tile -background Black \\ -blur 0x.6 -motion-blur 0x15-60 -normalize \\ - +distort Polar 0 +repage star_spiral.gif`.trim() - } + +distort Polar 0 +repage star_spiral.gif`.trim(), + }, + + { + name: 'falling stars', + description: `use "-motion-blur" to create a field of falling stars`, + command: ` +convert -size 100x100 xc: +noise Random -channel R -threshold .4% \\ + -negate -channel RG -separate +channel \\ + \( +clone \) -compose multiply -flatten \\ + -virtual-pixel tile -blur 0x.4 -motion-blur 0x20+45 -normalize \\ + star_fall.gif`.trim(), + }, + { + name: 'simple stars', + description: `A random noise image is used to thin itself out generate a speckle pattern. Then some effects and colors`, + command: ` +convert -size 100x100 xc: +noise Random -channel R -threshold 5% \\ + -negate -channel RG -separate +channel \\ + -compose multiply -composite speckles.gif + +convert -size 100x100 xc: +noise Random -channel R -threshold 1% \\ + -negate -channel RG -separate +channel \\ + \( +clone \) -compose multiply -flatten \\ + -virtual-pixel tile -blur 0x.4 -contrast-stretch .8% \\ + stars.gif + +convert -size 100x100 xc: +noise Random -channel R -threshold 1% \\ + -negate -channel RG -separate +channel \\ + \( xc: +noise Random \) -compose multiply -flatten \\ + -virtual-pixel tile -blur 0x.4 -contrast-stretch .8% \\ + stars_colored.gif +`.trim(), + }, + + // commented - not working : + { + name: 'star bursts', + description: `Here we motion blur the stars in six directions (in pairs) then merge them together to create a field of 'star bursts', such as you get in a glass lens.`, + command: ` +convert -size 100x100 xc: +noise Random -channel R -threshold .2% \\ + -negate -channel RG -separate +channel \\ + \( +clone \) -compose multiply -flatten \\ + -virtual-pixel tile -blur 0x.3 \\ + \( -clone 0 -motion-blur 0x10+15 -motion-blur 0x10+195 \) \\ + \( -clone 0 -motion-blur 0x10+75 -motion-blur 0x10+255 \) \\ + \( -clone 0 -motion-blur 0x10-45 -motion-blur 0x10+135 \) \\ + -compose screen -background black -flatten -normalize \\ + -compose multiply -layers composite \\ + -set delay 30 -loop 0 -layers Optimize \\ + star_field.gif`.trim(), + }, + + { + name: 'stars animation', + description: `By combining the above with a plasma glitter animation you can make set of stars that look like christmas decorations.`, + command: ` +convert -size 100x100 xc: +noise Random -separate \\ + null: \\ + \( xc: +noise Random -separate -threshold 50% -negate \) \\ + -compose CopyOpacity -layers composite \\ + null: \\ + plasma:red-firebrick plasma:red-firebrick plasma:red-firebrick \\ + -compose Screen -layers composite \\ + null: \\ + \( xc: +noise Random -channel R -threshold .08% \\ + -negate -channel RG -separate +channel \\ + \( +clone \) -compose multiply -flatten \\ + -virtual-pixel tile -blur 0x.4 \\ + \( -clone 0 -motion-blur 0x15+90 -motion-blur 0x15-90 \) \\ + \( -clone 0 -motion-blur 0x15+30 -motion-blur 0x15-150 \) \\ + \( -clone 0 -motion-blur 0x15-30 -motion-blur 0x15+150 \) \\ + -compose screen -background black -flatten -normalize \) \\ + -compose multiply -layers composite \\ + -set delay 30 -loop 0 -layers Optimize stars_xmas.gif + +`.trim(), + }, + + { + name: 'radial flare', + description: `the width of the initial image before polar distorting, basically sets the number of rays that will be produced`, + command: ` + convert -size 100x1 xc: +noise Random -channel G -separate +channel \\ + -scale 100x100! +write flare_1a.png \\ + \( -size 100x100 gradient:gray(100%) -sigmoidal-contrast 10x50% \) \\ + -colorspace sRGB -compose hardlight -composite +write flare_1b.png \\ + -virtual-pixel HorizontalTileEdge -distort Polar -1 \\ + flare_1_final.png +`.trim(), + }, + + + { + name: 'radial flare2', + description: `another example using multiple overlays to achieve a different looking flare. Note the technique used to generating intermediate debugging and example images showing the steps involved.`, + command: ` +convert -size 100x1 xc: +noise Random -channel G -separate +channel \\ + -size 100x99 xc:black -append -motion-blur 0x35-90 \\ + \( -size 100x50 gradient:gray(0) \\ + -evaluate cos .5 -sigmoidal-contrast 3,100% \\ + -size 100x50 xc:gray(0) -append \) \\ + \( -size 1x50 xc:gray(0) \\ + -size 1x1 xc:gray(50%) \\ + -size 1x49 xc:gray(0) \\ + -append -blur 0x2 -scale 100x100! \) \\ + -scene 10 +write flare_2%x.png \\ + -background gray(0) -compose screen -flatten +write flare_2f.png \\ + -virtual-pixel HorizontalTileEdge -distort Polar -1 \\ + -colorspace sRGB flare_2_final.png +`.trim(), + }, + + + + + ] let selectExampleCounter = 0 sampleCommandTemplates.forEach(template => { - const example : Example = { + const example: Example = { name: template.name, description: template.description, - command: async function(inputFiles: MagickInputFile[]) { + async command(inputFiles: MagickInputFile[]) { const img = inputFiles[0] const info = await extractInfo(img) const context = { ...template.defaultTemplateContext, imageWidth: info[0].image.geometry.width, imageHeight: info[0].image.geometry.height } const command = template.template(context)[0].map(s => s === '$INPUT' ? img.name : s === '$OUTPUT' ? `output${selectExampleCounter++}.png` : s) return command - } + }, } commandExamples.push(example) diff --git a/spec/executeSpec.ts b/spec/executeSpec.ts index 11efe5b..0dc9874 100644 --- a/spec/executeSpec.ts +++ b/spec/executeSpec.ts @@ -1,4 +1,5 @@ -import { buildInputFile, compare, extractInfo, execute, executeOne } from '../src' +import { buildInputFile, compare, execute, executeAndReturnOutputFile, executeOne, extractInfo } from '../src' +import { showImages } from './testUtil' export default describe('execute', () => { @@ -71,9 +72,51 @@ export default describe('execute', () => { }) const result2 = await executeOne({ inputFiles: [await buildInputFile('fn.png', 'image1.png')], - commands: ['convert image1.png -rotate 70 -scale 23% image2.gif'], + commands: ['convert image1.png -rotate 70 -scale 23% image2.png'], }) - expect(await compare(outputFiles.find(f => f.name === 'image3.jpg'), result2.outputFiles[0])).toBe(true) + const image3 = outputFiles.find(f => f.name === 'image3.jpg') + const image2 = result2.outputFiles[0] + // await showImages([image3,image2]) + expect(await compare(image3, image2)).toBe(true) + done() + }) + + it('supports just a command when no input files are necessary', async done => { + const { outputFiles } = await execute([ + 'convert rose: -rotate 70 image2.gif', + 'convert image2.gif -scale 23% image3.jpg', + ]) + const result2 = await execute('convert rose: -rotate 70 -scale 23% image2.png') + const image3 = outputFiles.find(f => f.name === 'image3.jpg') + const image2 = result2.outputFiles[0] + await showImages([image3, image2]) + + expect(await compare(image3, image2)).toBe(true) + done() + }) + + it('convert won\'t replace input files', async done => { + const input = await buildInputFile('fn.png') + const result = await execute({ + inputFiles: [input], + commands: ['convert fn.png -rotate 10 fn.png'], + }) + const output = result.outputFiles.find(f => f.name === 'fn.png') + expect(output).toBeUndefined() + done() + }) + + it('mogrify will replace input files', async done => { + const input = await buildInputFile('fn.png') + const result = await execute({ + inputFiles: [input], + commands: ['mogrify -rotate 10 fn.png'], + }) + const output = result.outputFiles.find(f => f.name === 'fn.png') + expect(output).toBeDefined() + const converted = await executeAndReturnOutputFile({ inputFiles: [input], commands: 'convert fn.png -rotate 10 output.png' }) + // await showImages([output, converted]) + expect(await compare(output, converted)).toBe(true) done() }) @@ -134,5 +177,21 @@ export default describe('execute', () => { }) }) + describe('executeAndReturnOutputFile', () => { + it('should support using just a command when input files are not necessary', async done => { + const out = await executeAndReturnOutputFile('convert rose: -rotate 55 -resize 55% out.png') + expect(out.name).toBe('out.png') + + const out2 = await executeAndReturnOutputFile(` + convert rose: -rotate 55 out1.png + convert out1.png -resize 55% out2.png + `, 'out2.png') + expect(out2.name).toBe('out2.png') + expect(await compare(out, out2)).toBe(true) + + done() + }) + }) + xit('event emitter', () => { }) }) diff --git a/spec/testUtil.ts b/spec/testUtil.ts new file mode 100644 index 0000000..3f9471c --- /dev/null +++ b/spec/testUtil.ts @@ -0,0 +1,12 @@ +import pMap from 'p-map' +import { loadImageElement, MagickFile } from '../src' + +export async function showImages(images: MagickFile[]): Promise { + await pMap(images, async image => { + const el = document.createElement('img') + el.title = image.name + el.alt = image.name + document.body.appendChild(el) + return await loadImageElement(image, el) + }, {concurrency: 1}) +} diff --git a/spec/util/htmlSpec.ts b/spec/util/htmlSpec.ts index 8f72007..6c9e027 100644 --- a/spec/util/htmlSpec.ts +++ b/spec/util/htmlSpec.ts @@ -1,39 +1,34 @@ -import { buildInputFile, loadImageElement, compare, execute } from '../../src' +import { buildInputFile, loadImageElement, compare, execute, Call, executeOne } from '../../src' export default describe('util/html', () => { describe('loadImageElement', () => { - it('should display an input image in an html img element', async done => { + it('should display an input and output images in an html img element', async done => { const img1 = await buildInputFile('fn.png') - const el = document.createElement('img') - document.body.appendChild(el) - + let el = document.createElement('img') + // document.body.appendChild(el) expect(el.src).toBeFalsy() await loadImageElement(img1, el) expect(el.src).toBeTruthy() - expect('visually check in the browser').toBe('visually check in the browser') - - const img2 = await buildInputFile(el.src, 'image2.png') + // expect('visually check in the browser').toBe('visually check in the browser') + let img2 = await buildInputFile(el.src, 'image2.png') expect(await compare(img1, img2)).toBe(true) - done() - }) - xit('should display an output image in an html img element', async done => { - // const result = execute({inputFiles: [await buildInputFile('fn.png')], commands :[ 'convert fn.png -rotate 90 out.git']) + const {outputFiles} = await executeOne({inputFiles: [img1], commands: ['convert fn.png -rotate 55 out.png']}) + const out = outputFiles[0] - // const el = document.createElement('img') + el = document.createElement('img') // document.body.appendChild(el) - - // expect(el.src).toBeFalsy() - // await loadImageElement(img1, el) - // expect(el.src).toBeTruthy() + expect(el.src).toBeFalsy() + await loadImageElement(out, el) + expect(el.src).toBeTruthy() // expect('visually check in the browser').toBe('visually check in the browser') - // const img2 = await buildInputFile(el.src, 'image2.png') + img2 = await buildInputFile(el.src, 'image2.png') + expect(await compare(out, img2)).toBe(true) - // expect(await compare(img1, img2)).toBe(true) done() }) diff --git a/src/execute.ts b/src/execute.ts index 254c208..dde6923 100644 --- a/src/execute.ts +++ b/src/execute.ts @@ -1,6 +1,7 @@ import { MagickInputFile, MagickOutputFile, outputFileToInputFile, call, asCommand } from '.' import pMap from 'p-map' import { CallResult } from './magickApi' +import { values } from './util/misc'; export type Command = (string | number)[] @@ -21,7 +22,8 @@ export interface ExecuteResultOne extends CallResult { } /** execute first command in given config */ -export async function executeOne(config: ExecuteConfig): Promise { +export async function executeOne(configOrCommand: ExecuteConfig | ExecuteCommand): Promise { + const config = asExecuteConfig(configOrCommand) let result: CallResult = { stderr: [], stdout: [], @@ -45,6 +47,30 @@ export async function executeOne(config: ExecuteConfig): Promise { + const config = asExecuteConfig(configOrCommand) + const result = await execute(config) + return outputFileName ? result.outputFiles.find(f => f.name === outputFileName) : result.outputFiles.length && result.outputFiles[0] +} + // execute event emitter export interface ExecuteEvent { @@ -104,7 +130,9 @@ export interface ExecuteResult extends ExecuteResultOne { * * ``` */ -export async function execute(config: ExecuteConfig): Promise { + +export async function execute(configOrCommand: ExecuteConfig | ExecuteCommand): Promise { + const config = asExecuteConfig(configOrCommand) config.inputFiles = config.inputFiles || [] const allOutputFiles: { [name: string]: MagickOutputFile } = {} const allInputFiles: { [name: string]: MagickInputFile } = {} @@ -117,7 +145,7 @@ export async function execute(config: ExecuteConfig): Promise { let allStderr = [] async function mapper(c: Command) { const thisConfig = { - inputFiles: Object.keys(allInputFiles).map(name => allInputFiles[name]), + inputFiles: values(allInputFiles), commands: [c], } const result = await executeOne(thisConfig) @@ -135,7 +163,7 @@ export async function execute(config: ExecuteConfig): Promise { await pMap(commands, mapper, { concurrency: 1 }) const resultWithError = results.find(r => r.exitCode !== 0) return { - outputFiles: Object.keys(allOutputFiles).map(name => allOutputFiles[name]), + outputFiles: values(allOutputFiles), errors: allErrors, results, stdout: allStdout, @@ -143,3 +171,4 @@ export async function execute(config: ExecuteConfig): Promise { exitCode: resultWithError ? resultWithError.exitCode : 0, } } + diff --git a/src/imageHome.ts b/src/imageHome.ts index 566ad05..31c5c57 100644 --- a/src/imageHome.ts +++ b/src/imageHome.ts @@ -1,5 +1,6 @@ import { MagickInputFile, MagickFile, asInputFile, getBuiltInImages } from '.' import pMap from 'p-map' +import { values } from './util/misc'; export interface ImageHome { remove(names: string[]): MagickInputFile[] @@ -33,7 +34,7 @@ class ImageHomeImpl implements ImageHome { } async getAll(): Promise { - return await Promise.all(Object.keys(this.images).map(k => this.images[k])) + return await Promise.all(values(this.images)) } register(file: MagickFile, name: string = file.name): MagickInputFilePromise { diff --git a/src/util/cli.ts b/src/util/cli.ts index 918faea..85da5e7 100644 --- a/src/util/cli.ts +++ b/src/util/cli.ts @@ -1,5 +1,6 @@ import { Command } from '..' import { ExecuteCommand } from '../execute' +import { flat } from './misc'; /** generates a valid command line command from given Call/execute Command. Works in a single command */ export function arrayToCliOne(command: Command): string { @@ -83,7 +84,3 @@ export function asCommand(c: ExecuteCommand): Command[] { } return c as Command[] } - -export function flat(arr: T[][]): T[] { - return arr.reduce((a, b) => a.concat(b)) -} diff --git a/src/util/imageCompare.ts b/src/util/imageCompare.ts index 7f0a3c8..a8a5241 100644 --- a/src/util/imageCompare.ts +++ b/src/util/imageCompare.ts @@ -1,6 +1,6 @@ import { asInputFile, Call, MagickFile, blobToString, MagickInputFile } from '..' -export async function compare(img1: MagickFile | string, img2: MagickFile | string, error: number = 0.01): Promise { +export async function compare(img1: MagickFile | string, img2: MagickFile | string, error: number = 0.015): Promise { const identical = await compareNumber(img1, img2) return identical <= error } diff --git a/src/util/misc.ts b/src/util/misc.ts new file mode 100644 index 0000000..6f085c3 --- /dev/null +++ b/src/util/misc.ts @@ -0,0 +1,10 @@ +// internal misc utilities + +export function values(object: { [k: string]: T }): T[] { + return Object.keys(object).map(name => object[name]) +} + + +export function flat(arr: T[][]): T[] { + return arr.reduce((a, b) => a.concat(b)) +}