Initial commit
This commit is contained in:
4
.dockerignore
Normal file
4
.dockerignore
Normal file
@@ -0,0 +1,4 @@
|
||||
*
|
||||
!Dockerfile
|
||||
!docker-entrypoint.sh
|
||||
!scan-one
|
||||
17
.gitignore
vendored
Normal file
17
.gitignore
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
# Generated scans
|
||||
scans/
|
||||
|
||||
# Local environment overrides
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Local tooling
|
||||
.agents/
|
||||
.codex/
|
||||
.idea/
|
||||
.vscode/
|
||||
|
||||
# OS metadata
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
53
Dockerfile
Normal file
53
Dockerfile
Normal file
@@ -0,0 +1,53 @@
|
||||
FROM alpine:3.24
|
||||
|
||||
ARG HPLIP_PLUGIN_VERSION=3.26.4
|
||||
ARG HPLIP_PLUGIN_SHA256=199f78f8af7f36894d7180e9090963ce2550a75ec701f8a4ba37665a9746fdf0
|
||||
|
||||
RUN apk add --no-cache \
|
||||
dbus \
|
||||
gcompat \
|
||||
hplip \
|
||||
imagemagick \
|
||||
imagemagick-pdf \
|
||||
sane-backend-hpaio \
|
||||
sane-utils \
|
||||
tzdata
|
||||
|
||||
# The M1217 scanner needs HP's proprietary Marvell plug-in. HP ships it as a
|
||||
# glibc binary, so gcompat is preloaded when SANE starts it on Alpine/musl.
|
||||
RUN wget \
|
||||
-q \
|
||||
-O /tmp/hplip-plugin.run \
|
||||
"https://www.openprinting.org/download/printdriver/auxfiles/HP/plugins/hplip-${HPLIP_PLUGIN_VERSION}-plugin.run" \
|
||||
&& echo "${HPLIP_PLUGIN_SHA256} /tmp/hplip-plugin.run" | sha256sum -c - \
|
||||
&& mkdir -p /tmp/hplip-plugin /usr/share/hplip/scan/plugins \
|
||||
&& sh /tmp/hplip-plugin.run \
|
||||
--target /tmp/hplip-plugin \
|
||||
--noexec \
|
||||
>/dev/null \
|
||||
&& install \
|
||||
-m 0755 \
|
||||
/tmp/hplip-plugin/bb_marvell-x86_64.so \
|
||||
/usr/share/hplip/scan/plugins/bb_marvell-x86_64.so \
|
||||
&& ln -sf \
|
||||
/usr/share/hplip/scan/plugins/bb_marvell-x86_64.so \
|
||||
/usr/share/hplip/scan/plugins/bb_marvell.so \
|
||||
&& mkdir -p /var/lib/hp \
|
||||
&& printf \
|
||||
'[plugin]\ninstalled = 1\neula = 1\nversion = %s\n' \
|
||||
"${HPLIP_PLUGIN_VERSION}" \
|
||||
> /var/lib/hp/hplip.state \
|
||||
&& chmod 0644 /var/lib/hp/hplip.state \
|
||||
&& rm -rf /tmp/hplip-plugin /tmp/hplip-plugin.run
|
||||
|
||||
ENV LD_PRELOAD=/lib/libgcompat.so.0
|
||||
|
||||
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
|
||||
COPY scan-one /usr/local/bin/scan-one
|
||||
|
||||
RUN chmod 0755 \
|
||||
/usr/local/bin/docker-entrypoint.sh \
|
||||
/usr/local/bin/scan-one
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
|
||||
CMD ["/usr/local/bin/scan-one"]
|
||||
33
Makefile
Normal file
33
Makefile
Normal file
@@ -0,0 +1,33 @@
|
||||
SHELL := /bin/ash
|
||||
.SHELLFLAGS := -eu -c
|
||||
|
||||
SCAN_CONTAINER := hp-m1217-scanner-run
|
||||
|
||||
.PHONY: scan build clean check
|
||||
|
||||
scan: check build
|
||||
@mkdir -p scans
|
||||
@docker rm -f "$(SCAN_CONTAINER)" >/dev/null 2>&1 || true
|
||||
@cleanup() { \
|
||||
docker rm -f "$(SCAN_CONTAINER)" >/dev/null 2>&1 || true; \
|
||||
}; \
|
||||
trap cleanup EXIT HUP INT TERM; \
|
||||
PUID="$$(id -u)" PGID="$$(id -g)" \
|
||||
docker compose run --rm --name "$(SCAN_CONTAINER)" scanner
|
||||
|
||||
build: check
|
||||
@docker compose build scanner
|
||||
|
||||
clean: check
|
||||
@docker rm -f "$(SCAN_CONTAINER)" >/dev/null 2>&1 || true
|
||||
@docker compose down --rmi all --remove-orphans
|
||||
|
||||
check:
|
||||
@command -v docker >/dev/null 2>&1 || { \
|
||||
echo "Error: docker is required." >&2; \
|
||||
exit 1; \
|
||||
}
|
||||
@docker compose version >/dev/null 2>&1 || { \
|
||||
echo "Error: the Docker Compose plugin is required." >&2; \
|
||||
exit 1; \
|
||||
}
|
||||
33
README.md
Normal file
33
README.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# HP M1217 scanner
|
||||
|
||||
The scanner runs in an Alpine Linux container. Build the image and scan one
|
||||
page from the flatbed with:
|
||||
|
||||
```sh
|
||||
make scan
|
||||
```
|
||||
|
||||
The resulting PDF is written to `scans/`. The command passes the current user
|
||||
and group IDs to the container, so the PDF is owned by the invoking user.
|
||||
|
||||
Optional settings can be passed to `make`:
|
||||
|
||||
```sh
|
||||
RESOLUTION=600 MODE=Gray make scan
|
||||
```
|
||||
|
||||
Supported variables are `RESOLUTION`, `MODE`, `SCAN_SOURCE`, `SCAN_WIDTH`,
|
||||
`SCAN_HEIGHT`, `JPEG_QUALITY`, `SCAN_TIMEOUT` (seconds), and `SANE_DEVICE`.
|
||||
The default flatbed area is `215.9 × 297 mm`; override it when using a
|
||||
different source or page size.
|
||||
|
||||
If an interrupted run leaves a container behind, the next `make scan` removes
|
||||
that exact scan container before opening the USB device.
|
||||
|
||||
Remove the scanner container, Compose resources, and the built image with:
|
||||
|
||||
```sh
|
||||
make clean
|
||||
```
|
||||
|
||||
Scanned PDFs in `scans/` are preserved.
|
||||
22
compose.yaml
Normal file
22
compose.yaml
Normal file
@@ -0,0 +1,22 @@
|
||||
services:
|
||||
scanner:
|
||||
build:
|
||||
context: .
|
||||
image: hp-m1217-scanner:latest
|
||||
privileged: true
|
||||
restart: "no"
|
||||
environment:
|
||||
TZ: ${TZ:-Europe/Berlin}
|
||||
PUID: ${PUID:-1000}
|
||||
PGID: ${PGID:-1000}
|
||||
RESOLUTION: ${RESOLUTION:-300}
|
||||
MODE: ${MODE:-Color}
|
||||
SCAN_SOURCE: ${SCAN_SOURCE:-Flatbed}
|
||||
SCAN_WIDTH: ${SCAN_WIDTH:-215.9}
|
||||
SCAN_HEIGHT: ${SCAN_HEIGHT:-297}
|
||||
JPEG_QUALITY: ${JPEG_QUALITY:-90}
|
||||
SCAN_TIMEOUT: ${SCAN_TIMEOUT:-300}
|
||||
SANE_DEVICE: ${SANE_DEVICE:-}
|
||||
volumes:
|
||||
- /dev/bus/usb:/dev/bus/usb
|
||||
- ./scans:/scans
|
||||
14
docker-entrypoint.sh
Normal file
14
docker-entrypoint.sh
Normal file
@@ -0,0 +1,14 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
mkdir -p /run/dbus
|
||||
|
||||
rm -f /run/dbus/pid
|
||||
|
||||
if [[ ! -s /etc/machine-id ]]; then
|
||||
dbus-uuidgen --ensure=/etc/machine-id
|
||||
fi
|
||||
|
||||
dbus-daemon --system --fork
|
||||
|
||||
exec "$@"
|
||||
210
scan-one
Executable file
210
scan-one
Executable file
@@ -0,0 +1,210 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
OUTPUT_DIR="${OUTPUT_DIR:-/scans}"
|
||||
RESOLUTION="${RESOLUTION:-300}"
|
||||
MODE="${MODE:-Color}"
|
||||
SCAN_SOURCE="${SCAN_SOURCE:-Flatbed}"
|
||||
SCAN_WIDTH="${SCAN_WIDTH:-215.9}"
|
||||
SCAN_HEIGHT="${SCAN_HEIGHT:-297}"
|
||||
JPEG_QUALITY="${JPEG_QUALITY:-90}"
|
||||
SCAN_TIMEOUT="${SCAN_TIMEOUT:-300}"
|
||||
PUID="${PUID:-0}"
|
||||
PGID="${PGID:-0}"
|
||||
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
DEVICE="${SANE_DEVICE:-}"
|
||||
|
||||
if [ -z "$DEVICE" ]; then
|
||||
DEVICE="$(scanimage -f '%d%n' 2>/dev/null | sed -n '/^hpaio:/{p;q;}')"
|
||||
fi
|
||||
|
||||
if [ -z "$DEVICE" ]; then
|
||||
echo "Error: HP scanner not found." >&2
|
||||
scanimage -L || true
|
||||
exit 2
|
||||
fi
|
||||
|
||||
STAMP="$(date '+%Y-%m-%d_%H-%M-%S')"
|
||||
|
||||
TMP_PNM="$(mktemp "/tmp/${STAMP}.pnm.XXXXXX")"
|
||||
TMP_FIXED="${TMP_PNM}.fixed"
|
||||
OUTPUT_FILE="${OUTPUT_DIR}/${STAMP}.pdf"
|
||||
OUTPUT_COMPLETE=0
|
||||
|
||||
cleanup() {
|
||||
rm -f "$TMP_PNM" "$TMP_FIXED"
|
||||
if [ "$OUTPUT_COMPLETE" -ne 1 ]; then
|
||||
rm -f "$OUTPUT_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
trap cleanup EXIT HUP INT TERM
|
||||
|
||||
SUFFIX=1
|
||||
while [ -e "$OUTPUT_FILE" ]; do
|
||||
OUTPUT_FILE="${OUTPUT_DIR}/${STAMP}_${SUFFIX}.pdf"
|
||||
SUFFIX=$((SUFFIX + 1))
|
||||
done
|
||||
|
||||
echo "Scanner: $DEVICE"
|
||||
echo "Source: $SCAN_SOURCE"
|
||||
echo "Resolution: $RESOLUTION dpi"
|
||||
echo "Mode: $MODE"
|
||||
echo "Area: ${SCAN_WIDTH} x ${SCAN_HEIGHT} mm"
|
||||
echo "Output: $OUTPUT_FILE"
|
||||
|
||||
set +e
|
||||
timeout "$SCAN_TIMEOUT" scanimage \
|
||||
-d "$DEVICE" \
|
||||
--source "$SCAN_SOURCE" \
|
||||
--resolution "$RESOLUTION" \
|
||||
--mode "$MODE" \
|
||||
-x "$SCAN_WIDTH" \
|
||||
-y "$SCAN_HEIGHT" \
|
||||
--format=pnm \
|
||||
--output-file="$TMP_PNM"
|
||||
SCAN_STATUS=$?
|
||||
set -e
|
||||
|
||||
if [ "$SCAN_STATUS" -ne 0 ]; then
|
||||
if [ "$SCAN_STATUS" -eq 124 ] || [ "$SCAN_STATUS" -eq 137 ] || [ "$SCAN_STATUS" -eq 143 ]; then
|
||||
echo "Error: scanner did not respond within ${SCAN_TIMEOUT} seconds." >&2
|
||||
else
|
||||
echo "Error: scanimage failed with status ${SCAN_STATUS}." >&2
|
||||
fi
|
||||
exit "$SCAN_STATUS"
|
||||
fi
|
||||
|
||||
if [ ! -s "$TMP_PNM" ]; then
|
||||
echo "Error: scanner returned an empty PNM file." >&2
|
||||
exit 3
|
||||
fi
|
||||
|
||||
normalize_pnm_height() {
|
||||
MAGIC="$(sed -n '1p' "$TMP_PNM")"
|
||||
COMMENT="$(sed -n '2p' "$TMP_PNM")"
|
||||
DIMENSIONS="$(sed -n '3p' "$TMP_PNM")"
|
||||
|
||||
if [ "$COMMENT" != '# SANE data follows' ]; then
|
||||
echo "Error: scanner returned an unexpected PNM header." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
set -- $DIMENSIONS
|
||||
if [ "$#" -ne 2 ]; then
|
||||
echo "Error: scanner returned invalid PNM dimensions." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
WIDTH="$1"
|
||||
HEIGHT="$2"
|
||||
|
||||
case "${WIDTH}:${HEIGHT}" in
|
||||
*[!0-9:]* | :* | *:)
|
||||
echo "Error: scanner returned invalid PNM dimensions." >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$MAGIC" in
|
||||
P6)
|
||||
CHANNELS=3
|
||||
HEADER_LINES=4
|
||||
;;
|
||||
P5)
|
||||
CHANNELS=1
|
||||
HEADER_LINES=4
|
||||
;;
|
||||
P4)
|
||||
CHANNELS=0
|
||||
HEADER_LINES=3
|
||||
;;
|
||||
*)
|
||||
echo "Error: scanner returned unsupported PNM type ${MAGIC}." >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "$MAGIC" = P4 ]; then
|
||||
ROW_BYTES=$(((WIDTH + 7) / 8))
|
||||
else
|
||||
MAX_VALUE="$(sed -n '4p' "$TMP_PNM")"
|
||||
case "$MAX_VALUE" in
|
||||
'' | *[!0-9]*)
|
||||
echo "Error: scanner returned an invalid PNM color depth." >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "$MAX_VALUE" -gt 255 ]; then
|
||||
SAMPLE_BYTES=2
|
||||
else
|
||||
SAMPLE_BYTES=1
|
||||
fi
|
||||
ROW_BYTES=$((WIDTH * CHANNELS * SAMPLE_BYTES))
|
||||
fi
|
||||
|
||||
HEADER_BYTES="$(head -n "$HEADER_LINES" "$TMP_PNM" | wc -c)"
|
||||
FILE_BYTES="$(wc -c < "$TMP_PNM")"
|
||||
PAYLOAD_BYTES=$((FILE_BYTES - HEADER_BYTES))
|
||||
EXPECTED_BYTES=$((ROW_BYTES * HEIGHT))
|
||||
|
||||
if [ "$PAYLOAD_BYTES" -eq "$EXPECTED_BYTES" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ "$PAYLOAD_BYTES" -lt 0 ] || [ $((PAYLOAD_BYTES % ROW_BYTES)) -ne 0 ]; then
|
||||
echo "Error: scanner returned a truncated PNM row." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
ACTUAL_HEIGHT=$((PAYLOAD_BYTES / ROW_BYTES))
|
||||
MISSING_ROWS=$((HEIGHT - ACTUAL_HEIGHT))
|
||||
|
||||
if [ "$MISSING_ROWS" -ne 1 ]; then
|
||||
echo "Error: scanner returned ${ACTUAL_HEIGHT} rows instead of ${HEIGHT}." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "Warning: correcting the HPAIO one-row PNM metadata error." >&2
|
||||
|
||||
{
|
||||
head -n 2 "$TMP_PNM"
|
||||
printf '%s %s\n' "$WIDTH" "$ACTUAL_HEIGHT"
|
||||
if [ "$HEADER_LINES" -eq 4 ]; then
|
||||
printf '%s\n' "$MAX_VALUE"
|
||||
fi
|
||||
tail -c "+$((HEADER_BYTES + 1))" "$TMP_PNM"
|
||||
} > "$TMP_FIXED"
|
||||
|
||||
mv "$TMP_FIXED" "$TMP_PNM"
|
||||
}
|
||||
|
||||
normalize_pnm_height
|
||||
|
||||
echo "Scan completed."
|
||||
|
||||
magick \
|
||||
-density "$RESOLUTION" \
|
||||
"$TMP_PNM" \
|
||||
-compress JPEG \
|
||||
-quality "$JPEG_QUALITY" \
|
||||
"$OUTPUT_FILE"
|
||||
|
||||
if [ ! -s "$OUTPUT_FILE" ]; then
|
||||
echo "Error: PDF was not created." >&2
|
||||
exit 4
|
||||
fi
|
||||
|
||||
PDF_MAGIC="$(dd if="$OUTPUT_FILE" bs=5 count=1 2>/dev/null)"
|
||||
if [ "$PDF_MAGIC" != '%PDF-' ]; then
|
||||
echo "Error: output file is not a valid PDF." >&2
|
||||
exit 5
|
||||
fi
|
||||
|
||||
chown "$PUID:$PGID" "$OUTPUT_FILE" 2>/dev/null || true
|
||||
|
||||
OUTPUT_COMPLETE=1
|
||||
echo "Saved: $OUTPUT_FILE"
|
||||
Reference in New Issue
Block a user