#!/bin/bash # scan2term - simulation for showing how to split ram pnm image by bunch of lines # for showing them in terminal, simultaneously, while post-process are running. # (C) 2026 Felix Hauri - felix@f-hauri.ch # Licensed under terms of GPL v3. www.gnu.org # Version: 0.1.0 -- Last update: Tue Jul 14 11:34:14 CEST 2026 # # Usage: # scan2term [Resolution] [Mode] [File] # Resolution: Any integer value. Default to `75` # Mode Any of `Lineart`, `Gray` or `Color`, Default to `Lineart` # File Any PS or PDF file. Default to embed printer-driver-pnm2ppa A4 # If run with environment variable `doRealScan` set, then run real `scanimage` # program instead of scanner simulation: # doRealScan=true /path/to/scan2term.sh 300 Color # # Use - `img2sixel` if your termnal support them. or `catimg`. # - `perl` for down image data rate to something under 1Mb/sec. # # Note: Enable sixels, windowops and colors in XTerm, by running: # xterm -ti vt340 -xrm 'XTerm*allowWindowOps: true' \ # -xrm 'XTerm*numColorRegisters: 256' # # scan2term.sh depends on: perl-base, file, catimg, netpbm, xdg-utils, sane-utils, # libsixel-bin, coreutils, grep, ghostscript, zstd, tesseract-ocr. # termPreview() { local resol=${1:-75} mode=${2:-Color} mmwidth=${3:-210.22} \ mmheight=${4:-297.3} tCaps factor cheight cwidth local -i lines cols byteperpoint=3 maxheight maxwidth printf -v mmwidth %.2f "$mmwidth" printf -v mmheight %.2f "$mmheight" [[ ${mode,,} == gr* ]] && byteperpoint=1 # This require Xresource "XTerm*allowWindowOps: true" IFS=\; 1000 ? 1000 : maxwidth )) termView() { img2sixel - echo # Required under XTerm but not under Konsole } ;; *';4' ) # Sixel capable (Konsole) maxwidth=$(( maxwidth > 1000 ? 1000 : maxwidth )) termView() { img2sixel - } ;; * ) termView() { catimg -w$((2 * cols)) - } ;; esac cols=$(( maxwidth / cwidth + ( maxwidth % cwidth ? 1 : 0 ) )) factor=0000$(( maxwidth * 254000000 / ${mmwidth/.} / resol )) printf -v factor '%.4f' ${factor::-5}.${factor: -5}; pamscale "$factor" | # pamscale will promote pbm to pgm ( # Read and adapt pnm header head='' width='' hlines=4 for (( tot=0, lne=0, cnt=1 ; cnt/dev/null \ bs=$(( byteperpoint * width * cheight )) ) | termView done ) } testPage() { # Embeded A4 Testpage. From printer-driver-pnm2ppa package if [[ -f $testFile ]] ; then cat "$testFile" return 0 fi zstd -d < <(base64 -d <<-"EOTestPage" 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 LqdCf5swvRISG11FVmBFwaVSBU2Dl34= EOTestPage ) } progName="$0" die() { printf >&2 '%s ERROR: %s\n' "$progName" "$*" exit 1; } if [[ $1 ]]; then case $1 in *[^0-9]* ) die "1st argume must be an integer!";; esac (( $1 )) || die "Resolution must be a not null positive integer!" fi # Define max output resolution to 200DPI for post-processing sample outRes=$(( ${1:-75} > 200 ? 200 : ${1:-75} )) dev=pbmraw case ${2,,} in col* ) dev=pnmraw ;; gr* ) dev=pgmraw ;; lin* ) dev=pbmraw ;; esac testFile='' if [[ $3 ]]; then [[ -e $3 ]] || die "File '$3' not found." [[ -r $3 ]] || die "File '$3' not readable." case $(file -b --mime-type "$3") in application/postscript|application/pdf ) : ;; * ) die "File '$3' could not be proceeded by Ghostscript!" ;; esac testFile="$3" fi # shellcheck disable=SC2034 vars="$(set)" start=$EPOCHREALTIME # Indepenant FD for graphic exec {txtViewFd}>&1 # Real scan tool: if [[ $doRealScan ]]; then scanimage --mode ${2:-Lineart} --format pnm --resolution ${1:-75} \ -l 0 -t .2 -x 210.22 -y 297.30 else # Simulation scan tool ( gs + perl ): gs -sDEVICE=$dev -q -sOutputFile=- -dNOPAUSE -dBATCH -dSAFER -dLastPage=1 \ -r"${1:-75}" - < <(testPage) -c quit | perl -e 'while (sysread STDIN,my($buff),128) { syswrite STDOUT,$buff;select undef,undef,undef,.0001 }' fi | # Uncoment this to show *real* overall data rate. # tee >( len=$(wc -c); bc -l <<<"$len / ($EPOCHREALTIME-$start)" >/dev/tty ) | tee >( termPreview "${@:1:2}" >&$txtViewFd ) | # Post-processing sample if (( ${1:-75} == outRes)) ; then cat else factor=0000$(( outRes * 100000 / ${1:-75} )) printf -v factor '%.4f\n' ${factor::-5}.${factor: -5} pamscale $factor fi | pamtopng | tesseract --dpi "$outRes" - "/tmp/dummyScan" pdf 2> >( grep -v 'Error in \(pixScanForForeground\|boxClipToRectangle\)' >&2 ) # Uncoment this two lines to see how `termPreview` impact main bash environment. # diff <(echo "$vars") <(set|grep -v ^vars=) | # sed -nE 's/^< (.*)/\o33[2m-\1\o33[0m/p;s/^> (.*)/\o33[1m+\1\o33[0m/p' xdg-open /tmp/dummyScan.pdf