photos2pdf/photos2pdf
2023-12-10 23:14:24 +01:00

434 lines
11 KiB
Bash
Executable File

#!/bin/bash
##
# Configuration
# Before morphology analysis, reduce image dimensions by this factor
simplify=2
# Morphology to apply to reduce content to main area
morphology="Open:6"
morphology_shape="Disk"
# Number of colors images are quantized to for component detection
colors=4
# Neighbors to evaluate when detecting connected components
connected_components=8
# Area treshold for connected components detection
area_treshold=1000
# Sigmoidal contrast adjustment used when trimming
sigmoidal_contrast_trim="30,30%"
# Sigmoidal contrast adjustment visible in result images
sigmoidal_contrast="45,50%"
# Number of colors result images are quantized to
posterize=6
# Keep intermediate images after processing
keep_tmpfiles=true
# Paper size of all pages
paper="a4paper"
# Offset in page where image should be placed
offset="1cm 3cm"
# Factor to scale image by before placing in page
scale=0.75
# PDF backdrop template directory
templatedir=/var/lib/photos2pdf/template
# Filename of complete result PDF
result_filename="result.pdf"
# Directory of original files to process (initially unset)
dir=
##
# Functions
log() {
level=$1
msg=$2
printf "%s: %s\n" "$level" "$msg" >&2
}
error() {
log ERROR "$1"
exit "${2:-1}"
}
warning() {
log WARNING "$1"
}
info() {
log INFO "$1"
}
print_help() {
cat << 'EOF'
photos2pdf
==========
Description
-----------
Trim and brighten JPGs, produce PDF containing one photo per page.
Usage
-----
```shell
photos2pdf [OPTION ...] DIR
```
Arguments
---------
* `DIR`: Directory containing the original photos.
Options
-------
* `--simplify INT`: Before morphology analysis, reduce
image dimensions by this factor
(default: 2).
* `--morphology STRING`: Morphology to apply for trimming
(default "Open:6").
* `--morphology-shape STRING`: Shape of morphology to apply for
trimming (default: "Disk").
* `--colors INT`: Number of colors images are quantized
to for component detection (default: 4).
* `--connected-components INT`: Neighbors to evaluate when detecting
connected components (default: 8).
* `--area_treshold INT`: Area treshold for connected components
detection (default: 1000).
* `--sigmoidal-contrast-trim STRING`: Sigmoidal contrast adjustment
used when trimming (default: "30,30%").
* `--sigmoidal-contrast STRING`: Sigmoidal contrast adjustment visible
in result images (default: "45,50%").
* `--posterize INT`: Number of colors result images are
quantized to (default: 6).
* `--keep-tmpfiles true|false`: Keep temporary files (default: false).
* `--paper STRING`: PDF page size (default: "a4paper").
* `--offset STRING`: Offset in page where image should be
placed (default: "1cm 3cm").
* `--scale FLOAT`: Factor to scale image by before placing
in page (default: 0.75).
* `--templatedir STRING`: PDF backdrop directory (default:
"/var/lib/photos2pdf/template").
* `--result-filename STRING`: Filename of complete result PDF
(default: "result.pdf").
EOF
}
##
# Arguments
i=1
options_done=false
while [[ $i -le $# ]] ; do
arg=$(eval "echo \$$i")
if ! "$options_done" ; then
case "$arg" in
-h|--help)
print_help
exit 0
;;
--simplify| \
--morphology| \
--morphology-shape| \
--colors| \
--connected-components| \
--area-treshold| \
--sigmoidal-contrast-trim| \
--sigmoidal-contrast| \
--posterize| \
--keep-tmpfiles| \
--paper| \
--offset| \
--scale| \
--templatedir| \
--result-filename)
var=$(echo "$arg" | sed -e 's|^\-\-||;s|\-|_|g;')
i=$((i+1))
arg=$(eval "echo \$$i")
eval "$var=\"$arg\""
i=$((i+1))
continue
;;
--)
options_done=true
i=$((i+1))
continue
;;
-*)
error "Unknown option \"$arg\"; aborted."
;;
esac
fi
case "$arg" in
*)
[[ -n $dir ]] && error "Only one input directory can be specified; aborted."
dir=$arg
i=$((i+1))
;;
esac
done
[[ -z $dir ]] && error "Usage: $0 [OPTION ..] DIR"
##
# Main Program
job_id=$(printf "%s" "$origdir" | sha256sum | cut -d" " -f1)
origdir=$(readlink -f "$dir")
[[ -d $origdir ]] || error "Directory $dir not found; aborted."
if [[ -n $TMP ]] ; then
tmp_base=$TMP/photos2pdf/$job_id
elif [[ -n $XDG_RUNTIME_DIR ]] ; then
tmp_base=$XDG_RUNTIME_DIR/photos2pdf/$job_id
else
tmp_base=/tmp/photos2pdf/$job_id
fi
indir="$tmp_base/in"
tmpdir="$tmp_base/tmp"
outdir="$tmp_base/out"
pdfdir="$tmp_base/pdf"
if ! test -d "$origdir" ; then
error "Directory \"$origdir\" not found; aborted."
elif ! mkdir -p "$indir" "$outdir" "$pdfdir" "$tmpdir" ; then
error "Could not create working directories; aborted."
elif ! rm -f -- "$indir/"*.* ; then
error "Could not delete existing input files; aborted."
elif ! rm -f -- "$tmpdir/"*.* ; then
error "Could not delete existing tempfiles; aborted."
elif ! rm -f -- "$outdir/"*.* ; then
error "Could not delete existing results; aborted."
elif ! rm -f -- "$pdfdir/"*.* ; then
error "Could not delete existing PDFs; aborted."
fi
resize_percent=$((100/simplify))
i=1
rv=0
while read -r jpg ; do
renamed=$(printf "%02d" "$i").jpg
if cp "$jpg" "$indir/$renamed" ; then
info "Copied \"$jpg\" to \"$renamed\"."
else
warning "Could not copy \"$jpg\" to \"$renamed\"."
rv=1
fi
i=$((i+1))
done < <(ls --reverse --sort time -- "$origdir/"*.jpg)
if [[ $rv -ne 0 ]] ; then
error "One or more errors during input file naming; aborted." "$rv"
fi
for jpg in "$indir/"*.jpg ; do
base=$(basename "$jpg" .jpg)
tmp1="$tmpdir/$base.tmp1.png"
tmp2="$tmpdir/$base.tmp2.png"
com="$tmpdir/$base.com.png"
trimmed="$tmpdir/$base.trimmed.png"
inf="$tmpdir/$base.txt"
out="$outdir/$base.png"
if ! convert \
"$jpg" \
-colorspace RGB \
-sigmoidal-contrast "$sigmoidal_contrast_trim" \
-resize "$resize_percent%" \
-colors "$colors" \
-morphology "$morphology" "$morphology_shape" \
"$tmp1"
then
warning "Could not create test image \"$tmp1\"; skipped."
rv=1
continue
fi
draw=()
v=255
while read -r primitive ; do
draw+=("-fill" "rgb($v,$v,$v)" "-draw" "$primitive")
v=$((v-2))
done < <(
convert \
"$tmp1" \
-define connected-components:verbose=true \
-define connected-components:exclude-header=true \
-define connected-components:sort-order=increasing \
-define connected-components:mean-color=true \
-define connected-components:area-threshold="$area_treshold" \
-virtual-pixel None \
-connected-components "$connected_components" \
-auto-level \
"$com" | \
perl -e '
while(<>){
chomp;
next unless /(\d+)x(\d+)\+(\d+)\+(\d+)/;
printf
"rectangle %d,%d,%d,%d\n",
$1, $2, $1+$3, $2+$4;
}
' | \
sort | \
uniq
)
if ! convert \
"$tmp1" \
-fill black -colorize 100 \
"${draw[@]}" \
"$tmp2"
then
warning "Could not convert \"$tmp1\" to \"$tmp2\"; skipped."
rv=1
continue
elif ! convert \
"$tmp2" \
-trim info: \
> "$inf"
then
warning "Could not determine trim info from \"$tmp2\"; skipped."
rv=1
continue
fi
if "$keep_tmpfiles" ; then
if ! convert \
"$tmp2" \
-trim \
"$trimmed"
then
warning "Could not test trim \"$tmp2\"; skipped."
rv=1
continue
fi
fi
# upscale crop area from smaller temporary picture sizes
crop=$(simplify=$simplify perl -MEnv -e '
while(<>) {
chomp();
s/(\d+)/$1*${simplify}/eg;
/(\d+)x(\d+) \d+x\d+\+(\d+)\+(\d+)/ && do {
print $1."x".$2."+".$3."+".$4;
}
}
' < "$inf")
info "Processing $base: crop=$crop ..."
if ! convert \
"$jpg" \
-crop "$crop" \
-sigmoidal-contrast "$sigmoidal_contrast" \
+dither -posterize "$posterize" \
"$out"
then
warning "Could not process \"$jpg\"; skipped."
rv=1
continue
fi
done
if [[ $rv -ne 0 ]] ; then
error "One or more errors during image conversion; aborted." "$rv"
fi
for png in "out/"*.png ; do
info "Processing \"$png\" ..."
read -r width height < <(
identify -verbose "$png" | \
perl -ne '/Geometry: (\d+)x(\d+)/ && print "$1 $2"'
)
if [[ -z $width ]] || [[ -z $height ]] ; then
warning "Could not identify width or height of \"$png\"; skipped."
rv=1
continue
elif [[ $width -gt $height ]] ; then
backdrop="$templatedir/a4paper/landscape.pdf"
jam_landscape=--landscape
else
backdrop="$templatedir/a4paper/portrait.pdf"
jam_landscape=--no-landscape
fi
base=$(basename "$png" .png)
pdf="$tmpdir/$base.pdf"
jammed="$tmpdir/$base-pdfjam.pdf"
backdropped="$tmpdir/$base.backdropped.pdf"
if ! convert "$png" "$pdf" ; then
warning "Could not convert \"$png\" to PDF; skipped."
rv=1
continue
elif ! pdfjam \
--quiet \
--outfile "$tmpdir" \
--paper "$paper" \
"$jam_landscape" \
--scale "$scale" \
--offset "$offset" \
"$pdf"
then
warning "Could not align \"$pdf\"; skipped."
rv=1
continue
elif ! pdftk \
"$jammed" \
stamp "$backdrop" \
output "$backdropped"
then
warning "Could not watermark \"$pdf\"; skipped."
rv=1
continue
fi
done
input=()
while read -r pdf ; do
input+=("$pdf")
done < <(ls -- "$tmpdir/"*.backdropped.pdf)
if ! pdftk "${input[@]}" cat output "$pdfdir/$result_filename" ; then
warning "Could not concatenate \"$result_filename\"."
rv=1
elif ! "$keep_tmpfiles" ; then
if ! rm -f -- "$tmp_base" ; then
warning "Could not remove temporary files."
rv=1
fi
fi
if [[ $rv -ne 0 ]] ; then
error "One or more errors; aborted." "$rv"
fi