Skip to content

Instantly share code, notes, and snippets.

@sjlongland
Created May 3, 2024 06:18
Show Gist options
  • Save sjlongland/b860ab49660eccf584ffc2a8230c0398 to your computer and use it in GitHub Desktop.
Save sjlongland/b860ab49660eccf584ffc2a8230c0398 to your computer and use it in GitHub Desktop.
Generating a cross-product of images for SSTV from some stock images and layers

Generating SSTV stock images from source images and layers

This is how I've been generating lots of interesting stock images, scaled and prepared for SSTV transmission. Most modes require either 320×240 or more common, 320×256px images. I also want variations for different occasions:

  • calling CQ
  • greetings (morning, afternoon, evening)
  • departing
  • blank ones that can be used in templates

All images are to have a call-sign applied if none is present.

For this, I scale all input images to 320×240px, add a 16px header with the call-sign in morse (pre-prepared in The Gimp), and provide a number of transparent PNG "layers" that can be overlayed.

netpbm does most of the grunt work, with GNU make orchestrating.

"Background" layer

This contains the top 16px header and a background to overlay the image on top of. Should be the same size as the output (320×256px), opaque true-colour PNG.

This gets put in layers/${CALLSIGN}/header-bg.png -- one for each callsign you use.

Callsign layer

This is a stylised "de ${CALLSIGN}" image, which will be overlayed over the image. Again, same size as the output, but this time, it should be a transparent true-colour PNG.

This gets put in layers/${CALLSIGN}/de-call.png -- one for each callsign you use.

Message layers

These are additional message layers for different variations of the image.

You can use any name you like: consisting of non-whitespace filename characters. The image itself is a true-colour transparent PNG the same size as the output (320×256px).

In my configuration I have:

  • cq-sstv
  • morning-all
  • afternoon-all
  • evening-all
  • 73-all
  • 73-gotta-fly

The name empty is reserved, for "no message layer", making an "empty" output image that you can use in QSSTV templates.

Source images

These are images in PNG or JPEG format, stored in the src/ directory. They are assumed to be non-transparent, and should be roughly landscape. If the image is a more widescreen ratio, it will be auto-cropped to a 4:3 aspect ratio (picking the image centre).

Makefile configuration

These are the first two lines of the Makefile. CALLSIGNS should be all your callsigns, both permanent and temporary. (e.g. in Australia, on 3 days a year, we substitute the VK prefix with AX.) Space-separated.

VARIANTS describes what variants of the source images to generate. Space-separated.

Generating images

Run make, and your images should be placed in tx_stock/.

Generating the images

Run make

CALLSIGNS ?= vk4msl ax4msl
VARIANTS ?= empty cq-sstv morning-all afternoon-all evening-all 73-all 73-gotta-fly
CALLSIGN ?=
VARIANT ?=
ifeq ($(VARIANT),)
# VARIANT not set, iterate over VARIANTS
.PHONY: all
all:
for var in $(VARIANTS); do \
$(MAKE) VARIANT=$$var || break; \
done
else
ifeq ($(CALLSIGN),)
# CALLSIGN not set, iterate over CALLSIGNS
.PHONY: all
all:
for call in $(CALLSIGNS); do \
$(MAKE) CALLSIGN=$$call VARIANT=$(VARIANT) || break; \
done
else
# CALLSIGN and VARIANT set
BACKGROUND=layers/$(CALLSIGN)/header-bg.png
CALLSIGN_LAYER=layers/$(CALLSIGN)/de-call.png
VARIANT_LAYER=layers/$(VARIANT).png
SRC_FILE_TYPES=png jpg
RATIO_W=4
RATIO_H=3
BACKGROUND_PAM=$(patsubst %.png,%.pam,$(BACKGROUND))
CALLSIGN_PAM=$(patsubst %.png,%.pam,$(CALLSIGN_LAYER))
VARIANT_LAYER_PAM=$(patsubst %.png,%.pam,$(VARIANT_LAYER))
# Cross-product, all file types; nocall- and $(CALLSIGN)- prefixes
ALL_SOURCES=$(foreach prefix,nocall $(CALLSIGN),\
$(foreach extn,$(SRC_FILE_TYPES),\
$(patsubst src/%.$(extn),src/%-$(VARIANT).pam,\
$(wildcard src/$(prefix)-*.$(extn))\
)\
)\
)
ALL_OUTPUTS=$(patsubst tx_stock/nocall-%,tx_stock/$(CALLSIGN)-%,\
$(patsubst src/%.pam,tx_stock/%.png,$(ALL_SOURCES))\
)
all: $(ALL_OUTPUTS)
tx_stock/$(CALLSIGN)-%-empty.png: scaled/$(CALLSIGN)-%.pam | Makefile
pamtopng $< > $@
tx_stock/$(CALLSIGN)-%-$(VARIANT).png: scaled/$(CALLSIGN)-%.pam $(VARIANT_LAYER_PAM) | Makefile
pamcomp -valign=bottom -align=right $(VARIANT_LAYER_PAM) $< \
| pamtopng > $@
scaled/$(CALLSIGN)-%.pam: src/$(CALLSIGN)-%.pam src/$(CALLSIGN)-%.pam.sz \
$(BACKGROUND_PAM)
( \
set -x; \
read w h < $<.sz; \
echo "dimensions: $${w} by $${h}" >&2; \
if [ $${w} -ge $${h} ]; then \
echo "aspect-ratio crop" >&2; \
out_w=$$(( ($${h}*$(RATIO_W)) / $(RATIO_H) )); \
echo "landscape: crop $${w} to $${out_w}" >&2; \
if [ $${out_w} -le $${w} ]; then \
echo "no crop" >&2; \
cat $< \
else \
diff_w=$$(( $${w} - $${out_w} )); \
left=$$(( $${diff_w} / 2 )); \
right=$$(( $${w} - $${left} )); \
echo "crop $${w}: $${left}-$${right}" >&2; \
pamcut -left $${left} \
-right $${right} $<; \
fi; \
else \
echo "portrait: no crop" >&2; \
cat $<; \
fi; \
) \
| pamscale -xyfit 320 240 \
| pamcomp -valign=bottom -align=center - $(BACKGROUND_PAM) \
> $@
scaled/$(CALLSIGN)-%.pam: src/nocall-%.pam src/nocall-%.pam.sz \
$(CALLSIGN_PAM) $(BACKGROUND_PAM)
( \
set -x; \
read w h < $<.sz; \
echo "dimensions: $${w} by $${h}" >&2; \
if [ $${w} -ge $${h} ]; then \
echo "aspect-ratio crop" >&2; \
out_w=$$(( ($${h}*$(RATIO_W)) / $(RATIO_H) )); \
diff_w=$$(( $${w} - $${out_w} )); \
echo "landscape: crop $${w} to $${out_w} ($${diff_w})" >&2; \
if [ $${diff_w} -le 10 ]; then \
echo "no crop" >&2; \
cat $<; \
else \
left=$$(( $${diff_w} / 2 )); \
right=$$(( $${w} - $${left} - 1 )); \
echo "crop $${w}: $${left}-$${right}" >&2; \
pamcut -left $${left} \
-right $${right} $<; \
fi; \
else \
echo "portrait: no crop" >&2; \
cat $<; \
fi; \
) \
| pamscale -xyfit 320 240 \
| pamcomp -valign=bottom -align=center - $(BACKGROUND_PAM) \
| pamcomp -valign=bottom -align=center $(CALLSIGN_PAM) - \
> $@
src/%.pam: src/%.jpg
djpeg $< > $@
src/%.pam: src/%.png
pngtopam $< > $@
src/%.pam.sz: src/%.pam
pamfile -size $< > $@
layers/%.pam: layers/%.png
pngtopam -alphapam $< > $@
endif
endif
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment