summaryrefslogtreecommitdiff
path: root/Master/texmf-dist/scripts
diff options
context:
space:
mode:
authorKarl Berry <karl@freefriends.org>2023-01-24 21:10:39 +0000
committerKarl Berry <karl@freefriends.org>2023-01-24 21:10:39 +0000
commit7817c1ae7ea32b28ffd3874e773b3f4e9d93a3c9 (patch)
tree8299a8007ef0c88188f82b7d230ed4400d1751ca /Master/texmf-dist/scripts
parentc6ae2f8f2336c3703c19eb2b49e76bf4c38ef48e (diff)
pagelayout (24jan23)
git-svn-id: svn://tug.org/texlive/trunk@65625 c570f23f-e606-0410-a88d-b1316a301751
Diffstat (limited to 'Master/texmf-dist/scripts')
-rwxr-xr-xMaster/texmf-dist/scripts/pagelayout/pagelayoutapi371
-rw-r--r--Master/texmf-dist/scripts/pagelayout/pagelayoutapi.1.md87
-rwxr-xr-xMaster/texmf-dist/scripts/pagelayout/textestvis506
-rw-r--r--Master/texmf-dist/scripts/pagelayout/textestvis.1.md55
4 files changed, 1019 insertions, 0 deletions
diff --git a/Master/texmf-dist/scripts/pagelayout/pagelayoutapi b/Master/texmf-dist/scripts/pagelayout/pagelayoutapi
new file mode 100755
index 00000000000..295beef2f48
--- /dev/null
+++ b/Master/texmf-dist/scripts/pagelayout/pagelayoutapi
@@ -0,0 +1,371 @@
+#!/bin/sh
+#
+# Copyright (c) 2022-2023 by Friedemann Bartels
+#
+# This file may be distributed and/or modified under the
+# conditions of the LaTeX Project Public License, either
+# version 1.3c of this license or (at your option) any later
+# version. The latest version of this license is in:
+#
+# http://www.latex-project.org/lppl.txt
+#
+# and version 1.3c or later is part of all distributions of
+# LaTeX version 2008/05/04 or later.
+#
+
+version=1.0.3
+
+_convert() {
+ turbo=$1
+ filename=$2
+ filenamecache=$3
+ fileextcache=$4
+ originalwidthsp=$5
+ originalheightsp=$6
+ originalcropleftsp=$7
+ originalcroprightsp=$8
+ originalcroptopsp=$9
+ originalcropbottomsp=${10}
+ density=${11}
+ displaywidth=${12}
+ displayheight=${13}
+ resizethreshold=${14}
+ unsharp=${15}
+ quality="${16}"
+
+ mkdir -p cache/"$filenamecache"
+ originalwidth=$( magick identify -ping -format %w "$filename" )
+ originalheight=$( magick identify -ping -format %h "$filename" )
+ width=$(( (10 * originalwidth * (originalwidthsp - originalcropleftsp - originalcroprightsp) / originalwidthsp + 5) / 10 ))
+ height=$(( (10 * originalheight * (originalheightsp - originalcroptopsp - originalcropbottomsp) / originalheightsp + 5) / 10 ))
+ cropleft=$(( (10 * originalwidth * originalcropleftsp / originalwidthsp + 5) / 10 ))
+ croptop=$(( (10 * originalheight * originalcroptopsp / originalheightsp + 5) / 10 ))
+ resizewidth=$(( (10 * displaywidth * 100 * density / 473628672 + 5) / 10 ))
+ if [ $(( resizewidth * resizethreshold / 100 )) -gt $width ]; then
+ resizewidth=$width
+ resizeheight=$height
+ else
+ resizeheight=$(( resizewidth * height / width + 1 ))
+ fi
+
+ x="x"
+ if [ $turbo = 1 ]; then
+ export MAGICK_THREAD_LIMIT=1
+ fi
+ if [ "$quality" != "" ]; then
+ quality="-quality $quality"
+ fi
+ if [ "$unsharp" != "" ]; then
+ unsharp="-unsharp $unsharp"
+ fi
+
+ magick "$filename" -crop $width$x$height+$cropleft+$croptop -resize $resizewidth$x$resizeheight $unsharp $quality "cache/$filenamecache/.g$fileextcache"
+ mv "cache/$filenamecache/.g$fileextcache" "cache/$filenamecache/g$fileextcache"
+}
+
+_startbatchprocess() {
+ success=0
+ for file in cache/*; do
+ if [ -f "$file" ]; then
+ success=1
+ name=$( basename "${file}" )
+ mv "$file" "cache/.$name" 2> /dev/null
+ if [ $? -eq 0 ]; then
+ line=$(head -n 1 "cache/.$name")
+
+ IFS="%"
+ set -- $line
+ IFS=" "
+ _convert 1 "$@"
+
+ rm "cache/.$name"
+ break
+ fi
+ fi
+ done
+
+ if [ $success -eq 1 ]; then
+ _startbatchprocess
+ fi
+}
+
+getwidth() {
+ if command -v magick >/dev/null 2>&1; then
+ filename=$1
+
+ originalwidth=$( magick identify -ping -format %w "$filename" )
+ echo $originalwidth
+ else
+ echo errormagicknotinstalled
+ fi
+}
+
+optimize() {
+ if command -v magick >/dev/null 2>&1; then
+ filenamecache=$2
+ fileextcache=$3
+
+ if [ -f cache/"$filenamecache" ]; then
+ mv cache/"$filenamecache" cache/."$filenamecache" 2> /dev/null
+ if [ $? -eq 0 ]; then
+ line=$(head -n 1 "cache/.$filenamecache")
+
+ IFS="%"
+ set -- $line
+ IFS=" "
+ _convert 1 "$@"
+
+ rm "cache/.$filenamecache"
+ else
+ while [ ! -f cache/"$filenamecache"/g"$fileextcache" ]; do
+ sleep 0.05
+ done
+ fi
+ elif [ -d cache/"$filenamecache" ]; then
+ while [ ! -f cache/"$filenamecache"/g"$fileextcache" ]; do
+ sleep 0.05
+ done
+ else
+ _convert 0 "$@"
+ fi
+
+ echo ok
+ else
+ echo errormagicknotinstalled
+ fi
+}
+
+makeshadow() {
+ if command -v magick >/dev/null 2>&1; then
+ filename=$1
+ stdDeviation=$2
+ opacity=$3
+ fill=$4
+ width=$5
+ height=$6
+ framewidth=$7
+ frameheight=$8
+ frameborder=$9
+ frameradius=${10}
+
+ [ ! -d cache ] && mkdir cache
+ mkdir -p cache/$filename
+ echo "<?xml version='1.0' encoding='UTF-8' standalone='no'?>
+ <svg
+ width='$width'
+ height='$height'
+ version='1.1'
+ xmlns='http://www.w3.org/2000/svg'
+ xmlns:svg='http://www.w3.org/2000/svg'>
+ <defs>
+ <filter
+ style='color-interpolation-filters:sRGB'
+ id='blur'
+ x='-4'
+ y='-4'
+ width='8'
+ height='8'>
+ <feGaussianBlur
+ stdDeviation='$stdDeviation' />
+ </filter>
+ </defs>
+ <rect
+ style='filter:url(#blur);opacity:$opacity;fill:$fill'
+ width='$framewidth'
+ height='$frameheight'
+ x='$frameborder'
+ y='$frameborder'
+ rx='$frameradius'
+ ry='$frameradius' />
+ </svg>
+ " > cache/$filename/s.svg
+ inkscape cache/$filename/s.svg --export-dpi=600 --export-filename cache/$filename/s.pdf
+ rm cache/$filename/s.svg
+
+ echo ok
+ else
+ echo errorinkscapenotinstalled
+ fi
+}
+
+import() {
+ if [ -d import ]; then
+ importdir=import
+ elif [ "$PAGELAYOUT_IMPORT_DIRECTORY" != "" ]; then
+ if [ -d "$PAGELAYOUT_IMPORT_DIRECTORY" ]; then
+ importdir="$PAGELAYOUT_IMPORT_DIRECTORY"
+ else
+ echo $PAGELAYOUT_IMPORT_DIRECTORY
+ exit
+ fi
+ else
+ echo errornoimportdir
+ exit
+ fi
+
+ defaultdirectory=$( echo $1 | sed -e 's/^{//g' | sed -e 's/}{.*//g' )
+ directorylist=$( echo $1 | sed -e 's/^{//g' | sed -e 's/}$//g' | sed -e 's/}{/$/g' )
+
+ for entry in "$importdir"/*.*
+ do
+ if [ -f "$entry" ]; then
+ name=$( basename "${entry}" )
+ if [ "$directorylist" != "" ]; then
+ echo "$directorylist" | tr '$' '\n' | while read directory; do
+ if [ -f "$directory$name" ]; then
+ mv "$entry" "$directory"
+ fi
+ done
+ fi
+ if [ -f "$entry" ]; then
+ if [ -d "$defaultdirectory" ]; then
+ mv "$entry" "$defaultdirectory"
+ else
+ mv "$entry" .
+ fi
+ fi
+ cachename=$( echo "$name" | sed -e 's/\.[a-zA-Z]*$//g' )
+ rm -rf cache/"$cachename"*
+ fi
+ done
+}
+
+startturbo() {
+ file="$(echo $1 | sed -e 's/\.pdflatex$//g')"
+ if [ -f $file.tex ]; then
+ tmpdir=$( mktemp -d 2>/dev/null )/pagelayoutturbo$( date "+%Y%m%d%H%M%S" )$RANDOM
+ mkdir $tmpdir
+
+ cp $file.tex $tmpdir/pagelayoutturbobatchoptimizexyz.tex
+
+ command=$(echo $(ps -p $PPID -o command ))
+ engine=$(echo $command | sed -e 's/^[A-Z ]*//g' | sed -e 's/ .*//g' | tr -d '\n')
+
+ case "$command" in
+ *-shell-escape*)
+ shellescape="--shell-escape"
+ ;;
+ esac
+
+ if [ "$engine" = "xelatex" ]; then
+ nopdf="-no-pdf"
+ fi
+
+ $engine $shellescape -interaction=batchmode $nopdf --output-directory $tmpdir $tmpdir/pagelayoutturbobatchoptimizexyz.tex > /dev/null
+
+ rm -rf $tmpdir
+ fi
+}
+
+batchoptimize() {
+ [ ! -d cache ] && mkdir cache
+
+ length=$(( $# / 15 ))
+ cores=$( nproc )
+ [ $cores -gt $length ] && cores=$length
+
+ while [ $length -gt 0 ]; do
+ filenamecache="$2"
+
+ if [ ! -e cache/"$filenamecache" ]; then
+ index=0
+ while [ $index -lt 15 ]; do
+ if [ $index -eq 0 ]; then
+ item="$1"
+ else
+ item="$item%$1"
+ fi
+
+ shift
+ index=$(( index + 1 ))
+ done
+
+ echo $item > cache/"$filenamecache"
+ else
+ shift 15
+ fi
+
+ length=$(( length - 1 ))
+ done
+
+ cores=$(( cores - 1 ))
+ while [ $cores -gt 0 ]; do
+ _startbatchprocess &
+ sleep 0.01
+ cores=$(( cores - 1 ))
+ done
+}
+
+usage() {
+ cat <<HELP_USAGE
+usage: pagelayoutapi getwidth % FILE
+ pagelayoutapi optimize % FILE % FILENAME % EXTENSION % ORIGINALWIDTH
+ % ORIGINALHEIGHT % CROPLEFT % CROPRIGHT % CROPTOP % CROPBOTTOM
+ % DENSITY % WIDTH % HEIGHT % DOWNSAMPLETHRESHOLD % UNSHARP % QUALITY
+ pagelayoutapi makeshadow % FILENAME % STANDARDDEVIATION % OPACITY % COLOR
+ % WIDTH % HEIGHT % FRAMEWIDTH % FRAMEHEIGHT % MARGIN % BORDERRADIUS
+ pagelayoutapi start [% import % [GRAPHICSPATH]] [% turbo % JOBNAME]
+ pagelayoutapi batchoptimize % BATCHLIST
+
+ --help Prints a help message.
+ --version Prints version information.
+HELP_USAGE
+}
+
+version() {
+ echo pagelayoutapi $version
+}
+
+input=$( echo "$@" | sed -e 's/\\//g' | sed -e 's/ %/%/g' | sed -e 's/% /%/g' )
+IFS="%"
+set -- $input
+IFS=" "
+
+case "$1" in
+getwidth)
+ shift
+
+ getwidth "$1"
+ ;;
+optimize)
+ shift
+
+ optimize "$@"
+ ;;
+makeshadow)
+ shift
+
+ makeshadow "$@"
+ ;;
+start)
+ shift
+
+ if [ "$1" = "import" ]; then
+ shift
+
+ import "$1"
+
+ shift
+ fi
+
+ if [ "$1" = "turbo" ]; then
+ shift
+
+ startturbo "$1" &
+ fi
+
+ echo ok
+ ;;
+batchoptimize)
+ shift
+
+ batchoptimize "$@" &
+
+ echo ok
+ ;;
+--version)
+ version
+ ;;
+--help)
+ usage
+esac
diff --git a/Master/texmf-dist/scripts/pagelayout/pagelayoutapi.1.md b/Master/texmf-dist/scripts/pagelayout/pagelayoutapi.1.md
new file mode 100644
index 00000000000..ab04ddd0b8e
--- /dev/null
+++ b/Master/texmf-dist/scripts/pagelayout/pagelayoutapi.1.md
@@ -0,0 +1,87 @@
+pagelayoutapi(1) -- API for the pagelayout LaTeX class
+====
+
+## SYNOPSIS
+
+`pagelayoutapi getwidth` % FILE
+
+`pagelayoutapi optimize` % FILE % FILENAME % EXTENSION % ORIGINALWIDTH % ORIGINALHEIGHT % CROPLEFT % CROPRIGHT % CROPTOP % CROPBOTTOM % DENSITY % WIDTH % HEIGHT % DOWNSAMPLETHRESHOLD % UNSHARP % QUALITY
+
+`pagelayoutapi makeshadow` % FILENAME % STANDARDDEVIATION % OPACITY % COLOR % WIDTH % HEIGHT % FRAMEWIDTH % FRAMEHEIGHT % MARGIN % BORDERRADIUS
+
+`pagelayoutapi start` [% import % [GRAPHICSPATH]] [% turbo % JOBNAME]
+
+`pagelayoutapi batchoptimize` % BATCHLIST
+
+## DESCRIPTION
+
+This script provides an API for the pagelayout LaTeX class.
+
+* `pagelayoutapi getwidth`:
+ Returns the width in pixels for the given image file.
+
+* `pagelayoutapi optimize`:
+ Creates a cropped, rezised, sharpend and compressed image and stores it in the cache directory.
+
+* `pagelayoutapi makeshadow`:
+ Creates a shadow image and stores it in the cache directory.
+
+* `pagelayoutapi start`:
+ The import parameter triggers the image import (see pagelayout-manual.pdf chapter "Image Optimization").
+ Running the command with the parameter turbo, where the jobname is the filename of the document, processes the document in a special batch mode, that creates a batch list and calls the command `pagelayoutapi batchoptimize`.
+
+* `pagelayoutapi batchoptimize`:
+ Optimizes multiple images in parallel.
+
+## OPTIONS
+
+* `--help`:
+ Prints a help message.
+* `--version`:
+ Prints version information.
+
+## PARAMETERS
+
+* FILE:
+ filename with extension (eg. IMG1234.JPEG)
+* FILENAME:
+ filename without extension (eg. IMG1234)
+* EXTENSION:
+ optimized file extension (.jpg|.png)
+* DENSITY:
+ density in ppi (eg. 72)
+* DOWNSAMPLETHRESHOLD:
+ downsample threshold (integer >= 100)
+* UNSHARP:
+ unsharp filter (eg. 2x1)
+* QUALITY:
+ quality (integer > 0, <= 100)
+* STANDARDDEVIATION:
+ standard deviation (decimal > 0.0)
+* OPACITY:
+ opacity (decimal >= 0.0, <= 1.0)
+* COLOR:
+ color string (eg. pink)
+* GRAPHICSPATH:
+ list of directories (eg. {images/}{tmp/})
+* JOBNAME:
+ LaTeX filename without extension (eg. my-document)
+* BATCHLIST:
+ a flat list of batch items where each batch item is a flat list of the 15 parameters required by the command \`pagelayoutapi optimize\`
+
+All other parameters are length dimensions. The command `pagelayoutapi optimize` expects integer values in the LaTeX unit sp (eg. 65536). The command `pagelayoutapi makeshadow` expects decimal values in a SVG compatible unit (eg. 420.0pt).
+
+## ENVIRONMENT
+
+`pagelayoutapi` requires ImageMagick 7.0 or later and Inkscape 1.0 or later.
+
+* `PAGELAYOUT_IMPORT_DIRECTORY`:
+ Defines a system wide import directory.
+
+## AUTHOR
+
+`pagelayoutapi` was written by Friedemann Bartels. <https://github.com/friedemannbartels>
+
+## LICENSE
+
+Copyright (c) 2022-2023 Friedemann Bartels. Free use of this software is granted under the terms of the LaTeX Project Public License version 1.3c or later.
diff --git a/Master/texmf-dist/scripts/pagelayout/textestvis b/Master/texmf-dist/scripts/pagelayout/textestvis
new file mode 100755
index 00000000000..36b3ff6cfbf
--- /dev/null
+++ b/Master/texmf-dist/scripts/pagelayout/textestvis
@@ -0,0 +1,506 @@
+#!/bin/sh
+#
+# Copyright (c) 2022-2023 by Friedemann Bartels
+#
+# This file may be distributed and/or modified under the
+# conditions of the LaTeX Project Public License, either
+# version 1.3c of this license or (at your option) any later
+# version. The latest version of this license is in:
+#
+# http://www.latex-project.org/lppl.txt
+#
+# and version 1.3c or later is part of all distributions of
+# LaTeX version 2008/05/04 or later.
+#
+
+version=1.0.3
+
+OK="$( tput setaf 2 )•$( tput sgr0 )"
+FAILED="$( tput setaf 1 )$( tput bold )x$( tput sgr0 )"
+NEW="$( tput setaf 3 )$( tput bold )+$( tput sgr0 )"
+REMOVED="$( tput setaf 5 )$( tput bold )-$( tput sgr0 )"
+ERROR="$( tput setaf 1 )$( tput bold )!$( tput sgr0 )"
+
+_playok() {
+ if [ -f /System/Library/Sounds/Glass.aiff ]; then
+ afplay /System/Library/Sounds/Glass.aiff >/dev/null 2>&1
+ fi
+}
+
+_playhm() {
+ if [ -f /System/Library/Sounds/Basso.aiff ]; then
+ afplay /System/Library/Sounds/Basso.aiff >/dev/null 2>&1
+ fi
+}
+
+_playerror() {
+ if [ -f /System/Library/Sounds/Sosumi.aiff ]; then
+ afplay /System/Library/Sounds/Sosumi.aiff >/dev/null 2>&1
+ fi
+}
+
+_gettime() {
+ if command -v gdate &> /dev/null; then
+ echo $( gdate +%s%3N )
+ else
+ if command -v date &> /dev/null; then
+ milliseconds=$( date +%3N )
+ if [ $milliseconds = 3N ]; then
+ echo $(( SECONDS * 1000 ))
+ else
+ echo $( date +%s%3N )
+ fi
+ else
+ echo $(( SECONDS * 1000 ))
+ fi
+ fi
+}
+
+_starttimer() {
+ starttime=$( _gettime )
+}
+
+_stoptimer() {
+ endtime=$( _gettime )
+
+ elapsedtime=$(( endtime - starttime ))
+ elapsedseconds=$(( elapsedtime / 1000 ))
+ elapsedmilliseconds=$(( elapsedtime % 1000 ))
+ elapsed=$elapsedseconds.$( printf "%03d" "$elapsedmilliseconds" )
+}
+
+test() {
+ _starttimer
+ tmpdir=$( mktemp -d 2>/dev/null )/textest$( date "+%Y%m%d%H%M%S" )$RANDOM
+ mkdir $tmpdir
+
+ if [ $3 = true ] && [ -d cache ]; then
+ rm -rf cache
+ fi
+
+ okcount=0
+ failedcount=0
+ newcount=0
+ removedcount=0
+ fileokcount=0
+ filefailedcount=0
+ defaultfiller=23
+
+ echo ""
+ for entry in *$5*.tex
+ do
+ if [ $entry != "*$5*.tex" ]; then
+ name=$( echo $entry | sed -e 's/\.tex$//g' )
+ if [ ${name:0:1} != _ ]; then
+ printf "$( tput sgr0 )$name"
+
+ runtwice=0
+ if [ "${name:$(( ${#name} - 2 )):2}" = "@2" ]; then
+ runtwice=1
+ fi
+
+ if [ "$4" != "xelatex" ]; then
+ name="$name.$4"
+ fi
+
+ passed=1
+ rm -f $name.failed.*
+
+ cp $entry $tmpdir/$name.tex
+ shell=""
+ if [ $1 = true ]; then
+ shell="--shell-escape"
+ fi
+ $4 -interaction=batchmode $shell --output-directory $tmpdir $tmpdir/$name.tex > /dev/null
+ if [ $? -eq 1 ]; then
+ passed=0
+ fi
+ if [ $runtwice -eq 1 ]; then
+ $4 -interaction=batchmode $shell --output-directory $tmpdir $tmpdir/$name.tex > /dev/null
+ if [ $? -eq 1 ]; then
+ passed=0
+ fi
+ fi
+
+ magick convert -density $2 -quiet $tmpdir/$name.pdf $tmpdir/$name.png
+ if [ -f "$tmpdir/$name.png" ]; then
+ mv $tmpdir/$name.png $tmpdir/$name-0.png
+ fi
+
+ if [ -f "$name.approved.pdf" ]; then
+ magick convert -density $2 -quiet $name.approved.pdf $tmpdir/$name.approved.png
+ if [ -f "$tmpdir/$name.approved.png" ]; then
+ mv $tmpdir/$name.approved.png $tmpdir/$name.approved-0.png
+ fi
+ fi
+
+ if [ $passed -eq 0 ]; then
+ filler=$(( defaultfiller - ${#name} - 2 ))
+ else
+ filler=$(( defaultfiller - ${#name} ))
+ fi
+ while [ 0 -le $filler ]; do
+ printf " "
+ filler=$(( filler - 1 ))
+ done
+ if [ $passed -eq 0 ]; then
+ printf " $ERROR"
+ fi
+
+ index=0
+ while [ -f "$tmpdir/$name-$index.png" ]; do
+ if [ -f "$tmpdir/$name.approved-$index.png" ]; then
+ changedpixels=$( magick compare -quiet -metric AE $tmpdir/$name-$index.png $tmpdir/$name.approved-$index.png null: 2>&1 )
+ if [ $changedpixels -eq 0 ]; then
+ okcount=$(( okcount + 1 ))
+ printf " $OK"
+ else
+ failedcount=$(( failedcount + 1 ))
+ passed=0
+ printf " $FAILED"
+ magick compare -quiet $tmpdir/$name-$index.png $tmpdir/$name.approved-$index.png $name.failed.$(( index + 1 )).png
+ if [ -f "$tmpdir/$name.pdf" ]; then
+ mv $tmpdir/$name.pdf $name.failed.pdf
+ fi
+ fi
+ else
+ newcount=$(( newcount + 1 ))
+ passed=0
+ printf " $NEW"
+ mv $tmpdir/$name-$index.png $name.failed.$(( index + 1 )).png
+ if [ -f "$tmpdir/$name.pdf" ]; then
+ mv $tmpdir/$name.pdf $name.failed.pdf
+ fi
+ fi
+ index=$(( index + 1 ))
+ if [ `expr $index % 20` -eq 0 ] && [ -f "$tmpdir/$name-$index.png" ]; then
+ echo ""
+ filler=$defaultfiller
+ while [ 0 -le $filler ]; do
+ printf " "
+ filler=$(( filler - 1 ))
+ done
+ fi
+ done
+ while [ -f "$tmpdir/$name.approved-$index.png" ]; do
+ removedcount=$(( removedcount + 1 ))
+ passed=0
+ printf " $REMOVED"
+ mv $tmpdir/$name.approved-$index.png $name.failed.$(( index + 1 )).png
+ if [ -f "$tmpdir/$name.pdf" ]; then
+ mv $tmpdir/$name.pdf $name.failed.pdf
+ fi
+ index=$(( index + 1 ))
+ if [ `expr $index % 20` -eq 0 ] && [ -f "$tmpdir/$name.approved-$index.png" ]; then
+ echo ""
+ filler=$defaultfiller
+ while [ 0 -le $filler ]; do
+ printf " "
+ filler=$(( filler - 1 ))
+ done
+ fi
+ done
+
+ if [ $passed -eq 1 ]; then
+ fileokcount=$(( fileokcount + 1 ))
+ else
+ filefailedcount=$(( filefailedcount + 1 ))
+ fi
+
+ echo ""
+ fi
+ fi
+ done
+
+ rm -rf $tmpdir
+ _stoptimer
+ seconds=$( printf "%.1f" "$elapsed" )
+
+ filecount=$(( fileokcount + filefailedcount ))
+
+ if [ $filecount -gt 0 ]; then
+ echo ""
+ fi
+ if [ $filecount -eq $fileokcount ]; then
+ if [ $filecount -eq 0 ]; then
+ _playhm &
+ echo "$( tput sgr0 )Tested 0 files 🧐."
+ else
+ _playok &
+ echo "$( tput sgr0 )Successfully tested $filecount files in $seconds seconds 🎉."
+ fi
+ echo ""
+ else
+ _playerror &
+ echo "$( tput sgr0 )Tested $filecount files in $seconds seconds."
+ echo ""
+
+ if [ $fileokcount -gt 0 ]; then
+ filler=$(( 6 + ${#filecount} - ${#fileokcount} ))
+ while [ 0 -le $filler ]; do
+ printf " "
+ filler=$(( filler - 1 ))
+ done
+ printf "$( tput setaf 2 )$fileokcount OK$( tput sgr0 )"
+ echo ""
+ fi
+ if [ $filefailedcount -gt 0 ]; then
+ filler=$(( 6 + ${#filecount} - ${#filefailedcount} ))
+ while [ 0 -le $filler ]; do
+ printf " "
+ filler=$(( filler - 1 ))
+ done
+ printf "$( tput setaf 1 )$filefailedcount Failed$( tput sgr0 )"
+ echo ""
+ fi
+ echo ""
+ exit 1
+ fi
+}
+
+approve() {
+ filecount=0
+
+ suffix=""
+ if [ "$1" != "xelatex" ]; then
+ suffix=".$1"
+ fi
+
+ echo ""
+ for entry in *$2*$suffix.failed.pdf
+ do
+ if [ $entry != "*$2*$suffix.failed.pdf" ]; then
+ filecount=$(( filecount + 1 ))
+ name=$( echo $entry | sed -e "s/$suffix.failed.pdf//" )
+ echo $name
+ rm -f $name$suffix.approved.pdf
+ if [ -f "$name.aux" ]; then
+ cp $name$suffix.failed.pdf $name$suffix.approved.pdf
+ else
+ mv $name$suffix.failed.pdf $name$suffix.approved.pdf
+ fi
+
+ rm -f $name$suffix.failed.*
+ fi
+ done
+
+ if [ $filecount -gt 0 ]; then
+ echo ""
+ fi
+ if [ $filecount -eq 1 ]; then
+ echo "Approved 1 file."
+ else
+ echo "Approved $filecount files."
+ fi
+ echo ""
+}
+
+compare() {
+ filea=$2
+ fileb=$3
+ tmpdir=$( mktemp -d 2>/dev/null)/textest$( date "+%Y%m%d%H%M%S" )$RANDOM
+ mkdir $tmpdir
+
+ namea=$( echo $filea | sed -e 's/\.\///g' | sed -e 's/\.pdf$//g' )
+ nameb=$( echo $fileb | sed -e 's/\.\///g' | sed -e 's/\.pdf$//g' )
+ basenamea=$(basename $namea)
+ basenameb=$(basename $nameb)
+
+ rm -f $namea.diff.*.png
+
+ spacer=""
+ if [ -f "$filea" ]; then
+ if [ -f "$fileb" ]; then
+ magick convert -density $1 -quiet $filea $tmpdir/$basenamea.png
+ if [ -f "$tmpdir/$basenamea.png" ]; then
+ mv $tmpdir/$basenamea.png $tmpdir/$basenamea-0.png
+ fi
+ magick convert -density $1 -quiet $fileb $tmpdir/$basenameb.png
+ if [ -f "$tmpdir/$basenameb.png" ]; then
+ mv $tmpdir/$basenameb.png $tmpdir/$basenameb-0.png
+ fi
+
+ index=0
+ while [ -f "$tmpdir/$basenamea-$index.png" ]; do
+ if [ -f "$tmpdir/$basenameb-$index.png" ]; then
+ changedpixels=$( magick compare -quiet -metric AE $tmpdir/$basenamea-$index.png $tmpdir/$basenameb-$index.png null: 2>&1 )
+ if [ $changedpixels -eq 0 ]; then
+ printf "$spacer$OK"
+ spacer=" "
+ else
+ printf "$spacer$FAILED"
+ spacer=" "
+ magick compare -quiet $tmpdir/$basenamea-$index.png $tmpdir/$basenameb-$index.png $namea.diff.$(( index + 1 )).png
+ fi
+ else
+ printf "$spacer$NEW"
+ spacer=" "
+ fi
+ index=$(( index + 1 ))
+ done
+ while [ -f "$tmpdir/$basenameb-$index.png" ]; do
+ printf "$spacer$REMOVED"
+ spacer=" "
+ index=$(( index + 1 ))
+ done
+ echo ""
+ else
+ echo "File $fileb not found."
+ fi
+ else
+ echo "File $filea not found."
+ fi
+
+ rm -rf $tmpdir
+}
+
+perf() {
+ if [ -f $4 ]; then
+ tmpdir=$( mktemp -d 2>/dev/null )/textestperf$( date "+%Y%m%d%H%M%S" )$RANDOM
+ mkdir $tmpdir
+ basename=$(basename $4)
+
+ if [ $1 = true ] && [ -d cache ]; then
+ rm -rf cache
+ fi
+
+ cp $4 $tmpdir
+ shell=""
+ if [ $2 = true ]; then
+ shell="--shell-escape"
+ fi
+ _starttimer
+ $3 -interaction=batchmode $shell --output-directory $tmpdir $tmpdir/$basename > /dev/null
+ _stoptimer
+ echo "$elapsed"
+
+ rm -rf $tmpdir
+ else
+ echo "File $4 not found."
+ fi
+}
+
+usage() {
+ cat <<HELP_USAGE
+usage: textest [-cs] [-e ENGINE] [-d DENSITY] [PATTERN]
+ textest approve [-e ENGINE] [PATTERN]
+ textest compare [-d DENSITY] FILE FILE
+ textest perf [-cs] [-e ENGINE] FILE
+
+ -c Clears cache.
+ -s Enables shell escape.
+ -d DENSITY Sets density in ppi (default 72).
+ -e ENGINE Sets latex engine (default xelatex).
+ --help Prints a help message.
+ --version Prints version information.
+HELP_USAGE
+}
+
+version() {
+ echo textest $version
+}
+
+case "$1" in
+approve)
+ shift
+ engine=xelatex
+
+ while getopts ":e:" arg; do
+ case "${arg}" in
+ e)
+ engine="${OPTARG}"
+ ;;
+ ?)
+ echo "Invalid option: -${OPTARG}."
+ ;;
+ esac
+ done
+
+ shift "$(( OPTIND - 1 ))"
+
+ approve $engine $1
+ ;;
+compare)
+ shift
+
+ density=72
+
+ while getopts ":d:" arg; do
+ case "${arg}" in
+ d)
+ density="${OPTARG}"
+ ;;
+ ?)
+ echo "Invalid option: -${OPTARG}."
+ ;;
+ esac
+ done
+
+ shift "$(( OPTIND - 1 ))"
+
+ compare $density $1 $2
+ ;;
+perf)
+ shift
+
+ shell=false
+ clear=false
+ engine=xelatex
+
+ while getopts ":e:sc" arg; do
+ case "${arg}" in
+ c)
+ clear=true
+ ;;
+ e)
+ engine="${OPTARG}"
+ ;;
+ s)
+ shell=true
+ ;;
+ ?)
+ echo "Invalid option: -${OPTARG}."
+ ;;
+ esac
+ done
+
+ shift "$(( OPTIND - 1 ))"
+
+ perf $clear $shell $engine $1
+ ;;
+--version)
+ version
+ ;;
+--help)
+ usage
+ ;;
+*)
+ density=72
+ shell=false
+ clear=false
+ engine=xelatex
+
+ while getopts ":e:d:sc" arg; do
+ case "${arg}" in
+ c)
+ clear=true
+ ;;
+ d)
+ density="${OPTARG}"
+ ;;
+ e)
+ engine="${OPTARG}"
+ ;;
+ s)
+ shell=true
+ ;;
+ ?)
+ echo "Invalid option: -${OPTARG}."
+ ;;
+ esac
+ done
+
+ shift "$(( OPTIND - 1 ))"
+
+ test $shell $density $clear $engine $1
+esac
diff --git a/Master/texmf-dist/scripts/pagelayout/textestvis.1.md b/Master/texmf-dist/scripts/pagelayout/textestvis.1.md
new file mode 100644
index 00000000000..00b475d9353
--- /dev/null
+++ b/Master/texmf-dist/scripts/pagelayout/textestvis.1.md
@@ -0,0 +1,55 @@
+textestvis(1) -- visual regression tests for LaTeX
+====
+
+## SYNOPSIS
+
+`textestvis` [-cs] [-e ENGINE] [-d DENSITY] [PATTERN]
+
+`textestvis approve` [-e ENGINE] [PATTERN]
+
+`textestvis compare` [-d DENSITY] FILE FILE
+
+`textestvis perf` [-cs] [-e ENGINE] FILE
+
+## DESCRIPTION
+
+Perform visual regression tests for LaTeX documents.
+
+* `textestvis`:
+ Creates PDF files for all tex files matching the pattern and performs a visual regression test. If no pattern is specified, all files in the current directory are tested except for files whose names begin with an underscore.
+
+* `textestvis approve`:
+ Replaces the reference files with the test files.
+
+* `textestvis compare`:
+ Creates a visual diff for two pdf files.
+
+* `textestvis perf`:
+ Measures the time for creating a pdf file from the given tex file.
+
+## OPTIONS
+
+* `-c`:
+ Clears cache.
+* `-s`:
+ Enables shell escape.
+* `-d` <DENSITY>:
+ Sets density in ppi (default 72).
+* `-e` <ENGINE>:
+ Sets latex engine (default xelatex).
+* `--help`:
+ Prints a help message.
+* `--version`:
+ Prints version information.
+
+## ENVIRONMENT
+
+`textestvis` requires ImageMagick 7.0 or later.
+
+## AUTHOR
+
+`textestvis` was written by Friedemann Bartels. <https://github.com/friedemannbartels>
+
+## LICENSE
+
+Copyright (c) 2022-2023 Friedemann Bartels. Free use of this software is granted under the terms of the LaTeX Project Public License version 1.3c or later.