Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #!/opt/local/bin/bash5
- #===============================================================================
- # PAGE/PANEL IMAGING/COMPOSITING LIBRARY
- #
- # This is a library of functions used to manipulate PNM images, the main goal
- # of which being to assemble panel images into a final page.
- #
- # This file must be sourced by scripts which create individual pages.
- #
- # The code here provides a complete page rendering system that composites
- # multiple image panels onto a configurable background. Panels can include
- # optional borders, transparency cutouts, and gutters to create dynamic
- # layouts. It supports reading various image formats including JPEG, JPEG-XL,
- # PNG, and NetPBM (PBM/PGM/PPM/PAM) files, and converts them into a uniform RGB
- # or RGBA stream for processing.
- #
- # Panel data is stored in a global associative array with position, size, and
- # image path. The pipeline automatically selects RGB or RGBA compositing based
- # on panel requirements, enabling support for transparent cutouts. Pages can
- # have solid, gradient, or image backgrounds, with images automatically scaled
- # or cropped to fit.
- #
- # The library provides functions to overlay panels, rectangles, labels,
- # watermarks, and page-edge shadows. Labels and watermarks are rendered from
- # fonts into masks and composited with specified alignment and opacity.
- # Shadows are dynamically generated according to background brightness,
- # simulating book folds on verso and recto pages.
- #
- # Page rendering is implemented as a streaming pipeline: background creation,
- # panel overlay, label placement, watermark application, shadow rendering, and
- # final output to disk. Recursive functions are used to sequentially overlay
- # panels. An alternative entry point allows adding overlays to a previously
- # rendered intermediate image. All parameters are validated prior to rendering
- # to ensure consistency and correctness.
- #===============================================================================
- # GLOBAL VARIABLES
- #
- # Externally defined constants (defined prior to sourcing this module):
- #
- # CONTENT_HEIGHT
- # CONTENT_WIDTH
- # Height and width of main content on page, in pixels, not counting panel
- # borders.
- #
- # MARGIN_BOTTOM
- # MARGIN_TOP
- # MARGIN_LEFT
- # MARGIN_RIGHT
- # Size of margin areas, in pixels.
- #
- # PANEL_BORDER
- # Thickness of border around panel images, in pixels.
- #
- # PANEL_BORDER_COLOR
- # Color of panel border, given as a hexadecimal color value.
- #
- # PANEL_GUTTER
- # Thickness of gutter between panel images, in pixels, not counting borders.
- #
- # PAGE_HEIGHT
- # PAGE_WIDTH
- # Height and width of full page, in pixels. This determines exactly the
- # dimensions of the final output image.
- #
- # PAGE_SHADOW_GAMMA
- # Gamma curve adjustment value determining the light falloff of rendered
- # shadows on the page. (Used only in verso/recto page configurations.)
- #
- # PAGE_SHADOW_WIDTH
- # Width of shadow in pixels, counting from the center of the page spread.
- # (Used only in verso/recto page configurations.)
- #
- # PAGE_LABEL
- # Text to be displayed beneath the panels, either centered or flush left or
- # flush right. Can be the empty string, but is typically a page number.
- #
- # PAGE_LABEL_COLOR
- # Color of page label text, given as a hexadecimal color value.
- #
- # PAGE_LABEL_GUTTER
- # Height, in pixels, of the gap between the page content and the page label.
- #
- # PAGE_LABEL_HEIGHT
- # Height, in pixels, of the area allocated for the page label.
- #
- # PAGE_LABEL_FONT_SIZE
- # Height of rendered page label, in pixels (DOUBLE-CHECK THIS!), spanning
- # the entire em height of the typeface (ALSO DOUBLE-CHECK THIS!).
- #
- # PAGE_LABEL_FONT
- # PostScript-compatible typeface name to be used in rendering the page
- # label.
- #
- # TEXT_WATERMARK
- # Text to be displayed vertically along the sides of pages to indicate a
- # draft or special copy for certain eyes only. (Normally left blank.)
- # The color is assumed to be the same as PANEL_BORDER, but at 25% opacity.
- #
- # TEXT_WATERMARK_GUTTER
- # Width, in pixels, of the gap between the page content and the text
- # watermark (which is rendered vertically).
- #
- # TEXT_WATERMARK_HEIGHT
- # Width, in pixels, of the area allocated for the text watermark (which is
- # rendered vertically).
- #
- # TEXT_WATERMARK_FONT_SIZE
- # Size of the rendered text watermark, in pixels (DOUBLE-CHECK THIS!).
- #
- # TEXT_WATERMARK_FONT
- # PostScript-compatible typeface name to be used in rendering the text
- # watermark.
- #
- # SPLASH_WATERMARK
- # Text to be displayed diagonally across the whole page, very obnoxiously.
- # (Normally left blank.) The color is assumed to be white, but at 15%
- # opacity.
- #
- # SPLASH_WATERMARK_FONT_SIZE
- # Size of the rendered full-page splash watermark, in pixels (DOUBLE-CHECK
- # THIS!).
- #
- # SPLASH_WATERMARK_FONT
- # PostScript-compatible typeface name to be used in rendering the full-page
- # splash watermark.
- #
- # FONTS_DIR [currently unused]
- # Directory containing TTF files to use in rendering text.
- #
- # Interally defined:
- #
- # BACKGROUND_COLOR1
- # BACKGROUND_COLOR2
- # Defaults to empty string. Optionally contains a hexadecimal color code.
- # If no colors are defined, a solid white backdrop is applied. If one
- # color is defined, a solid backdrop of that color is applied. If two
- # colors are defined, a linear gradient is applied vertically, with the
- # first color at the top and the second color at the bottom.
- #
- # BACKGROUND_IMAGE_PATH
- # Defaults to empty string. Optionally contains path to image file.
- #
- # PANEL_LIST
- # Array of panels on the page. Starts empty and is grown as panels are
- # added.
- #
- # PANEL_COUNT
- # Count of panels on the page. Starts at zero and counts upward as panels
- # are added.
- #
- # PANEL_FIELDS
- # Constant array of field names for panel attributes.
- #
- # PANEL_RENDERING_PIPELINE
- # String determining which rendering pipeline (RGB or RGBA) is to be used
- # when compositing the page. Starts as "rgb" and may change to "rgba" if
- # a more complex rendering pipeline is needed, for example overlapping
- # panels.
- #-------------------------------------------------------------------------------
- #===============================================================================
- # ERROR HANDLING
- #-------------------------------------------------------------------------------
- error_message()
- {
- printf "%s: %s\n" $0 "$*" 1>&2
- false
- }
- #-------------------------------------------------------------------------------
- error_message_exit()
- {
- error_message "$@"
- case $- in
- *i*) false ;; # Interactive shell context
- *) exit 1 ;; # Non-interactive script context
- esac
- }
- #-------------------------------------------------------------------------------
- suppress_stderr()
- {
- local SUPPRESS="$1"; shift
- "$@" 2> >(grep -v "$SUPPRESS" 1>&2)
- }
- #===============================================================================
- # PARAMETER TESTS
- #-------------------------------------------------------------------------------
- param_general_validate()
- {
- local PARAM_NAME="$1"
- local PARAM_VALUE="$2"
- local REGEX_MATCH="$3"
- local TYPE="$4"
- if [[ ! "$PARAM_VALUE" =~ $REGEX_MATCH ]]; then
- error_message "${PARAM_NAME}: ${PARAM_VALUE}: Invalid value; must be $TYPE"
- return 1
- fi
- return 0
- }
- #-------------------------------------------------------------------------------
- param_integer_validate()
- {
- local PARAM_NAME="$1"
- local PARAM_VALUE="$2"
- param_general_validate \
- "$PARAM_NAME" "$PARAM_VALUE" '^-?(0|[1-9][0-9]*)$' "integer"
- }
- #-------------------------------------------------------------------------------
- param_nonnegative_integer_validate()
- {
- local PARAM_NAME="$1"
- local PARAM_VALUE="$2"
- param_general_validate \
- "$PARAM_NAME" "$PARAM_VALUE" '^(0|[1-9][0-9]*)$' "non-negative integer"
- }
- #-------------------------------------------------------------------------------
- param_positive_integer_validate()
- {
- local PARAM_NAME="$1"
- local PARAM_VALUE="$2"
- param_general_validate \
- "$PARAM_NAME" "$PARAM_VALUE" '^[1-9][0-9]*$' "positive integer"
- }
- #-------------------------------------------------------------------------------
- param_real_validate()
- {
- local PARAM_NAME="$1"
- local PARAM_VALUE="$2"
- param_general_validate \
- "$PARAM_NAME" "$PARAM_VALUE" \
- '^-?([0-9]+|[0-9]*\.[0-9]+|[0-9]+\.[0-9]*)$' \
- "real number"
- }
- #===============================================================================
- # FILENAME TESTS
- #-------------------------------------------------------------------------------
- image_path_validate()
- {
- local IMAGE_PATH="$1"
- if [[ ! -e "$IMAGE_PATH" ]]; then
- error_message "${IMAGE_PATH}: Not found"
- return 1
- fi
- case "${IMAGE_PATH##*.}" in
- jpg|jpeg)
- ;;
- jxl|jpegxl)
- ;;
- png)
- ;;
- pbm|pgm|ppm|pnm|pam)
- ;;
- *)
- error_message_exit "${IMAGE_PATH}: Unsupported image file extension"
- return 1
- ;;
- esac
- return 0
- }
- #-------------------------------------------------------------------------------
- image_filename_has_borderless_directive()
- {
- local IMAGE_PATH="$1"
- local REGEX_MATCH_BORDERLESS_DIRECTIVE='\[borderless\]'
- [[ "${IMAGE_PATH##*/}" =~ $REGEX_MATCH_BORDERLESS_DIRECTIVE ]]
- }
- #-------------------------------------------------------------------------------
- image_filename_has_cutout_directive()
- {
- local IMAGE_PATH="$1"
- local REGEX_MATCH_CUTOUT_DIRECTIVE='\^'
- [[ "${IMAGE_PATH##*/}" =~ $REGEX_MATCH_CUTOUT_DIRECTIVE ]]
- }
- #===============================================================================
- # IMAGE CREATION (ARBITRARY SIZE)
- #-------------------------------------------------------------------------------
- image_rgb_make_color()
- {
- local WIDTH=$1
- local HEIGHT=$2
- local COLOR="$3"
- ppmmake -maxval=255 "$COLOR" $WIDTH $HEIGHT
- }
- #-------------------------------------------------------------------------------
- image_rgb_make_color_gradient()
- {
- local WIDTH=$1
- local HEIGHT=$2
- local COLOR_TOP="$3"
- local COLOR_BOTTOM="$4"
- pamgradient -maxval=255 \
- "$COLOR_TOP" "$COLOR_TOP" "$COLOR_BOTTOM" "$COLOR_BOTTOM" \
- $WIDTH $HEIGHT
- }
- #-------------------------------------------------------------------------------
- image_rgba_make_color()
- {
- local WIDTH=$1
- local HEIGHT=$2
- local COLOR="$3"
- local OPACITY=$4
- pamstack \
- -quiet -tupletype RGB_ALPHA \
- <( ppmmake -maxval=255 "$COLOR" $WIDTH $HEIGHT ) \
- <( pgmmake -maxval=255 $OPACITY $WIDTH $HEIGHT )
- }
- #-------------------------------------------------------------------------------
- image_rgba_make_transparent()
- {
- local WIDTH=$1
- local HEIGHT=$2
- image_rgba_make_color $WIDTH $HEIGHT "black" 0
- }
- #-------------------------------------------------------------------------------
- image_rgba_make_opaque_black()
- {
- local WIDTH=$1
- local HEIGHT=$2
- image_rgba_make_color $WIDTH $HEIGHT "black" 1
- }
- #-------------------------------------------------------------------------------
- image_rgba_make_opaque_white()
- {
- local WIDTH=$1
- local HEIGHT=$2
- image_rgba_make_color $WIDTH $HEIGHT "white" 1
- }
- #===============================================================================
- # IMAGE OUTPUT
- #
- # Write stream to final output format on disk.
- #-------------------------------------------------------------------------------
- image_write()
- {
- local IMAGE_PATH="$1"
- if [[ "$IMAGE_PATH" == "" ]] || [[ "$IMAGE_PATH" == "-" ]]; then
- cat
- else
- case "${IMAGE_PATH##*.}" in
- jpg|jpeg)
- pnmtojpeg -dct=float -optimize -quality 100 >"$IMAGE_PATH"
- ;;
- jxl|jpegxl)
- ppmtojxl -q 100 -e 3 >"$IMAGE_PATH"
- ;;
- png)
- pamtopng >"$IMAGE_PATH"
- ;;
- ppm)
- ppmtoppm >"$IMAGE_PATH"
- ;;
- pnm)
- pnmtopnm >"$IMAGE_PATH"
- ;;
- pam)
- pamtopam >"$IMAGE_PATH"
- ;;
- *)
- error_message_exit "${IMAGE_PATH}: Unsupported image file extension"
- return
- ;;
- esac
- fi
- }
- #===============================================================================
- # IMAGE INPUT
- #
- # Read image files from disk and produce input stream.
- #-------------------------------------------------------------------------------
- image_dimensions()
- {
- local IMAGE_PATH="$1"
- image_rgb_borderless "$IMAGE_PATH" | head -2 | tail -1
- }
- #-------------------------------------------------------------------------------
- image_rgb_borderless()
- {
- local IMAGE_PATH="$1"
- local WIDTH=$2 # (unused)
- local HEIGHT=$3 # (unused)
- case "${IMAGE_PATH##*.}" in
- jpg|jpeg)
- jpegtopnm -quiet -dct float "$IMAGE_PATH"
- ;;
- jxl|jpegxl)
- jxltoppm "$IMAGE_PATH"
- ;;
- png)
- pngtopam "$IMAGE_PATH"
- ;;
- pbm|pgm|ppm|pnm|pam)
- cat "$IMAGE_PATH"
- ;;
- *)
- error_message_exit "${IMAGE_PATH}: Unsupported image file extension"
- return
- ;;
- esac
- }
- #-------------------------------------------------------------------------------
- image_rgb_bordered()
- {
- local IMAGE_PATH="$1"
- local WIDTH=$2
- local HEIGHT=$3
- local PNMPAD_COLOR=
- case "$PANEL_BORDER_COLOR" in
- "#000000"|"#000"|"#0"|black) PNMPAD_COLOR=black ;;
- "#FFFFFF"|"#FFF"|"#F"|white) PNMPAD_COLOR=white ;;
- *) PNMPAD_COLOR= ;;
- esac
- if [[ "$PNMPAD_COLOR" != "" ]]; then
- pnmpad \
- -$PNMPAD_COLOR \
- -left=$PANEL_BORDER -right=$PANEL_BORDER \
- -top=$PANEL_BORDER -bottom=$PANEL_BORDER \
- <( image_rgb_borderless "$IMAGE_PATH" $WIDTH $HEIGHT )
- else
- pamcomp \
- -align=center -valign=middle \
- <( image_rgb_borderless "$IMAGE_PATH" $WIDTH $HEIGHT ) \
- <( ppmmake \
- -maxval 255 \
- "$PANEL_BORDER_COLOR" \
- $(( WIDTH + (PANEL_BORDER * 2) )) \
- $(( HEIGHT + (PANEL_BORDER * 2) )) \
- )
- fi
- }
- #-------------------------------------------------------------------------------
- image_rgba_bordered()
- {
- local IMAGE_PATH="$1"
- local WIDTH=$2
- local HEIGHT=$3
- pamstack \
- -quiet -tupletype RGB_ALPHA \
- <( image_rgb_bordered "$IMAGE_PATH" \
- $WIDTH $HEIGHT ) \
- <( pgmmake -maxval=255 1 \
- $(( WIDTH + (PANEL_BORDER * 2) )) \
- $(( HEIGHT + (PANEL_BORDER * 2) )) \
- )
- }
- #-------------------------------------------------------------------------------
- image_rgb()
- {
- local IMAGE_PATH="$1"
- local WIDTH=$2
- local HEIGHT=$3
- if image_filename_has_borderless_directive "$IMAGE_PATH"; then
- image_rgb_borderless "$IMAGE_PATH" $WIDTH $HEIGHT
- else
- image_rgb_bordered "$IMAGE_PATH" $WIDTH $HEIGHT
- fi
- }
- #===============================================================================
- # MAINTAIN PANEL LIST
- #-------------------------------------------------------------------------------
- BACKGROUND_IMAGE_PATH=
- BACKGROUND_COLOR1=
- BACKGROUND_COLOR2=
- declare -a PANEL_FIELDS=( \
- LEFT \
- TOP \
- WIDTH \
- HEIGHT \
- IMAGE_PATH \
- )
- declare -A PANEL_LIST=
- declare -i PANEL_COUNT=
- PANEL_RENDERING_PIPELINE=
- #-------------------------------------------------------------------------------
- panel_list_init()
- {
- BACKGROUND_IMAGE_PATH=
- BACKGROUND_COLOR1=white
- BACKGROUND_COLOR2=
- PANEL_LIST=()
- PANEL_COUNT=0
- PANEL_RENDERING_PIPELINE=rgb
- }
- #-------------------------------------------------------------------------------
- panel_list_set_panel_border_color()
- {
- PANEL_BORDER_COLOR="$1"
- }
- #-------------------------------------------------------------------------------
- panel_list_set_page_label_color()
- {
- PAGE_LABEL_COLOR="$1"
- }
- #-------------------------------------------------------------------------------
- panel_list_set_background_color()
- {
- BACKGROUND_COLOR1="$1"
- BACKGROUND_COLOR2="$2"
- }
- #-------------------------------------------------------------------------------
- panel_list_set_background_image()
- {
- BACKGROUND_IMAGE_PATH="$1"
- }
- #-------------------------------------------------------------------------------
- panel_list_set_background()
- {
- local BACKGROUND1="$1"
- local BACKGROUND2="$2"
- case "$BACKGROUND1" in
- "")
- ;;
- \#*)
- panel_list_set_background_color "$BACKGROUND1" "$BACKGROUND2"
- ;;
- *.jpg|*.jpeg|*.jxl|*.jpegxl|*.png|*.pbm|*.pgm|*.ppm|*.pnm|*.pam)
- panel_list_set_background_image "$BACKGROUND1"
- ;;
- *)
- error_message_exit "${BACKGROUND1}: Unsupported background type"
- ;;
- esac
- }
- #-------------------------------------------------------------------------------
- panel_list_append()
- {
- local LEFT=$1
- local TOP=$2
- local WIDTH=$3
- local HEIGHT=$4
- local IMAGE_PATH="$5"
- if image_filename_has_cutout_directive "$IMAGE_PATH"; then
- PANEL_RENDERING_PIPELINE=rgba
- fi
- local FIELD
- for FIELD in "${PANEL_FIELDS[@]}"; do
- eval "PANEL_LIST[$PANEL_COUNT,$FIELD]"='$'"$FIELD"
- done
- PANEL_COUNT+=1
- }
- #-------------------------------------------------------------------------------
- # Fetch a single panel from the panel list
- #
- # ENTRY: $1 specifies a panel by index.
- # PANEL is an associative array in the dynamic scope.
- #
- # EXIT: PANEL is populated with data, replacing any previous data.
- panel_list_fetch_by_index()
- {
- local PANEL_INDEX=$1
- if (( PANEL_INDEX < 0 )) || (( PANEL_INDEX >= PANEL_COUNT )); then
- error_message_exit "Invalid panel index $PANEL_INDEX"
- return
- fi
- PANEL=()
- local FIELD
- for FIELD in "${PANEL_FIELDS[@]}"; do
- PANEL[$FIELD]="${PANEL_LIST[$PANEL_INDEX,$FIELD]}"
- done
- }
- #-------------------------------------------------------------------------------
- panel_list_dump_info_by_index()
- {
- local PANEL_INDEX=$1
- local -A PANEL
- panel_list_fetch_by_index $PANEL_INDEX
- local FIELD
- for FIELD in "${PANEL_FIELDS[@]}"; do
- echo "Panel $PANEL_INDEX $FIELD = ${PANEL_LIST[$PANEL_INDEX,$FIELD]}"
- done
- }
- #-------------------------------------------------------------------------------
- panel_list_dump_all_info()
- {
- local PANEL_INDEX
- for (( PANEL_INDEX = 0; PANEL_INDEX < PANEL_COUNT; PANEL_INDEX++ )); do
- panel_list_dump_info_by_index $PANEL_INDEX
- done
- }
- #-------------------------------------------------------------------------------
- panel_list_validate()
- {
- local -i ERROR_COUNT=0
- local -i PANEL_INDEX
- local -A PANEL
- if [[ -n "$BACKGROUND_IMAGE_PATH" ]]; then
- image_path_validate "$BACKGROUND_IMAGE_PATH"; ERROR_COUNT+=$?
- fi
- for (( PANEL_INDEX = 0; PANEL_INDEX < PANEL_COUNT; PANEL_INDEX++ )); do
- panel_list_fetch_by_index $PANEL_INDEX
- image_path_validate "${PANEL[IMAGE_PATH]}";
- ERROR_COUNT+=$?
- param_integer_validate LEFT ${PANEL[LEFT]}
- ERROR_COUNT+=$?
- param_integer_validate TOP ${PANEL[TOP]}
- ERROR_COUNT+=$?
- param_positive_integer_validate WIDTH ${PANEL[WIDTH]}
- ERROR_COUNT+=$?
- param_positive_integer_validate HEIGHT ${PANEL[HEIGHT]}
- ERROR_COUNT+=$?
- done
- return $ERROR_COUNT
- }
- #===============================================================================
- # PAGE INITIALIZATION
- #-------------------------------------------------------------------------------
- page_rgba_make_transparent()
- {
- image_rgba_make_transparent $PAGE_WIDTH $PAGE_HEIGHT
- }
- #-------------------------------------------------------------------------------
- page_rgb_make_color()
- {
- local COLOR="$1"
- image_rgb_make_color $PAGE_WIDTH $PAGE_HEIGHT "$COLOR"
- }
- #-------------------------------------------------------------------------------
- page_rgb_make_color_gradient()
- {
- local COLOR_TOP="$1"
- local COLOR_BOTTOM="$2"
- image_rgb_make_color_gradient \
- $PAGE_WIDTH $PAGE_HEIGHT \
- "$COLOR_TOP" "$COLOR_BOTTOM"
- }
- #-------------------------------------------------------------------------------
- page_rgb_with_image()
- {
- local IMAGE_PATH="$1"
- local IMAGE_WIDTH
- local IMAGE_HEIGHT
- read IMAGE_WIDTH IMAGE_HEIGHT <<< $(image_dimensions "$IMAGE_PATH")
- if (( IMAGE_WIDTH == PAGE_WIDTH )) && \
- (( IMAGE_HEIGHT == PAGE_HEIGHT )); then
- # If the image is exactly the same dimensions as the page, then simply
- # read it without scaling.
- image_rgb_borderless "$IMAGE_PATH"
- elif (( IMAGE_WIDTH >= PAGE_WIDTH )) && \
- (( IMAGE_HEIGHT >= PAGE_HEIGHT )); then
- # Else, if the image is large enough to completely cover the page, then read
- # the image and crop it evenly to the page size. (Any or all edges could be
- # cropped here.)
- image_rgb_borderless "$IMAGE_PATH" \
- | pamcomp -align=center -valign=middle - <( page_rgb_make_color black )
- else
- # Otherwise, read the image and upscale it just enough to fill the entire
- # page, preserving its aspect ratio, and then crop it evenly to the page
- # dimensions. (Either the top and bottom edges or the left and right edges
- # will be cropped here, but not both.)
- image_rgb_borderless "$IMAGE_PATH" \
- | pamscale -filter=cubic -xyfill $PAGE_WIDTH $PAGE_HEIGHT \
- | pamcomp -align=center -valign=middle - <( page_rgb_make_color black )
- fi
- }
- #===============================================================================
- # PAGE MASKING [RGBA]
- #-------------------------------------------------------------------------------
- page_mask_out_rect()
- {
- local LEFT=$1
- local TOP=$2
- local WIDTH=$3
- local HEIGHT=$4
- pamarith \
- -and \
- - \
- <( pgmmake -maxval=255 1 $PAGE_WIDTH $PAGE_HEIGHT \
- | pamcomp -xoff=$LEFT -yoff=$TOP \
- <( pgmmake -maxval=255 0 $WIDTH $HEIGHT ) \
- - \
- )
- }
- #===============================================================================
- # PAGE OVERLAYING [RGB, RGBA]
- #-------------------------------------------------------------------------------
- # Overlay an arbitrary RGB stream [RGB, RGBA]
- page_overlay_rgb_stream()
- {
- local LEFT=$1
- local TOP=$2
- local STREAM="$3"
- pamcomp \
- -xoff=$LEFT \
- -yoff=$TOP \
- "$STREAM" \
- -
- }
- #-------------------------------------------------------------------------------
- # Overlay an arbitrary RGBA stream [RGB, RGBA]
- page_overlay_rgba_stream()
- {
- local LEFT=$1
- local TOP=$2
- local STREAM="$3"
- local ALPHA_STREAM="$4"
- local OPACITY="$5"
- pamcomp \
- -xoff=$LEFT \
- -yoff=$TOP \
- -alpha="$ALPHA_STREAM" \
- -opacity="$OPACITY" \
- "$STREAM" \
- -
- }
- #-------------------------------------------------------------------------------
- # Overlay a solid opaque rectangle [RGB, RGBA]
- page_overlay_rgb_rect()
- {
- local LEFT=$1
- local TOP=$2
- local WIDTH=$3
- local HEIGHT=$4
- local COLOR="$5"
- page_overlay_rgb_stream $LEFT $TOP \
- <( image_rgb_make_color $WIDTH $HEIGHT "$COLOR" )
- }
- #-------------------------------------------------------------------------------
- # Overlay panel [RGB]
- page_rgb_overlay_panel()
- {
- local LEFT=${PANEL[LEFT]}
- local TOP=${PANEL[TOP]}
- if ! image_filename_has_borderless_directive "${PANEL[IMAGE_PATH]}"; then
- LEFT=$((LEFT - PANEL_BORDER))
- TOP=$((TOP - PANEL_BORDER))
- fi
- pamcomp -xoff=$LEFT -yoff=$TOP \
- <( image_rgb \
- "${PANEL[IMAGE_PATH]}" \
- ${PANEL[WIDTH]} \
- ${PANEL[HEIGHT]} \
- ) \
- -
- }
- #-------------------------------------------------------------------------------
- # Overlay panel without gutter cutout [RGBA]
- page_rgba_overlay_panel_without_gutter_cutout()
- {
- pamcomp \
- -mixtransparency \
- -xoff=$(( PANEL[LEFT] - PANEL_BORDER )) \
- -yoff=$(( PANEL[TOP] - PANEL_BORDER )) \
- <( image_rgba_bordered \
- "${PANEL[IMAGE_PATH]}" \
- ${PANEL[WIDTH]} \
- ${PANEL[HEIGHT]} \
- ) \
- -
- }
- #-------------------------------------------------------------------------------
- # Overlay panel with gutter cutout [RGBA]
- #
- # NOTE: It does *not* work to optimize this to throw down a solid opaque gutter
- # if the page background is simply a plain color. The problem is not the gutter
- # per se but the constricted border of the affected surrounding images.
- page_rgba_overlay_panel_with_gutter_cutout()
- {
- page_overlay_rgb_rect \
- $(( PANEL[LEFT] - (2*PANEL_BORDER) - PANEL_GUTTER )) \
- $(( PANEL[TOP] - (2*PANEL_BORDER) - PANEL_GUTTER )) \
- $(( PANEL[WIDTH] + (4*PANEL_BORDER) + (2*PANEL_GUTTER) )) \
- $(( PANEL[HEIGHT] + (4*PANEL_BORDER) + (2*PANEL_GUTTER) )) \
- black \
- | page_mask_out_rect \
- $(( PANEL[LEFT] - PANEL_BORDER - PANEL_GUTTER )) \
- $(( PANEL[TOP] - PANEL_BORDER - PANEL_GUTTER )) \
- $(( PANEL[WIDTH] + 2 * (PANEL_BORDER + PANEL_GUTTER) )) \
- $(( PANEL[HEIGHT] + 2 * (PANEL_BORDER + PANEL_GUTTER) )) \
- | page_rgba_overlay_panel_without_gutter_cutout
- }
- #-------------------------------------------------------------------------------
- # Overlay panel [RGBA]
- page_rgba_overlay_panel()
- {
- if image_filename_has_cutout_directive "${PANEL[IMAGE_PATH]}"; then
- page_rgba_overlay_panel_with_gutter_cutout
- else
- page_rgba_overlay_panel_without_gutter_cutout
- fi
- }
- #-------------------------------------------------------------------------------
- # Overlay panel [RGB, RGBA]
- page_overlay_panel()
- {
- case $PANEL_RENDERING_PIPELINE in
- rgb) page_rgb_overlay_panel ;;
- rgba) page_rgba_overlay_panel ;;
- esac
- }
- #===============================================================================
- # PAGE UNDERLAYING [RGBA]
- #-------------------------------------------------------------------------------
- page_rgba_underlay_rgb_stream()
- {
- local STREAM="$1"
- pamcomp -mixtransparency - "$STREAM"
- }
- #-------------------------------------------------------------------------------
- page_rgba_underlay_rgb_color()
- {
- local COLOR="$1"
- page_rgba_underlay_rgb_stream <( page_rgb_make_color "$COLOR" )
- }
- #-------------------------------------------------------------------------------
- page_rgba_underlay_rgb_image()
- {
- local IMAGE_PATH="$1"
- page_rgba_underlay_rgb_stream <( page_rgb_with_image "$IMAGE_PATH" )
- }
- #===============================================================================
- # PAGE LABELING [RGB]
- #
- # After all panel compositing is complete, and before any shadow is applied, the
- # page label is created and applied.
- #-------------------------------------------------------------------------------
- page_label_mask()
- {
- local FOLIUM="$1"
- local TEXT="$2"
- case "$FOLIUM" in
- verso) local HALIGN=0.0 ;;
- recto) local HALIGN=1.0 ;;
- solo) local HALIGN=0.5 ;;
- *) error_message_exit "${FOLIUM}: Invalid side"; return ;;
- esac
- DOWNSAMPLE=16
- # magick convert \
- # -trim \
- # -background white \
- # -fill black \
- # -font "$FONTS_DIR/$PAGE_LABEL_FONT.ttf" \
- # -pointsize $PAGE_LABEL_FONT_SIZE \
- # -density $((72*DOWNSAMPLE)) \
- # label:"$TEXT" \
- # pbm:- \
- pbmtextps \
- -font $PAGE_LABEL_FONT \
- -fontsize $PAGE_LABEL_FONT_SIZE \
- -resolution $((72*DOWNSAMPLE)) \
- -crop \
- "$TEXT" \
- | pnminvert \
- | pgmtopgm \
- | pamscale -quiet -linear -reduce $DOWNSAMPLE \
- | pnmpad -black \
- -halign=$HALIGN \
- -valign=0.5 \
- -width $((CONTENT_WIDTH + (PANEL_BORDER * 4))) \
- -height $PAGE_LABEL_HEIGHT
- }
- #-------------------------------------------------------------------------------
- page_overlay_label()
- {
- local FOLIUM="$1"
- local TEXT="$2"
- if [[ "$TEXT" == "" ]]; then
- cat
- else
- page_overlay_rgba_stream \
- $((MARGIN_LEFT - (PANEL_BORDER * 2))) \
- $(( MARGIN_TOP + CONTENT_HEIGHT + PAGE_LABEL_GUTTER )) \
- <( image_rgb_make_color \
- $((CONTENT_WIDTH + (PANEL_BORDER * 4))) \
- $PAGE_LABEL_HEIGHT "$PAGE_LABEL_COLOR" ) \
- <( page_label_mask \
- "$FOLIUM" "$TEXT" ) \
- 1.0
- fi
- }
- #===============================================================================
- # PAGE WATERMARKING [RGB]
- #
- # After all panel compositing is complete, and before any shadow is applied, an
- # optional page-size watermark is created and applied.
- #-------------------------------------------------------------------------------
- page_watermark_mask()
- {
- local FOLIUM="$1"
- local TEXT="$2"
- case "$FOLIUM" in
- verso) local ROTATE="+90"; local HALIGN=1.0; local VALIGN=0.0 ;;
- recto) local ROTATE="-90"; local HALIGN=0.0; local VALIGN=1.0 ;;
- solo) local ROTATE="0" ; local HALIGN=0.5; local VALIGN=0.5 ;;
- *) error_message_exit "${FOLIUM}: Invalid side"; return ;;
- esac
- DOWNSAMPLE=16
- pbmtextps \
- -font $TEXT_WATERMARK_FONT \
- -fontsize $TEXT_WATERMARK_FONT_SIZE \
- -resolution $((72*DOWNSAMPLE)) \
- -crop \
- "$TEXT" \
- | pnminvert \
- | pgmtopgm \
- | pnmrotate $ROTATE \
- | pamscale -quiet -linear -reduce $DOWNSAMPLE \
- | pnmpad -black \
- -halign=$HALIGN \
- -valign=$VALIGN \
- -height $CONTENT_HEIGHT \
- -width $TEXT_WATERMARK_HEIGHT
- }
- #-------------------------------------------------------------------------------
- page_overlay_watermark()
- {
- local FOLIUM="$1"
- local TEXT="$2"
- if [[ "$TEXT" == "" ]]; then
- cat
- else
- case "$FOLIUM" in
- verso)
- # Place watermark vertically in upper-left corner.
- page_overlay_rgba_stream \
- $(( MARGIN_LEFT - TEXT_WATERMARK_HEIGHT - TEXT_WATERMARK_GUTTER )) \
- $MARGIN_TOP \
- <( image_rgb_make_color \
- $TEXT_WATERMARK_HEIGHT $CONTENT_HEIGHT "$PANEL_BORDER_COLOR" ) \
- <( page_watermark_mask \
- "$FOLIUM" "$TEXT" ) \
- 0.50
- ;;
- recto)
- # Place watermark vertically in lower-right corner.
- page_overlay_rgba_stream \
- $(( MARGIN_LEFT + CONTENT_WIDTH + TEXT_WATERMARK_GUTTER )) \
- $MARGIN_TOP \
- <( image_rgb_make_color \
- $TEXT_WATERMARK_HEIGHT $CONTENT_HEIGHT "$PANEL_BORDER_COLOR" ) \
- <( page_watermark_mask \
- "$FOLIUM" "$TEXT" ) \
- 0.50
- ;;
- solo)
- # Place watermark vertically in opposite corners (both upper-left and
- # Lower right).
- page_overlay_watermark "verso" "$TEXT" | \
- page_overlay_watermark "recto" "$TEXT"
- ;;
- *)
- error_message_exit "${FOLIUM}: Invalid side"; return ;;
- esac
- fi
- }
- #-------------------------------------------------------------------------------
- page_splash_watermark_mask()
- {
- local FOLIUM="$1"
- local TEXT="$2"
- case "$FOLIUM" in
- verso) local ROTATE="+60"; local HALIGN=0.5; local VALIGN=0.5 ;;
- recto) local ROTATE="-60"; local HALIGN=0.5; local VALIGN=0.5 ;;
- solo) local ROTATE="+60"; local HALIGN=0.5; local VALIGN=0.5 ;;
- *) error_message_exit "${FOLIUM}: Invalid side"; return ;;
- esac
- DOWNSAMPLE=4
- pbmtextps \
- -font $SPLASH_WATERMARK_FONT \
- -fontsize $SPLASH_WATERMARK_FONT_SIZE \
- -resolution $((72*DOWNSAMPLE)) \
- -crop \
- "$TEXT" \
- | pnminvert \
- | pgmtopgm \
- | pnmrotate $ROTATE \
- | pamscale -quiet -linear -reduce $DOWNSAMPLE \
- | pnmpad -black \
- -halign=$HALIGN \
- -valign=$VALIGN \
- -width $PAGE_WIDTH \
- -height $PAGE_HEIGHT
- }
- #-------------------------------------------------------------------------------
- page_overlay_splash_watermark()
- {
- local FOLIUM="$1"
- local TEXT="$2"
- if [[ "$TEXT" == "" ]]; then
- cat
- else
- page_overlay_rgba_stream \
- 0 0 \
- <( image_rgb_make_color \
- $PAGE_WIDTH $PAGE_HEIGHT "white" ) \
- <( page_splash_watermark_mask \
- "$FOLIUM" "$TEXT" ) \
- 0.15 # Opacity
- fi
- }
- #===============================================================================
- # PAGE SHADOWS [RGB]
- #-------------------------------------------------------------------------------
- hex_rgb_luminosity()
- {
- local HEX_RGB=$1
- if [[ $HEX_RGB == "" ]]; then
- echo "1.0"
- else
- perl -e '
- my $hex_rgb = $ARGV[0];
- my ($r, $g, $b) = map { $_ ** 2.2 }
- map { hex($_) / 255.0 }
- ($hex_rgb =~ m/^#(..)(..)(..)$/);
- my $lum = 0.299*$r + 0.587*$g + 0.114*$b;
- print "$lum\n";
- ' $HEX_RGB
- fi
- }
- #-------------------------------------------------------------------------------
- background_luminosity()
- {
- local HEX_RGB_1=$1
- local HEX_RGB_2=$2
- LUM1=$(hex_rgb_luminosity $HEX_RGB_1)
- LUM2=$(hex_rgb_luminosity $HEX_RGB_2)
- awk "BEGIN { print ($LUM1 + $LUM2) / 2; }"
- }
- #-------------------------------------------------------------------------------
- shadow_gamma_from_background_colors()
- {
- local LUM=$(background_luminosity $BACKGROUND_COLOR1 $BACKGROUND_COLOR2)
- awk "BEGIN { print 2.0 + (8.0 * ($LUM ** 2)); }"
- }
- #-------------------------------------------------------------------------------
- page_shadow_mask()
- {
- local FOLIUM="$1"
- local PAGE_SHADOW_GAMMA=$(shadow_gamma_from_background_colors)
- case "$FOLIUM" in
- verso) # Left page; shadow on right
- pgmramp -lr -maxval=65535 $PAGE_SHADOW_WIDTH $PAGE_HEIGHT \
- | pamflip -lr \
- | pnmgamma $PAGE_SHADOW_GAMMA \
- | pamdepth 255 \
- | pnminvert
- ;;
- recto) # Right page; shadow on left
- pgmramp -lr -maxval=65535 $PAGE_SHADOW_WIDTH $PAGE_HEIGHT \
- | pnmgamma $PAGE_SHADOW_GAMMA \
- | pamdepth 255 \
- | pnminvert
- ;;
- solo) # No shadow
- pbmmake -white $PAGE_WIDTH $PAGE_HEIGHT
- ;;
- *)
- error_message_exit "${FOLIUM}: Invalid side"
- return
- ;;
- esac
- }
- #-------------------------------------------------------------------------------
- page_overlay_shadow()
- {
- local FOLIUM="$1"
- local SHADOW_X_OFFSET
- case "$FOLIUM" in
- verso) SHADOW_X_OFFSET=$(( PAGE_WIDTH - PAGE_SHADOW_WIDTH )) ;;
- recto) SHADOW_X_OFFSET=0 ;;
- *) ;;
- esac
- case "$FOLIUM" in
- verso|recto) # Left or right page; shadow on right or left, respectively
- pamcomp \
- -xoff=$SHADOW_X_OFFSET \
- -yoff=0 \
- <( pamstack -quiet -tupletype RGB_ALPHA \
- <( ppmmake -maxval=255 black $PAGE_SHADOW_WIDTH $PAGE_HEIGHT ) \
- <( page_shadow_mask "$FOLIUM" ) \
- ) \
- -
- ;;
- solo) # No shadow
- cat
- ;;
- *)
- error_message_exit "${FOLIUM}: Invalid side"
- return
- ;;
- esac
- }
- #===============================================================================
- # PAGE RENDERING
- #-------------------------------------------------------------------------------
- page_render_prevalidate()
- {
- local -i ERROR_COUNT=0
- param_general_validate BACKGROUND_COLOR1 "$BACKGROUND_COLOR1" '^.+$' "color"
- ERROR_COUNT+=$?
- param_general_validate BACKGROUND_COLOR2 "$BACKGROUND_COLOR2" '^.*$' "color"
- ERROR_COUNT+=$?
- param_nonnegative_integer_validate PANEL_COUNT "$PANEL_COUNT"
- ERROR_COUNT+=$?
- param_general_validate PANEL_RENDERING_PIPELINE "$PANEL_RENDERING_PIPELINE" \
- '^(rgb|rgba)$' "colorspace"
- param_positive_integer_validate PANEL_BORDER "$PANEL_BORDER"
- ERROR_COUNT+=$?
- param_positive_integer_validate PANEL_GUTTER "$PANEL_GUTTER"
- ERROR_COUNT+=$?
- param_positive_integer_validate MARGIN_TOP "$MARGIN_TOP"
- ERROR_COUNT+=$?
- param_positive_integer_validate MARGIN_BOTTOM "$MARGIN_BOTTOM"
- ERROR_COUNT+=$?
- param_positive_integer_validate MARGIN_LEFT "$MARGIN_LEFT"
- ERROR_COUNT+=$?
- param_positive_integer_validate MARGIN_RIGHT "$MARGIN_RIGHT"
- ERROR_COUNT+=$?
- param_positive_integer_validate CONTENT_WIDTH "$CONTENT_WIDTH"
- ERROR_COUNT+=$?
- param_positive_integer_validate CONTENT_HEIGHT "$CONTENT_HEIGHT"
- ERROR_COUNT+=$?
- param_positive_integer_validate PAGE_WIDTH "$PAGE_WIDTH"
- ERROR_COUNT+=$?
- param_positive_integer_validate PAGE_HEIGHT "$PAGE_HEIGHT"
- ERROR_COUNT+=$?
- param_positive_integer_validate PAGE_LABEL_GUTTER "$PAGE_LABEL_GUTTER"
- ERROR_COUNT+=$?
- param_positive_integer_validate PAGE_LABEL_HEIGHT "$PAGE_LABEL_HEIGHT"
- ERROR_COUNT+=$?
- param_general_validate PAGE_LABEL_FONT "$PAGE_LABEL_FONT" '^.+$' "font"
- ERROR_COUNT+=$?
- param_positive_integer_validate PAGE_LABEL_FONT_SIZE "$PAGE_LABEL_FONT_SIZE"
- ERROR_COUNT+=$?
- param_general_validate PAGE_LABEL_COLOR "$PAGE_LABEL_COLOR" '^.+$' "color"
- ERROR_COUNT+=$?
- param_positive_integer_validate TEXT_WATERMARK_GUTTER "$TEXT_WATERMARK_GUTTER"
- ERROR_COUNT+=$?
- param_positive_integer_validate TEXT_WATERMARK_HEIGHT "$TEXT_WATERMARK_HEIGHT"
- ERROR_COUNT+=$?
- param_general_validate TEXT_WATERMARK_FONT "$TEXT_WATERMARK_FONT" '^.+$' "font"
- ERROR_COUNT+=$?
- param_positive_integer_validate TEXT_WATERMARK_FONT_SIZE "$TEXT_WATERMARK_FONT_SIZE"
- ERROR_COUNT+=$?
- param_positive_integer_validate PAGE_SHADOW_WIDTH "$PAGE_SHADOW_WIDTH"
- ERROR_COUNT+=$?
- param_real_validate PAGE_SHADOW_GAMMA "$PAGE_SHADOW_GAMMA"
- ERROR_COUNT+=$?
- panel_list_validate
- ERROR_COUNT+=$?
- return $ERROR_COUNT
- }
- #-------------------------------------------------------------------------------
- page_render_background()
- {
- if [[ "$BACKGROUND_IMAGE_PATH" == "" ]]; then
- if [[ "$BACKGROUND_COLOR2" == "" ]]; then
- page_rgb_make_color "$BACKGROUND_COLOR1"
- else
- page_rgb_make_color_gradient "$BACKGROUND_COLOR1" "$BACKGROUND_COLOR2"
- fi
- else
- page_rgb_with_image "$BACKGROUND_IMAGE_PATH"
- fi
- }
- #-------------------------------------------------------------------------------
- page_render_panel_by_index()
- {
- local PANEL_INDEX=$1
- local -A PANEL
- panel_list_fetch_by_index $PANEL_INDEX
- page_overlay_panel
- }
- #-------------------------------------------------------------------------------
- page_render_panels_by_index()
- {
- local PANEL_INDEX=${1:-0}
- if (( PANEL_INDEX < PANEL_COUNT - 1 )); then
- # Overlay one panel and establish a recursive pipe chain to overlay all
- # subsequent panels.
- page_render_panel_by_index $PANEL_INDEX \
- | page_render_panels_by_index $((PANEL_INDEX + 1))
- elif (( PANEL_INDEX == PANEL_COUNT - 1 )); then
- # Overlay one panel and stop recursing.
- page_render_panel_by_index $PANEL_INDEX
- else
- # This should only ever happen if the page has a background with exactly
- # zero panels to overlay. In this case, simply pass the input through to
- # the output unchanged, and halt recursion.
- cat
- fi
- }
- #-------------------------------------------------------------------------------
- page_rgb_render_background_and_panels()
- {
- page_render_background \
- | page_render_panels_by_index
- }
- #-------------------------------------------------------------------------------
- page_rgba_render_background_and_panels()
- {
- page_rgba_make_transparent \
- | page_render_panels_by_index \
- | page_rgba_underlay_rgb_stream <( page_render_background )
- }
- #-------------------------------------------------------------------------------
- page_render_background_and_panels()
- {
- case $PANEL_RENDERING_PIPELINE in
- rgb) page_rgb_render_background_and_panels ;;
- rgba) page_rgba_render_background_and_panels ;;
- "") error_message_exit "PANEL_RENDERING_PIPELINE is undefined" ;;
- esac
- }
- #-------------------------------------------------------------------------------
- page_render()
- {
- local FOLIUM="$1"
- local PAGE_LABEL="$2"
- local TEXT_WATERMARK="$3"
- local SPLASH_WATERMARK="$4"
- local OUTPUT_IMAGE_PATH="$5"
- case "$FOLIUM" in
- verso|recto|solo) ;;
- "") error_message_exit "FOLIUM undefined"; return ;;
- *) error_message_exit "${FOLIUM}: Invalid side"; return ;;
- esac
- local -i PRERENDER_ERROR_COUNT=0
- page_render_prevalidate; PRERENDER_ERROR_COUNT+=$?
- if (( PRERENDER_ERROR_COUNT > 0 )); then
- error_message_exit "$PRERENDER_ERROR_COUNT errors found in prerendering validation"
- false
- return
- fi
- page_render_background_and_panels \
- | page_overlay_label "$FOLIUM" "$PAGE_LABEL" \
- | page_overlay_watermark "$FOLIUM" "$TEXT_WATERMARK" \
- | page_overlay_splash_watermark "$FOLIUM" "$SPLASH_WATERMARK" \
- | page_overlay_shadow "$FOLIUM" \
- | image_write "$OUTPUT_IMAGE_PATH"
- }
- #-------------------------------------------------------------------------------
- page_render_from_intermediate()
- {
- local INPUT_IMAGE_PATH="$1"
- local FOLIUM="$2"
- local SPREAD="$3"
- local PAGE_LABEL="$4"
- local TEXT_WATERMARK="$5"
- local SPLASH_WATERMARK="$6"
- local OUTPUT_IMAGE_PATH="$7"
- case "$FOLIUM" in
- verso|recto|solo) ;;
- "") error_message_exit "FOLIUM undefined"; return ;;
- *) error_message_exit "${FOLIUM}: Invalid side"; return ;;
- esac
- if [[ $SPREAD == 1 ]]; then # 1-page spread get a shadow.
- image_rgb_borderless "$INPUT_IMAGE_PATH" \
- | page_overlay_label "$FOLIUM" "$PAGE_LABEL" \
- | page_overlay_watermark "$FOLIUM" "$TEXT_WATERMARK" \
- | page_overlay_splash_watermark "$FOLIUM" "$SPLASH_WATERMARK" \
- | page_overlay_shadow "$FOLIUM" \
- | image_write "$OUTPUT_IMAGE_PATH"
- elif [[ $SPREAD == 2 ]]; then # 2-page spreads do not get a shadow.
- image_rgb_borderless "$INPUT_IMAGE_PATH" \
- | page_overlay_label "$FOLIUM" "$PAGE_LABEL" \
- | page_overlay_watermark "$FOLIUM" "$TEXT_WATERMARK" \
- | image_write "$OUTPUT_IMAGE_PATH"
- else
- error_message_exit "Invalid SPREAD: $SPREAD"
- fi
- }
Advertisement