.
imgp-2.9/MANIFEST.in 0000664 0000000 0000000 00000000032 14477235441 0014064 0 ustar 00root root 0000000 0000000 include CHANGELOG LICENSE
imgp-2.9/Makefile 0000664 0000000 0000000 00000001012 14477235441 0013765 0 ustar 00root root 0000000 0000000 PREFIX ?= /usr/local
BINDIR ?= $(DESTDIR)$(PREFIX)/bin
MANDIR ?= $(DESTDIR)$(PREFIX)/share/man/man1
DOCDIR ?= $(DESTDIR)$(PREFIX)/share/doc/imgp
.PHONY: all install uninstall
all:
install:
install -m755 -d $(BINDIR)
install -m755 -d $(MANDIR)
install -m755 -d $(DOCDIR)
gzip -c imgp.1 > imgp.1.gz
install -m755 imgp $(BINDIR)
install -m644 imgp.1.gz $(MANDIR)
install -m644 README.md $(DOCDIR)
rm -f imgp.1.gz
uninstall:
rm -f $(BINDIR)/imgp
rm -f $(MANDIR)/imgp.1.gz
rm -rf $(DOCDIR)
test:
./imgp --help
imgp-2.9/README.md 0000664 0000000 0000000 00000024321 14477235441 0013614 0 ustar 00root root 0000000 0000000 imgp
Watch imgp resize a directory of images in lightning speed!
`imgp` is a command line image resizer and rotator for JPEG and PNG images. It can resize (or thumbnail) and rotate thousands of images in a go, at lightning speed, while saving significantly on storage.
Powered by multiprocessing, SIMD parallelism (thanks to the Pillow-SIMD library), an intelligent adaptive algorithm, recursive operations, shell completion scripts, EXIF preservation (and more), `imgp` is a very flexible utility with well-documented easy to use options.
`imgp` intends to be a stronger replacement of the Nautilus Image Converter extension, not tied to any file manager and way faster. On desktop environments (like Xfce or LxQt) which do not integrate Nautilus, `imgp` will save your day. File manager [nnn](https://github.com/jarun/nnn) provides a script to batch resize images with `imgp`.
### Table of Contents
- [Features](#features)
- [Adaptive mode](#adaptive-mode)
- [Performance](#performance)
- [Installation](#installation)
- [Dependencies](#dependencies)
- [From a package manager](#from-a-package-manager)
- [Release packages](#release-packages)
- [From source](#from-source)
- [Running standalone](#running-standalone)
- [Shell completion](#shell-completion)
- [Usage](#usage)
- [cmdline options](#cmdline-options)
- [Operational notes](#operational-notes)
- [Examples](#examples)
- [Developers](#developers)
### Features
- resize by percentage or resolution
- rotate clockwise by specified angle
- adaptive resize considering orientation
- brute force to a resolution
- optimize images to save more space
- limit processing by minimum image size
- convert PNG to JPEG
- erase exif metadata
- specify output JPEG image quality
- force smaller to larger resize
- process directories recursively
- overwrite source image option
- completion scripts for bash, fish, zsh
- minimal dependencies
#### Adaptive mode
- If the specified and image orientations are same [(H >= V and h > v) or (H < V and h < v)], the image is resized with the longer specified side as reference.
- In case of cross orientation [(H >= V and h <= v) or (H < V and h >= v)], the image is resized with the shorter specified side as reference. Same as non-adaptive.
For example, if an image has a resolution of 2048x1365 and is being resized to 1366x768:
- In regular mode (default), output image resolution will be 1152x768
- In adaptive mode, output image resolution will be 1366x910
### Performance
`imgp` could resize 8823 images (approx. 4.5GB in size) of mixed resolutions (high to regular) stored in a USB 2.0 external hard disk at an adaptive resolution of 1366x1000 in around 8 minutes. The resulting size was 897MB (approx. 20%).
`imgp` uses Python PIL/Pillow library. Nautilus Image Converter calls the `convert` utility from ImageMagick. For a comparative benchmark, head [here](https://github.com/uploadcare/pillow-simd#benchmarks).
### Installation
#### Dependencies
`imgp` requires Python 3.8 or later.
To install PIL library on Ubuntu, run:
$ sudo apt-get install python3-pil
or, using pip3:
$ sudo pip3 install pillow
pillow can be replaced by [pillow-simd](https://github.com/uploadcare/pillow-simd) on [SIMD](https://en.wikipedia.org/wiki/SIMD) processors.
#### From a package manager
Install `imgp` from your package manager. If the version available is dated try an alternative installation method.
Packaging status (expand)
Unlisted packagers:
● Homebrew TAP (brew install jarun/imgp/imgp
)
● PyPI (pip3 install imgp
)
● Source Mage (cast imgp
)
#### Release packages
Packages for Arch Linux, CentOS, Debian, Fedora, openSUSE Leap and Ubuntu are available with the [latest stable release](https://github.com/jarun/imgp/releases/latest).
#### From source
If you have git installed, clone this repository. Otherwise download the [latest stable release](https://github.com/jarun/imgp/releases/latest) or [development version](https://github.com/jarun/imgp/archive/master.zip) (*risky*).
Install to default location (`/usr/local`):
$ sudo make install
To remove, run:
$ sudo make uninstall
`PREFIX` is supported, in case you want to install to a different location.
#### Running standalone
`imgp` is a standalone utility. From the containing directory, run:
$ ./imgp
### Shell completion
Shell completion scripts for Bash, Fish and Zsh can be found in respective subdirectories of [auto-completion/](https://github.com/jarun/imgp/blob/master/auto-completion). Please refer to your shell's manual for installation instructions.
### Usage
#### cmdline options
```
usage: imgp [-h] [-x res] [-o deg] [-a] [-c] [-e] [-f] [-H] [-i] [-k] [-m] [-M res]
[-n] [-N] [-O] [-P] [-q N] [-r] [-s byte] [-w] [-d] [PATH [PATH ...]]
Resize, rotate JPEG and PNG images.
positional arguments:
PATH source file or dir [default: current dir]
optional arguments:
-h, --help show this help message and exit
-x res, --res res output resolution in HxV or percentage
-o deg, --rotate deg rotate clockwise by angle (in degrees)
-a, --adapt adapt to resolution by orientation [default: off]
-c, --convert convert PNG to JPG format [default: off]
-e, --eraseexif erase exif metadata [default: off]
-f, --force force to exact specified resolution [default: off]
-H, --hidden include hidden (dot) files [default: off]
-i, --includeimgp re-process _IMGP files. * RISKY: refer to docs
-k, --keep skip (honors -c or --pr) images matching specified
H or V or --res=100 [default: off]
-m, --mute operate silently [default: informative]
-M res, --minres res min resolution in HxV or percentage of --res to resize
-n, --enlarge enlarge smaller images [default: off]
-N, --nearest use nearest neighbour interpolation for PNG [default: antialias]
-O, --optimize optimize the output images [default: off]
-P, --progressive save JPEG images as progressive [default: off]
-q N, --quality N quality factor (N=1-95, JPEG only) [default: 75]
-r, --recurse process non-symbolic dirs recursively [default: off]
-s byte, --size byte minimum size to process an image [default: 1024]
-w, --overwrite overwrite source images [default: off]
-d, --debug enable debug logs [default: off]
```
#### Operational notes
- Multiple files and directories can be specified as source. If `PATH` is omitted, the current directory is processed.
- Output image names are appended with **_IMGP** if `--overwrite` option is not used. By default *_IMGP* files are not processed. Doing so may lead to potential race conditions when `--overwrite` option is used.
- PNG files with lower target hres/vres are not converted (even if `--convert` is used). Run `imgp --convert (*.png)` separately to convert those.
- Resize and rotate are lossy operations. For additional reductions in size try `--optimize` and `--eraseexif` options.
- Option `--optimize` is slower, the encoder makes an extra pass over the image in order to select optimal encoder settings.
- Progressive JPEG images are saved as progressive.
### Examples
1. Convert some images and directories:
$ imgp -x 1366x768 ~/ ~/Pictures/image3.png ~/Downloads/
/home/testuser/image1.png
3840x2160 -> 1365x768
11104999 bytes -> 1486426 bytes
/home/testuser/image2.jpg
2048x1365 -> 1152x768
224642 bytes -> 31421 bytes
/home/testuser/Pictures/image3.png
1920x1080 -> 1365x768
2811155 bytes -> 1657474 bytes
/home/testuser/Downloads/image4
2048x1365 -> 1152x768
224642 bytes -> 31421 bytes
2. Scale an image by 75% and overwrite the source image:
$ imgp -x 75 -w ~/image.jpg
/home/testuser/image.jpg
1366x767 -> 1025x575
120968 bytes -> 45040 bytes
3. Rotate an image clockwise by 90 degrees:
$ imgp -o 90 ~/image.jpg
120968 bytes -> 72038 bytes
4. Adapt the images in the current directory to 1366x1000 resolution. Visit all directories recursively, overwrite source images, ignore images with matching hres or vres but convert PNG images to JPEG.
$ imgp -x 1366x1000 -wrack
5. Set hres=800 and adapt vres maintaining the ratio.
$ imgp -x 800x0
Source omitted. Processing current directory...
./image1.jpg
1366x911 -> 800x534
69022 bytes -> 35123 bytes
./image2.jpg
1050x1400 -> 800x1067
458092 bytes -> 78089 bytes
6. Process images greater than 50KiB only:
$ imgp -wrackx 1366x1000 -s 51200
7. Generate a 64px adapative thumbnail of the last modified file in the current dir:
#!/usr/bin/env sh
thumb64 ()
{
pop=$(ls -1t | head -1)
imgp -acx 64x64 "$pop"
}
### Developers
1. Copyright © 2016-2023 [Arun Prakash Jana](https://github.com/jarun)
2. [Ananya Jana](https://github.com/ananyajana)
imgp-2.9/auto-completion/ 0000775 0000000 0000000 00000000000 14477235441 0015452 5 ustar 00root root 0000000 0000000 imgp-2.9/auto-completion/bash/ 0000775 0000000 0000000 00000000000 14477235441 0016367 5 ustar 00root root 0000000 0000000 imgp-2.9/auto-completion/bash/imgp-completion.bash 0000664 0000000 0000000 00000002170 14477235441 0022331 0 ustar 00root root 0000000 0000000 #
# Bash completion definition for imgp.
#
# Author:
# Arun Prakash Jana
#
_imgp () {
COMPREPLY=()
local IFS=$' \n'
local cur=$2 prev=$3
local -a opts opts_with_args
opts=(
-a --adapt
-c --convert
-e --eraseexif
-f --force
-H --hidden
-i --includeimgp
-k --keep
-m --mute
-M --minres
-n --enlarge
-N --nearest
-o --rotate
-O --optimize
-P --progressive
-q --quality
-r --recurse
-s --size
-w --overwrite
-x --res
-d --debug
-h --help
)
opts_with_arg=(
-M --minres
-o --rotate
-q --quality
-s --size
-x --res
)
# Do not complete non option names
[[ $cur == -* ]] || return 1
# Do not complete when the previous arg is an option expecting an argument
for opt in "${opts_with_arg[@]}"; do
[[ $opt == $prev ]] && return 1
done
# Complete option names
COMPREPLY=( $(compgen -W "${opts[*]}" -- "$cur") )
return 0
}
complete -F _imgp imgp
imgp-2.9/auto-completion/fish/ 0000775 0000000 0000000 00000000000 14477235441 0016403 5 ustar 00root root 0000000 0000000 imgp-2.9/auto-completion/fish/imgp.fish 0000664 0000000 0000000 00000003442 14477235441 0020215 0 ustar 00root root 0000000 0000000 #
# Fish completion definition for imgp.
#
# Author:
# Arun Prakash Jana
#
complete -c imgp -s a -l adapt --description 'adapt to resolution by orientation'
complete -c imgp -s c -l convert --description 'convert PNG to JPG format'
complete -c imgp -s e -l eraseexif --description 'erase exif metadata'
complete -c imgp -s f -l force --description 'force to exact specified resolution'
complete -c imgp -s H -l hidden --description 'include hidden (dot) files'
complete -c imgp -s i -l includeimgp --description 're-process _IMGP files'
complete -c imgp -s k -l keep --description 'skip images with matching hres or vres'
complete -c imgp -s m -l mute --description 'operate silently'
complete -c imgp -s M -l minres -r --description 'minimum resolution in HxV or %'
complete -c imgp -s n -l enlarge --description 'enlarge smaller images'
complete -c imgp -s N -l nearest --description 'use nearest neighbour interpolation for PNG'
complete -c imgp -s o -l rotate -r --description 'rotate clockwise by angle (in degrees)'
complete -c imgp -s O -l optimize --description 'optimize the output images'
complete -c imgp -l P -l progressive --description 'save JPEG as progressive'
complete -c imgp -s q -l quality -r --description 'specify quality factor (1-95)'
complete -c imgp -s r -l recurse --description 'process directories recursively'
complete -c imgp -s s -l size -r --description 'min byte size to process an image'
complete -c imgp -s w -l overwrite --description 'overwrite source images'
complete -c imgp -s x -l res -r --description 'output resolution in HxV or %'
complete -c imgp -s d -l debug --description 'enable debug logs'
complete -c imgp -s h -l help --description 'show help'
imgp-2.9/auto-completion/zsh/ 0000775 0000000 0000000 00000000000 14477235441 0016256 5 ustar 00root root 0000000 0000000 imgp-2.9/auto-completion/zsh/_imgp 0000664 0000000 0000000 00000003037 14477235441 0017277 0 ustar 00root root 0000000 0000000 #compdef imgp
#
# Completion definition for imgp.
#
# Author:
# Arun Prakash Jana
#
setopt localoptions noshwordsplit noksharrays
local -a args
args=(
'(-a --adapt)'{-a,--adapt}'[adapt to resolution by orientation]'
'(-c --convert)'{-c,--convert}'[convert PNG to JPG format]'
'(-e --eraseexif)'{-e,--eraseexif}'[erase exif metadata]'
'(-f --force)'{-f,--force}'[force to exact specified resolution]'
'(-H --hidden)'{-H,--hidden}'[include hidden (dot) files]'
'(-i --includeimgp)'{-i,--includeimgp}'[re-process _IMGP files]'
'(-k --keep)'{-k,--keep}'[skip images with matching hres or vres]'
'(-m --mute)'{-m,--mute}'[operate silently]'
'(-M --minres)'{-M,--minres}'[minimum resolution]:HxV or %'
'(-n --enlarge)'{-n,--enlarge}'[enlarge smaller images]'
'(-N --nearest)'{-N,--nearest}'[use nearest neighbour interpolation for PNG]'
'(-o --rotate)'{-o,--rotate}'[rotate clockwise by angle]:degree'
'(-O --optimize)'{-p,--optimize}'[optimize the output images]'
'(-P --progressive)'{-P,--progressive}'[save JPEG as progressive]'
'(-q --quiet)'{-q,--quality}'[specify quality factor (1-95)]:factor'
'(-r --recurse)'{-r,--recurse}'[process directories recursively]'
'(-s --size)'{-s,--size}'[min byte size to process an image]:bytes'
'(-w --overwrite)'{-w,--overwrite}'[overwrite source images]'
'(-x --res)'{-x,--res}'[output resolution]:HxV or %'
'(-d --debug)'{-z,--debug}'[enable debug logs]'
'(-h --help)'{-h,--help}'[show help]'
)
_arguments -S -s $args
imgp-2.9/imgp 0000775 0000000 0000000 00000053510 14477235441 0013221 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python3
#
# Resize or rotate JPEG and PNG images.
#
# Copyright © 2016-2023 Arun Prakash Jana
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
import argparse
import textwrap
from multiprocessing import Pool, Lock
import os
import sys
import time
import PIL
from PIL import Image, ImageFile
if not hasattr(PIL.Image, 'Resampling'): # Pillow<9.0
PIL.Image.Resampling = PIL.Image
# Globals
HRES = 0 # specified horizontal resolution
VRES = 0 # specified vertical resolution
scale = None # specified scale in percentage
rotate = None # specified rotation in degree
adapt = False # use adaptive resolution
convert = False # convert PNG to JPG
hidden = False # process hidden files
eraseexif = False # erase EXIF metadata
progressive = False # save as a progressive image
force = False # forced to exact specified resolution
includeimgp = False # process _IMGP files
keep = False # ignore images with matching hres or vres
minH = 0 # min horizontal resolution to convert
minV = 0 # min vertical resolution to convert
enlarge = False # enlarge images with smaller hres or vres
optimal = False # apply PIL library optimization when saving
mute = False # suppress additional information
recurse = False # recursively process subdirectories
size = 1024 # min byte size to process an image
qual = 75 # the default value of quality factor
overwrite = False # remove source images
debug = False # enable debug information
no_res_opt = False # --res is not specified
pad = '_IMGP' # output file suffix when --overwrite is unused
png_ip = Image.Resampling.LANCZOS # default interpolation for PNG
fill_color = '#ffffff' # BG color to strip alpha channel
init_time = time.time() # profile the total time taken
_VERSION_ = '2.9' # current program version
# Globals for multiprocessing
pool = None
lock = Lock()
count = 0 # successful conversion count
count_lowres = 0 # no-op count due to low resolution
def getres(res):
'''Retrieve hres and vres as a tuple
:param res: resolution in hresxvres format
'''
hxv = res.split('x')
if len(hxv) != 2:
return (0, 0)
try:
return (int(hxv[0]), int(hxv[1]))
except Exception:
return (0, 0)
def lock_print(msg):
'''Acquire lock and print a message'''
global lock
with lock:
print(msg)
def cb(data):
global count, count_lowres
if data:
count += data[0]
count_lowres += data[1]
def traverse_dir(path):
'''Traverse a directory recursively'''
global lock, pool
try:
for entry in os.scandir(path):
# Add check for hidden here
if not hidden and entry.name.startswith('.'):
continue
if entry.is_dir(follow_symlinks=False):
if recurse:
if not os.access(entry.path, os.W_OK | os.X_OK):
lock_print(entry.path + ': no permission')
continue
traverse_dir(entry.path)
elif (entry.is_file(follow_symlinks=False) and
entry.stat().st_size >= size):
if rotate is not None:
pool.apply_async(rotate_image, args=(entry.path,), callback=cb)
else:
pool.apply_async(resize_image, args=(entry.path,), callback=cb)
except OSError as e:
with lock:
print(e)
def rotate_image(src):
'''Rotate a source image'''
global lock
converted = False
_progressive = progressive
name, ext = os.path.splitext(src)
if name.endswith(pad) and not includeimgp:
return
try:
img = Image.open(src)
_format = img.format
if _format not in ['JPEG', 'PNG', 'MPO']:
if debug:
lock_print(src + ': not JPEG/PNG/MPO. format: ' + _format)
return
try:
if _format == 'JPEG' and not _progressive and img.info['progressive']:
if debug:
lock_print('resize_image: progressive')
_progressive = True
except Exception:
pass
try:
if eraseexif:
exifdata = b''
else:
exifdata = img.info['exif']
except Exception:
exifdata = b''
except OSError as e:
if not str(e).startswith('cannot identify image file'):
with lock:
print(e)
return
# Image.rotate(angle, resample=0, expand=0) expand = True means
# the image will be enlarged to hold the entire rotated image
img = img.rotate(-rotate, 0, True)
if not mute:
stats = src + '\n%ld bytes ->' % os.path.getsize(src)
if overwrite:
suffix = ''
else:
suffix = pad
if convert and _format == 'PNG':
if img.mode in ('RGBA', 'LA'):
background = Image.new(img.mode[:-1], img.size, fill_color)
background.paste(img, img.split()[-1])
img = background
elif img.mode == "P":
img.convert("RGBA")
dest = name + suffix + '.jpg'
_format = 'JPEG'
converted = True
else:
dest = name + suffix + ext
try:
img.save(dest, _format, quality=qual, optimize=optimal, exif=exifdata, progressive=_progressive)
if not mute:
lock_print('%s %ld bytes\n' % (stats, os.path.getsize(dest)))
if overwrite and converted:
os.remove(src)
return [1, 0]
except OSError as e:
with lock:
print(e)
def resize_image(src):
'''Resize a source image'''
global HRES, VRES, minH, minV, lock
converted = False
_progressive = progressive
name, ext = os.path.splitext(src)
if name.endswith(pad) and not includeimgp:
return
try:
img = Image.open(src)
_format = img.format
if _format not in ['JPEG', 'PNG', 'MPO']:
if debug:
lock_print(src + ': not JPEG/PNG/MPO. format: ' + _format)
return
# check if convert or progressive is specified without any resolution option
if no_res_opt:
if convert and _format != 'PNG':
if debug:
lock_print('resize_image: cannot convert non-PNG')
return
if progressive and _format not in ['JPEG', 'MPO']:
if debug:
lock_print('resize_image: cannot make non-JPEG/MPO progressive')
return
try:
if _format == 'JPEG' and not _progressive and img.info['progressive']:
if debug:
lock_print('resize_image: progressive')
_progressive = True
except Exception:
pass
try:
if eraseexif:
exifdata = b''
else:
exifdata = img.info['exif']
except Exception:
exifdata = b''
except OSError as e:
if not str(e).startswith('cannot identify image file'):
with lock:
print(e)
return
hres = img.size[0]
vres = img.size[1]
ImageFile.MAXBLOCK = hres * vres
antialias = png_ip if _format == 'PNG' else Image.Resampling.LANCZOS
if hres < minH or vres < minV:
if debug:
lock_print('resize_image: hres < minH or vres < minV')
return
if keep and (HRES == hres or VRES == vres or scale == 100):
if progressive and _format not in ['JPEG', 'MPO']:
return
if convert and _format != 'PNG':
return
elif scale:
if scale == 100:
_hres = hres
_vres = vres
else:
_hres = hres * scale/float(100)
# use custom round()
if _hres >= int(_hres) + .5:
_hres = _hres + 1
_hres = int(_hres)
_vres = vres * scale/float(100)
if _vres >= int(_vres) + .5:
_vres = _vres + 1
_vres = int(_vres)
img = img.resize((_hres, _vres), antialias)
if debug:
lock_print('resize_image: scaled to [%dx%d]' % (_hres, _vres))
elif HRES == 0 or VRES == 0:
if HRES:
if not enlarge and HRES > hres:
if not mute:
lock_print('%s has lesser hres\n' % src)
return [0, 1]
hratio = HRES/float(hres)
target_vres = vres * hratio
if target_vres >= int(target_vres) + .5:
target_vres = target_vres + 1
target_vres = int(target_vres)
img = img.resize((HRES, target_vres), antialias)
if debug:
lock_print('resize_image: 0 vres %dx%d' % (HRES, target_vres))
if VRES:
if not enlarge and VRES > vres:
if not mute:
lock_print('%s has lesser vres\n' % src)
return [0, 1]
vratio = VRES/float(vres)
target_hres = hres * vratio
if target_hres >= int(target_hres) + .5:
target_hres = target_hres + 1
target_hres = int(target_hres)
img = img.resize((target_hres, VRES), antialias)
if debug:
lock_print('resize_image: 0 hres %dx%d' % (target_hres, VRES))
elif HRES == hres and VRES == vres:
if debug:
lock_print('resize_image: same res [%dx%d]' % (HRES, VRES))
elif force:
img = img.resize((HRES, VRES), antialias)
if debug:
lock_print('resize_image: brute force res to [%dx%d]' % (HRES, VRES))
elif adapt:
ratio_img = float(hres/vres)
ratio_target = float(HRES/VRES)
if (ratio_target >= 1 and ratio_img > 1) or (ratio_target < 1 and ratio_img >= 1):
# same orientation (H >= V and h > v) or
# cross orientation (H < V and h >= v)
if not enlarge and HRES > hres:
if not mute:
lock_print('%s has lesser hres\n' % src)
return [0, 1]
if HRES != hres:
hratio = HRES/float(hres)
target_vres = vres * hratio
if target_vres >= int(target_vres) + .5:
target_vres = target_vres + 1
target_vres = int(target_vres)
img = img.resize((HRES, target_vres), antialias)
if debug:
lock_print('resize_image: vres adapted %dx%d' % (HRES, target_vres))
elif (ratio_target < 1 and ratio_img < 1) or (ratio_target >= 1 and ratio_img <= 1):
# same orientation (H < V and h < v) or
# cross orientation (H >= V and h <= v)
if not enlarge and VRES > vres:
if not mute:
lock_print('%s has lesser vres\n' % src)
return [0, 1]
if VRES != vres:
vratio = VRES/float(vres)
target_hres = hres * vratio
if target_hres >= int(target_hres) + .5:
target_hres = target_hres + 1
target_hres = int(target_hres)
img = img.resize((target_hres, VRES), antialias)
if debug:
lock_print('resize_image: hres adapted %dx%d' % (target_hres, VRES))
else:
ratio_img = float(hres/vres)
ratio_target = float(HRES/VRES)
if ratio_img >= ratio_target:
if not enlarge and HRES > hres:
if not mute:
lock_print('%s has lesser hres\n' % src)
return [0, 1]
# re-sample as per target horizontal resolution
hratio = HRES/float(hres)
target_vres = vres * hratio
if target_vres >= int(target_vres) + .5:
target_vres = target_vres + 1
target_vres = int(target_vres)
img = img.resize((HRES, target_vres), antialias)
if debug:
lock_print('resize_image: vres calculated %dx%d' % (HRES, target_vres))
else:
if not enlarge and VRES > vres:
if not mute:
lock_print('%s has lesser vres\n' % src)
return [0, 1]
# re-sample as per target vertical resolution
vratio = VRES/float(vres)
target_hres = hres * vratio
if target_hres >= int(target_hres) + .5:
target_hres = target_hres + 1
target_hres = int(target_hres)
img = img.resize((target_hres, VRES), antialias)
if debug:
lock_print('resize_image: hres calculated %dx%d' % (target_hres, VRES))
if not mute:
stats = src + '\n%dx%d -> %dx%d\n%ld bytes ->' % (hres, vres, img.size[0], img.size[1], os.path.getsize(src))
if overwrite:
suffix = ''
else:
suffix = pad
if convert and _format == 'PNG':
if img.mode in ('RGBA', 'LA'):
background = Image.new(img.mode[:-1], img.size, fill_color)
background.paste(img, img.split()[-1])
img = background
elif img.mode == "P":
img = img.convert("RGBA")
dest = name + suffix + '.jpg'
_format = 'JPEG'
converted = True
else:
dest = name + suffix + ext
try:
img.save(dest, _format, quality=qual, optimize=optimal, exif=exifdata, progressive=_progressive)
if not mute:
lock_print('%s %ld bytes\n' % (stats, os.path.getsize(dest)))
if overwrite and converted:
os.remove(src)
return [1, 0]
except OSError as e:
with lock:
print(e)
class ExtendedArgumentParser(argparse.ArgumentParser):
'''Extend classic argument parser'''
# Print additional help and info
@staticmethod
def print_extended_help(file=None):
if not file:
file = sys.stderr
file.write('''
Version %s
Copyright © 2016-2023 Arun Prakash Jana
License: GPLv3
Webpage: https://github.com/jarun/imgp
''' % _VERSION_)
# Help
def print_help(self, file=None):
super().print_help(file)
self.print_extended_help(file)
def parse_args(args=None, namespace=None):
'''Parse imgp arguments/options.
Parameters
----------
args : list, optional
Arguments to parse. Default is ``sys.argv``.
namespace : argparse.Namespace
Namespace to write to. Default is a new namespace.
Returns
-------
argparse.Namespace
Namespace with parsed arguments / options.
'''
argparser = ExtendedArgumentParser(description='Resize, rotate JPEG and PNG images.',
formatter_class=argparse.RawTextHelpFormatter)
addarg = argparser.add_argument
addarg('-x', '--res', nargs=1, metavar='res',
help='output resolution in HxV or percentage')
addarg('-o', '--rotate', type=int, metavar='deg',
help='rotate clockwise by angle (in degrees)')
addarg('-a', '--adapt', action='store_true',
help='adapt to resolution by orientation [default: off]')
addarg('-c', '--convert', action='store_true',
help='convert PNG to JPG format [default: off]')
addarg('-e', '--eraseexif', action='store_true',
help='erase exif metadata [default: off]')
addarg('-f', '--force', action='store_true',
help='force to exact specified resolution [default: off]')
addarg('-H', '--hidden', action='store_true',
help='include hidden (dot) files [default: off]')
addarg('-i', '--includeimgp', action='store_true',
help='re-process _IMGP files. * RISKY: refer to docs')
addarg('-k', '--keep', action='store_true',
help=textwrap.dedent('''\
skip (honors -c or --pr) images matching specified
H or V or --res=100 [default: off]'''))
addarg('-m', '--mute', action='store_true',
help='operate silently [default: informative]')
addarg('-M', '--minres', nargs=1, metavar='res',
help='min resolution in HxV or percentage of --res to resize')
addarg('-n', '--enlarge', action='store_true',
help='enlarge smaller images [default: off]')
addarg('-N', '--nearest', action='store_true',
help='use nearest neighbour interpolation for PNG [default: antialias]')
addarg('-O', '--optimize', action='store_true',
help='optimize the output images [default: off]')
addarg('-P', '--progressive', action='store_true',
help='save JPEG images as progressive [default: off]')
addarg('-q', '--quality', type=int, metavar='N', choices=range(1, 96),
help='quality factor (N=1-95, JPEG only) [default: 75]')
addarg('-r', '--recurse', action='store_true',
help='process non-symbolic dirs recursively [default: off]')
addarg('-s', '--size', type=int, metavar='byte',
help='minimum size to process an image [default: 1024]')
addarg('-w', '--overwrite', action='store_true',
help='overwrite source images [default: off]')
addarg('-d', '--debug', action='store_true',
help='enable debug logs [default: off]')
addarg('keywords', nargs='*', metavar='PATH',
help='source file or dir [default: current dir]')
# Show help and exit if no arguments
if len(sys.argv) < 2:
argparser.print_help(sys.stderr)
sys.exit(1)
return argparser.parse_args(args, namespace)
def main():
global HRES, VRES, scale, rotate, adapt, convert, hidden, eraseexif, force, \
includeimgp, keep, enlarge, optimal, progressive, mute, recurse, size, \
qual, overwrite, debug, no_res_opt, pool, init_time, count, png_ip, minH, minV
args = parse_args()
if args.rotate is not None and args.res is not None:
print('options --rotate and --res cannot be used together')
return
if args.res is not None:
if 'x' in args.res[0]:
HRES, VRES = getres(args.res[0])
if HRES == 0 and VRES == 0:
print('-ve values not allowed in --res, \
hres and vres cannot be 0 together')
return
else:
try:
if args.res[0].endswith('%'):
scale = int(args.res[0][:-1])
else:
scale = int(args.res[0])
if scale <= 0:
print('scale should be > 0%')
return
if scale > 100 and not args.enlarge:
print('use --enlarge to scale > 100%')
return
except Exception:
print('invalid value for --res')
return
elif args.rotate is not None:
rotate = args.rotate % 360
if not rotate:
print('cannot rotate by 0 degree')
return
elif args.convert or args.progressive or args.optimize:
no_res_opt = True
scale = 100
else:
print('missing image transformation')
return
if args.minres is not None:
if 'x' in args.minres[0]:
minH, minV = getres(args.minres[0])
if minH == 0 or minV == 0:
print('-ve values not allowed in --minres, \
H or V cannot be 0')
return
else:
if scale is not None:
print('both --res and --minres cannot be in percentage')
return
try:
if args.minres[0].endswith('%'):
minscale = int(args.minres[0][:-1])
else:
minscale = int(args.minres[0])
if minscale <= 0:
print('--minres should be > 0%')
return
minH = (HRES * minscale) / 100
minV = (VRES * minscale) / 100
except Exception:
print('invalid value for --minres')
return
adapt = args.adapt
convert = args.convert
hidden = args.hidden
eraseexif = args.eraseexif
force = args.force
includeimgp = args.includeimgp
keep = args.keep
enlarge = args.enlarge
optimal = args.optimize
progressive = args.progressive
mute = args.mute
recurse = args.recurse
if args.size is not None:
size = args.size
if args.quality is not None:
qual = args.quality
overwrite = args.overwrite
debug = args.debug
if args.nearest:
png_ip = PIL.Image.Resampling.NEAREST
pool = Pool()
for path in args.keywords:
if not os.path.exists(path):
print('%s does not exist' % path)
elif os.path.isdir(path):
traverse_dir(path)
elif args.rotate is not None:
if rotate_image(path):
count += 1
else:
if resize_image(path):
count += 1
# Process current directory if PATH is unspecified
if len(args.keywords) == 0:
if not mute:
print('Source omitted. Processing current directory...\n')
traverse_dir('.')
pool.close()
pool.join()
if not mute:
# Show number of images converted
if count:
print('%s processed in %.4f seconds.' % (count, time.time() - init_time))
# Show number of images not converted due to low resolution
if count_lowres:
print('%s not processed due to low resolution.' % count_lowres)
if __name__ == '__main__':
main()
imgp-2.9/imgp.1 0000664 0000000 0000000 00000014770 14477235441 0013362 0 ustar 00root root 0000000 0000000 .TH "IMGP" "1" "10 Sep 2023" "Version 2.9" "User Commands"
.SH NAME
imgp \- Resize, rotate JPEG and PNG images.
.SH SYNOPSIS
.B imgp [OPTIONS] [PATH [PATH ...]]
.SH DESCRIPTION
.B imgp
is a multiprocessing command line image resizer and rotator for JPEG and PNG images.
.PP
.B Features
.PP
* resize by percentage or resolution
* rotate clockwise by specified angle
* adaptive resize considering orientation
* brute force to a resolution
* optimize images to save more space
* limit processing by minimum image size
* convert PNG to JPEG
* erase exif metadata
* specify output JPEG image quality
* force smaller to larger resize
* process directories recursively
* overwrite source image option
.PP
.B Adaptive mode
.PP
- If the specified and image orientations are same [(H >= V and h > v) or (H < V and h < v)], the image is resized with the longer specified side as reference.
.br
- In case of cross orientation [(H >= V and h <= v) or (H < V and h >= v)], the image is resized with the shorter specified side as reference. Same as non-adaptive.
.PP
For example, if an image has a resolution of 2048x1365 and is being resized to 1366x768:
.br
- In regular mode (default), output image resolution will be 1152x768
.br
- In adaptive mode, output image resolution will be 1366x910
.PP
.B Operational notes
.PP
- Multiple files and directories can be specified as source. If \fBPATH\fR is omitted, the current directory is processed.
.br
- Output image names are appended with \fB_IMGP\fR if '--overwrite' option is not used. By default \fB_IMGP\fR files are not processed. Doing so may lead to potential race conditions when '--overwrite' option is used.
.br
- PNG files with lower target hres/vres are not converted (even if '--convert' is used). Run 'imgp --convert (*.png)' separately to convert those.
.br
- Resize and rotate are lossy operations. For additional reductions in size try '--optimize' and '--eraseexif' options.
.br
- Option '--optimize' is slower, the encoder makes an extra pass over the image in order to select optimal encoder settings.
.br
- Progressive JPEG images are saved as progressive.
.SH OPTIONS
.TP
.BI "-h, --help"
Show help text and exit.
.TP
.BI "-x, --res=" res
Output resolution in HRESxVRES or percentage.
.TP
.BI "-o, --rotate=" deg
Rotate clockwise by a specified angle (in degrees). Negative inputs rotate anti-clockwise. Rotation by 0 degree is not allowed.
.TP
.BI "-a, --adapt"
Adapt to specified resolution considering the orientation of the image. Disabled by default.
.TP
.BI "-c, --convert"
Convert PNG images to JPEG to save on space. The output image is saved with '.jpg' extension.
.TP
.BI "-e, --eraseexif"
Erase EXIF metadata of JPEG images. Preserved by default.
.TP
.BI "-f, --force"
Force to the exact specified resolution. Disabled by default.
.TP
.TP
.BI "-H, --hidden"
Include hidden (dot) files. By default hidden files are skipped.
.BI "-i, --includeimgp"
Process \fI_IMGP\fR files. Risky due to potential race conditions.
.TP
.BI "-k, --keep"
Do not process if image hres or vres matches specified hres or vres, or --res is 100. However, PNG images are converted to JPEG if --convert option is specified and JPEG images are made progressive is --progressive option is specified.
.TP
.BI "-n, --enlarge"
Enlarge smaller images. By default smaller images are not scaled if specified resolution is greater.
.TP
.BI "-N --nearest"
Use nearest neighbour interpolation for PNG images instead of default antialias.
.TP
.BI "-p, --optimize"
Optimize output images using PIL library optimization algorithm. Disabled by default.
.TP
.BI "-P, --progressive"
Save all output JPEG images as progressive, even if the source is not.
.TP
.BI "-q, --quality=" N
Save the image with a specified quality factor N (scale 1-95, default 75). JPEG only.
.TP
.BI "-m, --mute"
Do not show any operational output.
.TP
.BI "-M, --minres" res
minimum resolution in HxV or percentage of --res to resize
.TP
.BI "-r, --recurse"
Recursively process sub-directories. By default only the specified directory is processed. Symbolic links are ignored to avoid recursive loops.
.TP
.BI "-s, --size=" byte
Minimum size in bytes required to process an image. Acts as a guard against processing low-resolution images. Default 1024 bytes.
.TP
.BI "-w, --overwrite"
Overwrite the source images. By default an output image is saved with \fB_IMGP\fR appended to the source image name.
.br
.B NOTE:
If overwrite and convert options are used together, source PNG images are deleted.
.TP
.BI "-d, --debug"
Enable debugging.
.SH EXAMPLES
.PP
.IP 1. 4
Convert some images and directories:
.PP
.EX
.IP
.B $ imgp -x 1366x768 ~/ ~/Pictures/image3.png ~/Downloads/
/home/testuser/image1.png
3840x2160 -> 1365x768
11104999 bytes -> 1486426 bytes
/home/testuser/image2.jpg
2048x1365 -> 1152x768
224642 bytes -> 31421 bytes
/home/testuser/Pictures/image3.png
1920x1080 -> 1365x768
2811155 bytes -> 1657474 bytes
/home/testuser/Downloads/image4
2048x1365 -> 1152x768
224642 bytes -> 31421 bytes
.EE
.PP
.IP 2. 4
Scale an image by 75% and overwrite the source image:
.PP
.EX
.IP
.B $ imgp -x 75 -w ~/image.jpg
/home/testuser/image.jpg
1366x767 -> 1025x575
120968 bytes -> 45040 bytes
.EE
.PP
.IP 3. 4
Rotate an image clockwise by 90 degrees:
.PP
.EX
.IP
.B $ imgp -o 90 ~/image.jpg
120968 bytes -> 72038 bytes
.EE
.PP
.IP 4. 4
Adapt the images in the current directory to 1366x1000 resolution.
.br
Visit all directories recursively, overwrite source images, ignore images with matching hres or vres but convert PNG images to JPEG.
.PP
.EX
.IP
.B $ imgp -x 1366x1000 -wrack
.EE
.PP
.IP 5. 4
Set hres=800 and adapt vres maintaining the ratio.
.PP
.EX
.IP
.B $ imgp -x 800x0
Source omitted. Processing current directory...
\[char46]/image1.jpg
1366x911 -> 800x534
69022 bytes -> 35123 bytes
\[char46]/image2.jpg
1050x1400 -> 800x1067
458092 bytes -> 78089 bytes
.EE
.PP
.IP 6. 4
Process images greater than 50KiB only:
.PP
.EX
.IP
.B $ imgp -wrackx 1366x1000 -s 51200
.EE
.PP
.IP 7. 4
Generate a 64px adaptive thumbnail of the last modified file in the current dir:
.PP
.EX
.IP
.B #!/usr/bin/env sh
thumb64 ()
{
pop=$(ls -1t | head -1)
imgp -acx 64x64 "$pop"
}
.EE
.SH AUTHORS
Arun Prakash Jana
.SH HOME
.I https://github.com/jarun/imgp
.SH REPORTING BUGS
.I https://github.com/jarun/imgp/issues
.SH LICENSE
Copyright \(co 2016-2023 Arun Prakash Jana
.PP
License GPLv3+: GNU GPL version 3 or later .
.br
This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law.
imgp-2.9/packagecore.yaml 0000664 0000000 0000000 00000003371 14477235441 0015467 0 ustar 00root root 0000000 0000000 name: imgp
maintainer: Arun Prakash Jana
license: GPLv3
summary: High-performance cli batch image resizer and rotator.
homepage: https://github.com/jarun/imgp
commands:
install:
- make PREFIX="/usr" install DESTDIR="${BP_DESTDIR}"
packages:
centos7.5:
builddeps:
- make
deps:
- python-pillow
- python
commands:
pre:
- yum install epel-release
centos7.6:
builddeps:
- make
deps:
- python-pillow
- python
commands:
pre:
- yum install epel-release
centos7.7:
builddeps:
- make
deps:
- python-pillow
- python
commands:
pre:
- yum install epel-release
centos8.0:
builddeps:
- make
deps:
- python3-pillow
- python3
commands:
precompile:
- dnf install python3
debian9:
builddeps:
- make
deps:
- python3-pillow
- python3
debian10:
builddeps:
- make
deps:
- python3-pillow
- python3
fedora31:
builddeps:
- make
deps:
- python3-pillow
- python3
fedora32:
builddeps:
- make
deps:
- python3-pillow
- python3
opensuse15.1:
builddeps:
- make
deps:
- python3-Pillow
- python3
opensuse15.2:
builddeps:
- make
deps:
- python3-Pillow
- python3
opensuse.tumbleweed:
builddeps:
- make
deps:
- python3-Pillow
- python3
ubuntu16.04:
builddeps:
- make
deps:
- python3-pillow
- python3
ubuntu18.04:
builddeps:
- make
deps:
- python3-pillow
- python3
ubuntu20.04:
builddeps:
- make
deps:
- python3-pillow
- python3
imgp-2.9/setup.py 0000664 0000000 0000000 00000004042 14477235441 0014045 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python3
import re
import os.path
import shutil
import sys
from setuptools import setup, find_packages
from pkg_resources import get_distribution, DistributionNotFound
def get_dist(pkgname):
try:
return get_distribution(pkgname)
except DistributionNotFound:
return None
if os.path.isfile('imgp'):
shutil.copyfile('imgp', 'imgp.py')
with open('imgp.py', encoding='utf-8') as f:
version = re.search('_VERSION_ = \'([^\']+)\'', f.read()).group(1)
with open('README.md', encoding='utf-8') as f:
long_description = f.read()
requirements = ['pillow-simd' if get_dist('pillow-simd') is not None else 'pillow']
setup(
name='imgp',
version=version,
description='High-performance CLI batch image resizer & rotator',
long_description=long_description,
long_description_content_type="text/markdown",
author='Arun Prakash Jana',
author_email='engineerarun@gmail.com',
url='https://github.com/jarun/imgp',
license='GPLv3',
license_file='LICENSE',
python_requires='>=3.8', # requires pip>=9.0.0
platforms=['any'],
py_modules=['imgp'],
install_requires=requirements,
include_package_data=True,
entry_points={
'console_scripts': ['imgp=imgp:main']
},
extras_require={
'packaging': ['twine']
},
keywords='image processing resize rotate optimize',
classifiers=[
'Development Status :: 5 - Production/Stable',
'Environment :: Console',
'Intended Audience :: Developers',
'Intended Audience :: End Users/Desktop',
'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
'Natural Language :: English',
'Operating System :: OS Independent',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3 :: Only',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Topic :: Utilities'
]
)