453 lines
12 KiB
Bash
Executable File
453 lines
12 KiB
Bash
Executable File
#!/bin/bash
|
|
# photos2pdf - Convert Flipchart/Whiteboard Photos to PDF
|
|
##
|
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
# of this software and associated documentation files (the “Software”), to deal
|
|
# in the Software without restriction, including without limitation the rights
|
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
# copies of the Software, and to permit persons to whom the Software is
|
|
# furnished to do so, subject to the following conditions:
|
|
|
|
# The above copyright notice and this permission notice shall be included in
|
|
# all copies or substantial portions of the Software.
|
|
|
|
# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
# SOFTWARE.
|
|
|
|
##
|
|
# Configuration Defaults
|
|
|
|
# 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 "$outdir/"*.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
|
|
|