Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
```bash
npx -y optimo public/media # for a directory
npx -y optimo public/media/banner.png # for a file
npx -y optimo public/media/banner.png --losy # enable lossy + lossless mode
npx -y optimo public/media/banner.png --lossy # enable lossy + lossless mode
npx -y optimo public/media/banner.png --format jpeg # convert + optimize
npx -y optimo public/media/banner.png --resize 50% # resize + optimize
npx -y optimo public/media/banner.png --resize 100kB # resize to max file size
Expand Down Expand Up @@ -52,7 +52,7 @@ When `optimo` is executed, a pipeline of compressors is chosen based on the outp
Mode behavior:

- default: lossless-first pipeline.
- `-l, --losy`: lossy + lossless pass per matching compressor.
- `-l, --lossy`: lossy + lossless pass per matching compressor.
- `-m, --mute`: remove audio tracks from video outputs (default: `true`; use `--mute false` to keep audio).
- `-p, --preserve-exif`: preserve EXIF metadata on image outputs (default: `false`).
- `-u, --data-url`: return optimized image as data URL (single file only; image only).
Expand All @@ -75,7 +75,7 @@ const optimo = require('optimo')
// optimize a single file
await optimo.file('/absolute/path/image.jpg', {
dryRun: false,
losy: false,
lossy: false,
preserveExif: false,
format: 'webp',
resize: '50%',
Expand All @@ -93,7 +93,7 @@ await optimo.file('/absolute/path/image.jpg', {
})

await optimo.file('/absolute/path/video.mp4', {
losy: true,
lossy: true,
// mute defaults to true for videos; set false to keep audio
mute: false,
format: 'webm',
Expand Down
4 changes: 2 additions & 2 deletions bin/help.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Usage
$ ${blue('optimo')} <file|dir> [options]

Options
-l, --losy Enable lossy + lossless passes (default: false)
-l, --lossy Enable lossy + lossless passes (default: false)
-m, --mute Remove audio tracks from videos (default: true)
-p, --preserve-exif Preserve EXIF metadata (default: false)
-u, --data-url Return optimized image as data URL (file input only)
Expand All @@ -20,7 +20,7 @@ Options
Examples
$ optimo image.jpg # optimize a single image in place
$ optimo image.jpg --preserve-exif # keep EXIF metadata on output image
$ optimo image.jpg --losy # run lossy + lossless optimization passes
$ optimo image.jpg --lossy # run lossy + lossless optimization passes
$ optimo clip.mp4 --mute # optimize video and remove audio track
$ optimo clip.mp4 --mute false # optimize video and keep audio track
$ optimo image.png --dry-run # preview optimization without writing files
Expand Down
13 changes: 7 additions & 6 deletions bin/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,29 @@ const INSTALL_HINTS = {
'mozjpegtran/jpegtran': 'brew install mozjpeg',
'magick:jxl': 'brew install imagemagick-full && brew link --overwrite --force imagemagick-full'
}
const FALSE_VALUES = ['false', '0', 'no', 'off']

async function main () {
const argv = mri(process.argv.slice(2), {
alias: {
'data-url': 'u',
'dry-run': 'd',
format: 'f',
lossy: 'l',
losy: 'l',
mute: 'm',
'preserve-exif': 'p',
resize: 'r',
silent: 's',
verbose: 'v'
}
},
boolean: ['lossy', 'losy', 'mute', 'preserve-exif']
})

const input = argv._[0]
const dataUrl = argv['data-url'] === true
const isEnabled = value => !FALSE_VALUES.includes(String(value).toLowerCase())
const mute = argv.mute === undefined ? true : isEnabled(argv.mute)
const preserveExif = argv['preserve-exif'] === undefined ? false : isEnabled(argv['preserve-exif'])
const lossy = argv.lossy === undefined ? argv.losy : argv.lossy
const mute = argv.mute === undefined ? true : argv.mute
const preserveExif = argv['preserve-exif'] === undefined ? false : argv['preserve-exif']
let resize = argv.resize

if (resize !== undefined && resize !== null) {
Expand All @@ -61,7 +62,7 @@ async function main () {
!argv.silent && console.error()

const result = await fn(input, {
losy: argv.losy,
lossy,
mute,
preserveExif,
dryRun: argv['dry-run'],
Expand Down
28 changes: 14 additions & 14 deletions src/compressor/ffmpeg.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,20 +67,20 @@ const getScaleFilter = resizeConfig => {
* - `-an`: drop audio entirely from output.
* - `-movflags +faststart`: move MP4 metadata (moov atom) to file start for faster streaming start.
*
* `losy=true` intentionally picks more aggressive values for smaller files.
* `lossy=true` intentionally picks more aggressive values for smaller files.
*
* @param {{ ext: string, losy?: boolean, mute?: boolean }} params
* @param {{ ext: string, lossy?: boolean, mute?: boolean }} params
* @returns {string[]}
*/
const getCodecArgsByExt = ({ ext, losy = false, mute = true }) => {
const getCodecArgsByExt = ({ ext, lossy = false, mute = true }) => {
if (ext === '.webm') {
return [
'-c:v',
'libvpx-vp9',
'-b:v',
'0',
'-crf',
losy ? '35' : '31',
lossy ? '35' : '31',
'-row-mt',
'1',
'-tile-columns',
Expand All @@ -90,10 +90,10 @@ const getCodecArgsByExt = ({ ext, losy = false, mute = true }) => {
'-deadline',
'good',
'-cpu-used',
losy ? '2' : '1',
lossy ? '2' : '1',
'-pix_fmt',
'yuv420p',
...(mute ? [] : ['-c:a', 'libopus', '-b:a', losy ? '64k' : '96k'])
...(mute ? [] : ['-c:a', 'libopus', '-b:a', lossy ? '64k' : '96k'])
]
}

Expand All @@ -102,21 +102,21 @@ const getCodecArgsByExt = ({ ext, losy = false, mute = true }) => {
'-c:v',
'libtheora',
'-q:v',
losy ? '4' : '6',
...(mute ? [] : ['-c:a', 'libvorbis', '-q:a', losy ? '3' : '4'])
lossy ? '4' : '6',
...(mute ? [] : ['-c:a', 'libvorbis', '-q:a', lossy ? '3' : '4'])
]
}

return [
'-c:v',
'libx264',
'-preset',
losy ? 'medium' : 'slow',
lossy ? 'medium' : 'slow',
'-crf',
losy ? '28' : '23',
lossy ? '28' : '23',
'-pix_fmt',
'yuv420p',
...(mute ? [] : ['-c:a', 'aac', '-b:a', losy ? '96k' : '128k']),
...(mute ? [] : ['-c:a', 'aac', '-b:a', lossy ? '96k' : '128k']),
...(ext === '.mp4' || ext === '.m4v' || ext === '.mov'
? ['-movflags', '+faststart']
: [])
Expand All @@ -134,14 +134,14 @@ const getCodecArgsByExt = ({ ext, losy = false, mute = true }) => {
* inputPath: string,
* outputPath: string,
* resizeConfig?: { mode: string, value: string | number } | null,
* losy?: boolean,
* lossy?: boolean,
* mute?: boolean
* }} params
*/
const run = async ({ inputPath, outputPath, resizeConfig, losy = false, mute = true }) => {
const run = async ({ inputPath, outputPath, resizeConfig, lossy = false, mute = true }) => {
const ext = path.extname(outputPath).toLowerCase()
const scaleFilter = getScaleFilter(resizeConfig)
const codecArgs = getCodecArgsByExt({ ext, losy, mute })
const codecArgs = getCodecArgsByExt({ ext, lossy, mute })

await $(binaryPath, [
'-v',
Expand Down
4 changes: 2 additions & 2 deletions src/compressor/gifsicle.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ const runLossless = async ({ inputPath, outputPath }) =>
const runLossy = async ({ inputPath, outputPath }) =>
$(binaryPath, ['-O3', '--lossy=80', inputPath, '-o', outputPath])

const gif = withMeta('gif', async ({ inputPath, outputPath, losy = false }) => {
if (!losy) return runLossless({ inputPath, outputPath })
const gif = withMeta('gif', async ({ inputPath, outputPath, lossy = false }) => {
if (!lossy) return runLossless({ inputPath, outputPath })

const lossyPath = `${outputPath}.lossy.gif`
try {
Expand Down
30 changes: 15 additions & 15 deletions src/compressor/magick.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,13 @@ const MAGICK_SVG_FLAGS = []

const MAGICK_GENERIC_FLAGS = []

const flagsByExt = ({ ext, losy = false, preserveExif = false }) => {
if (ext === '.jpg' || ext === '.jpeg') return losy ? MAGICK_JPEG_LOSSY_FLAGS : MAGICK_JPEG_LOSSLESS_FLAGS
if (ext === '.png') return losy && !preserveExif ? MAGICK_PNG_LOSSY_FLAGS : MAGICK_PNG_LOSSLESS_FLAGS
const flagsByExt = ({ ext, lossy = false, preserveExif = false }) => {
if (ext === '.jpg' || ext === '.jpeg') return lossy ? MAGICK_JPEG_LOSSY_FLAGS : MAGICK_JPEG_LOSSLESS_FLAGS
if (ext === '.png') return lossy && !preserveExif ? MAGICK_PNG_LOSSY_FLAGS : MAGICK_PNG_LOSSLESS_FLAGS
if (ext === '.gif') return MAGICK_GIF_FLAGS
if (ext === '.webp') return losy ? MAGICK_WEBP_LOSSY_FLAGS : MAGICK_WEBP_LOSSLESS_FLAGS
if (ext === '.webp') return lossy ? MAGICK_WEBP_LOSSY_FLAGS : MAGICK_WEBP_LOSSLESS_FLAGS
if (ext === '.avif') return MAGICK_AVIF_FLAGS
if (ext === '.heic' || ext === '.heif') return losy ? MAGICK_HEIC_LOSSY_FLAGS : MAGICK_HEIC_LOSSLESS_FLAGS
if (ext === '.heic' || ext === '.heif') return lossy ? MAGICK_HEIC_LOSSY_FLAGS : MAGICK_HEIC_LOSSLESS_FLAGS
if (ext === '.jxl') return MAGICK_JXL_FLAGS
if (ext === '.svg') return MAGICK_SVG_FLAGS
return MAGICK_GENERIC_FLAGS
Expand Down Expand Up @@ -167,9 +167,9 @@ const writePng = async ({ inputPath, outputPath, flags, resizeGeometry }) => {
}
}

const runOnce = async ({ inputPath, outputPath, resizeGeometry, losy = false, preserveExif = false }) => {
const runOnce = async ({ inputPath, outputPath, resizeGeometry, lossy = false, preserveExif = false }) => {
const ext = path.extname(outputPath).toLowerCase()
const flags = [...(preserveExif ? [] : ['-strip']), ...flagsByExt({ ext, losy, preserveExif })]
const flags = [...(preserveExif ? [] : ['-strip']), ...flagsByExt({ ext, lossy, preserveExif })]

if (ext === '.png') {
return writePng({ inputPath, outputPath, flags, resizeGeometry })
Expand All @@ -178,7 +178,7 @@ const runOnce = async ({ inputPath, outputPath, resizeGeometry, losy = false, pr
return $(binaryPath, [inputPath, ...(resizeGeometry ? ['-resize', resizeGeometry] : []), ...flags, outputPath])
}

const runMaxSize = async ({ inputPath, outputPath, maxSize, losy = false, preserveExif = false }) => {
const runMaxSize = async ({ inputPath, outputPath, maxSize, lossy = false, preserveExif = false }) => {
const resultByScale = new Map()

const measureScale = async scale => {
Expand All @@ -191,7 +191,7 @@ const runMaxSize = async ({ inputPath, outputPath, maxSize, losy = false, preser
inputPath,
outputPath: candidatePath,
resizeGeometry,
losy,
lossy,
preserveExif
})

Expand Down Expand Up @@ -237,23 +237,23 @@ const runMaxSize = async ({ inputPath, outputPath, maxSize, losy = false, preser
}
}

const run = async ({ inputPath, outputPath, resizeConfig, losy = false, preserveExif = false }) => {
const run = async ({ inputPath, outputPath, resizeConfig, lossy = false, preserveExif = false }) => {
if (resizeConfig?.mode === 'max-size') {
return runMaxSize({
inputPath,
outputPath,
maxSize: resizeConfig.value,
losy,
lossy,
preserveExif
})
}

if (!losy) {
if (!lossy) {
return runOnce({
inputPath,
outputPath,
resizeGeometry: resizeConfig?.value,
losy: false,
lossy: false,
preserveExif
})
}
Expand All @@ -264,13 +264,13 @@ const run = async ({ inputPath, outputPath, resizeConfig, losy = false, preserve
inputPath,
outputPath: lossyPath,
resizeGeometry: resizeConfig?.value,
losy: true,
lossy: true,
preserveExif
})
await runOnce({
inputPath: lossyPath,
outputPath,
losy: false,
lossy: false,
preserveExif
})
} finally {
Expand Down
4 changes: 2 additions & 2 deletions src/compressor/svgo.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ const run = async ({ inputPath, outputPath, extraPlugins }) => {
}
}

const svg = withMeta('svg', async ({ inputPath, outputPath, losy = false }) => {
if (!losy) return run({ inputPath, outputPath, extraPlugins: EXTRA_COMMON_PLUGINS })
const svg = withMeta('svg', async ({ inputPath, outputPath, lossy = false }) => {
if (!lossy) return run({ inputPath, outputPath, extraPlugins: EXTRA_COMMON_PLUGINS })

const lossyPath = `${outputPath}.lossy.svg`
try {
Expand Down
18 changes: 10 additions & 8 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const runStepInPlaceIfSmaller = async ({ currentPath, extension, step, mute, pre
inputPath: currentPath,
outputPath: candidatePath,
resizeConfig: null,
losy: false,
lossy: false,
mute,
preserveExif
})
Expand All @@ -39,14 +39,14 @@ const runStepInPlaceIfSmaller = async ({ currentPath, extension, step, mute, pre
}
}

const executePipeline = async ({ pipeline, filePath, optimizedPath, resizeConfig, losy, mute, preserveExif }) => {
const executePipeline = async ({ pipeline, filePath, optimizedPath, resizeConfig, lossy, mute, preserveExif }) => {
const extension = path.extname(optimizedPath) || '.tmp'

await pipeline[0]({
inputPath: filePath,
outputPath: optimizedPath,
resizeConfig,
losy,
lossy,
mute,
preserveExif
})
Expand All @@ -57,7 +57,7 @@ const executePipeline = async ({ pipeline, filePath, optimizedPath, resizeConfig
extension,
mute,
preserveExif,
step: async args => step({ ...args, losy, preserveExif })
step: async args => step({ ...args, lossy, preserveExif })
})
}

Expand All @@ -66,8 +66,10 @@ const executePipeline = async ({ pipeline, filePath, optimizedPath, resizeConfig

const file = async (
filePath,
{ onLogs = () => {}, dryRun, format: outputFormat, resize, losy = false, mute = true, preserveExif = false, dataUrl = false } = {}
{ onLogs = () => {}, dryRun, format: outputFormat, resize, lossy, losy, mute = true, preserveExif = false, dataUrl = false } = {}
) => {
// Keep backward compatibility with the old `losy` option key.
const lossyMode = lossy === undefined ? (losy === undefined ? false : losy) : lossy
const outputPath = getOutputPath(filePath, outputFormat)
const resizeConfig = parseResize(resize)
const mediaKind = getMediaKind(outputPath)
Expand All @@ -77,7 +79,7 @@ const file = async (
const outputExt = path.extname(outputPath).toLowerCase()

const canUseJpegLosslessFastPath =
!losy &&
!lossyMode &&
!resizeConfig &&
!isConverting &&
(outputExt === '.jpg' || outputExt === '.jpeg') &&
Expand Down Expand Up @@ -108,7 +110,7 @@ const file = async (

const missingBinaries = ensureBinaries(
getRequiredBinaries(executionPipeline, {
losy,
lossy: lossyMode,
preserveExif
})
)
Expand Down Expand Up @@ -137,7 +139,7 @@ const file = async (
filePath,
optimizedPath,
resizeConfig,
losy,
lossy: lossyMode,
mute,
preserveExif
})
Expand Down