|
#!/bin/bash |
|
# |
|
# HP LaserJet P1005 driver install for modern macOS (Apple Silicon, tested macOS 26). |
|
# |
|
# Sandbox-native: Apple's cgpdftoraster (runs inside the CUPS sandbox) rasterises, |
|
# then rastertoxqx (system libs only) -> foo2xqx -> printer. No ghostscript, no |
|
# Homebrew. Firmware is auto-loaded on power-on by the foo2zjs osx-hplj-hotplug |
|
# daemon. See README.md for the why. |
|
# |
|
# Usage: sudo bash install.sh |
|
# |
|
set -euo pipefail |
|
|
|
QUEUE="${QUEUE:-HP_LaserJet_P1005}" |
|
FILTERDIR=/usr/libexec/cups/filter |
|
FWDIR=/usr/local/share/foo2xqx/firmware |
|
FOO2ZJS_REPO="${FOO2ZJS_REPO:-https://github.com/OpenPrinting/foo2zjs.git}" |
|
FW_URL="${FW_URL:-https://www.quirinux.org/printers/sihpP1005.tar.gz}" |
|
|
|
[ "$(id -u)" = 0 ] || { echo "Run with sudo: sudo bash $0" >&2; exit 1; } |
|
xcode-select -p >/dev/null 2>&1 || { echo "Install Xcode CLT first: xcode-select --install" >&2; exit 1; } |
|
|
|
work="$(mktemp -d)"; trap 'rm -rf "$work"' EXIT; cd "$work" |
|
|
|
echo ">> Cloning foo2zjs (for foo2xqx, arm2hpdl, hotplug daemon)" |
|
git clone --depth 1 "$FOO2ZJS_REPO" foo2zjs >/dev/null 2>&1 |
|
cd foo2zjs |
|
|
|
echo ">> Building foo2xqx + arm2hpdl (no external deps; bundled jbig)" |
|
cc -O2 -o foo2xqx foo2xqx.c jbig.c jbig_ar.c -lm |
|
cc -O2 -o arm2hpdl arm2hpdl.c |
|
|
|
echo ">> Building firmware hotplug daemon (patched off SIP-protected /usr/share)" |
|
sed -i.bak 's#/usr/share/foo2#/usr/local/share/foo2#g' osx-hotplug/osx-hplj-hotplug.m |
|
cc -O2 osx-hotplug/osx-hplj-hotplug.m -o osx-hplj-hotplug \ |
|
-framework IOKit -framework Foundation -framework CoreFoundation |
|
|
|
echo ">> Fetching + converting firmware" |
|
curl -fsSL -o sihpP1005.tar.gz "$FW_URL" |
|
tar xzf sihpP1005.tar.gz # -> sihpP1005.img |
|
./arm2hpdl sihpP1005.img > sihpP1005.dl |
|
|
|
echo ">> Building rastertoxqx" |
|
cat > rastertoxqx.c <<'RXEOF' |
|
/* |
|
* rastertoxqx - CUPS raster -> foo2xqx (XQX/ZjStream) filter for HP LaserJet P1005. |
|
* |
|
* Self-contained: reads CUPS raster on stdin (produced inside the CUPS sandbox by |
|
* Apple's cgpdftoraster), converts each page to pbmraw, and pipes it to foo2xqx. |
|
* Links only against libcupsimage/libcups (system), so it runs fine under the |
|
* macOS CUPS filter sandbox. foo2xqx lives beside this filter in the CUPS filter |
|
* dir (an allowed exec location); override with $FOO2XQX for offline testing. |
|
* |
|
* CUPS filter argv: 0=name 1=job 2=user 3=title 4=copies 5=options 6=[rasterfile] |
|
*/ |
|
#include <cups/raster.h> |
|
#include <stdio.h> |
|
#include <stdlib.h> |
|
#include <string.h> |
|
#include <unistd.h> |
|
#include <fcntl.h> |
|
#include <sys/wait.h> |
|
#include <spawn.h> |
|
|
|
extern char **environ; |
|
|
|
static const char *foo2xqx_path(void) { |
|
const char *e = getenv("FOO2XQX"); |
|
return (e && *e) ? e : "/usr/libexec/cups/filter/foo2xqx"; |
|
} |
|
|
|
/* Map page size (points) to foo2xqx paper code + clip offset (at 1200x600). */ |
|
static void paper_params(double wpt, double hpt, int *pcode, int *xoff, int *yoff) { |
|
struct { double w, h; int p, x, y; } tab[] = { |
|
{595, 842, 9, 176, 84}, /* A4 */ |
|
{612, 792, 1, 177, 84}, /* Letter */ |
|
{612,1008, 5, 177, 96}, /* Legal */ |
|
{420, 595, 11, 192, 96}, /* A5 */ |
|
{522, 756, 7, 192, 96}, /* Executive */ |
|
}; |
|
int best = 0; double bd = 1e18; |
|
for (unsigned i = 0; i < sizeof(tab)/sizeof(tab[0]); i++) { |
|
double d = (wpt-tab[i].w)*(wpt-tab[i].w) + (hpt-tab[i].h)*(hpt-tab[i].h); |
|
if (d < bd) { bd = d; best = i; } |
|
} |
|
*pcode = tab[best].p; *xoff = tab[best].x; *yoff = tab[best].y; |
|
} |
|
|
|
int main(int argc, char *argv[]) { |
|
int fd = 0; |
|
if (argc > 6 && argv[6] && argv[6][0]) { |
|
fd = open(argv[6], O_RDONLY); |
|
if (fd < 0) { perror("rastertoxqx: open"); return 1; } |
|
} |
|
|
|
int copies = 1; |
|
if (argc > 4 && argv[4]) { copies = atoi(argv[4]); if (copies < 1) copies = 1; } |
|
/* Density from options string: "...foo2xqxDensity=N..."; default 5. */ |
|
int density = 5; |
|
if (argc > 5 && argv[5]) { |
|
const char *p = strstr(argv[5], "foo2xqxDensity="); |
|
if (p) { int d = atoi(p + 15); if (d >= 1 && d <= 5) density = d; } |
|
} |
|
|
|
cups_raster_t *ras = cupsRasterOpen(fd, CUPS_RASTER_READ); |
|
if (!ras) { fprintf(stderr, "ERROR: rastertoxqx: cannot open raster stream\n"); return 1; } |
|
|
|
cups_page_header2_t h; |
|
if (!cupsRasterReadHeader2(ras, &h)) { |
|
fprintf(stderr, "ERROR: rastertoxqx: empty raster (no pages)\n"); |
|
cupsRasterClose(ras); return 1; |
|
} |
|
|
|
int rx = h.HWResolution[0], ry = h.HWResolution[1]; |
|
int pcode, xoff, yoff; |
|
paper_params(h.cupsPageSize[0], h.cupsPageSize[1], &pcode, &xoff, &yoff); |
|
if (rx == 600) xoff /= 2; /* wrapper halves x offset at 600 dpi */ |
|
|
|
char sres[32], sdim[32], sp[8], sn[8], sT[8], su[24], sl[24]; |
|
snprintf(sres, sizeof sres, "%dx%d", rx, ry); |
|
snprintf(sdim, sizeof sdim, "%ux%u", h.cupsWidth, h.cupsHeight); |
|
snprintf(sp, sizeof sp, "%d", pcode); |
|
snprintf(sn, sizeof sn, "%d", copies); |
|
snprintf(sT, sizeof sT, "%d", density); |
|
snprintf(su, sizeof su, "%dx%d", xoff, yoff); |
|
snprintf(sl, sizeof sl, "%dx%d", xoff, yoff); |
|
|
|
char *fargv[] = { (char*)foo2xqx_path(), |
|
"-r", sres, "-g", sdim, "-p", sp, "-m", "1", "-n", sn, |
|
"-d", "1", "-s", "7", "-T", sT, "-u", su, "-l", sl, NULL }; |
|
|
|
/* Pipe our generated pbmraw into foo2xqx; foo2xqx writes XQX to our stdout. */ |
|
int pfd[2]; |
|
if (pipe(pfd) < 0) { perror("pipe"); return 1; } |
|
posix_spawn_file_actions_t fa; |
|
posix_spawn_file_actions_init(&fa); |
|
posix_spawn_file_actions_adddup2(&fa, pfd[0], 0); /* child stdin = pipe read */ |
|
posix_spawn_file_actions_addclose(&fa, pfd[1]); |
|
pid_t pid; |
|
if (posix_spawn(&pid, fargv[0], &fa, NULL, fargv, environ) != 0) { |
|
fprintf(stderr, "ERROR: rastertoxqx: cannot exec %s\n", fargv[0]); |
|
return 1; |
|
} |
|
posix_spawn_file_actions_destroy(&fa); |
|
close(pfd[0]); |
|
FILE *out = fdopen(pfd[1], "wb"); |
|
|
|
unsigned rowbytes = (h.cupsWidth + 7) / 8; |
|
unsigned char *line = malloc(h.cupsBytesPerLine ? h.cupsBytesPerLine : rowbytes); |
|
unsigned char *pbm = malloc(rowbytes); |
|
int page = 0; |
|
|
|
do { |
|
page++; |
|
/* PBM (P4) header for this page. */ |
|
fprintf(out, "P4\n%u %u\n", h.cupsWidth, h.cupsHeight); |
|
for (unsigned y = 0; y < h.cupsHeight; y++) { |
|
cupsRasterReadPixels(ras, line, h.cupsBytesPerLine); |
|
if (h.cupsBitsPerColor == 1) { |
|
memcpy(pbm, line, rowbytes); |
|
/* PBM: 1=black. K:1=black (copy). W:1=white (invert). */ |
|
if (h.cupsColorSpace == CUPS_CSPACE_W || h.cupsColorSpace == CUPS_CSPACE_SW) |
|
for (unsigned b = 0; b < rowbytes; b++) pbm[b] = ~pbm[b]; |
|
} else { |
|
/* 8-bit (or deeper) grayscale -> threshold to 1-bit. */ |
|
unsigned bpp = h.cupsBitsPerPixel / 8; if (!bpp) bpp = 1; |
|
memset(pbm, 0, rowbytes); |
|
for (unsigned x = 0; x < h.cupsWidth; x++) { |
|
unsigned v = line[x * bpp]; |
|
int black = (h.cupsColorSpace == CUPS_CSPACE_K || |
|
h.cupsColorSpace == CUPS_CSPACE_SW || |
|
h.cupsColorSpace == CUPS_CSPACE_W) |
|
? ((h.cupsColorSpace == CUPS_CSPACE_K) ? (v >= 128) : (v < 128)) |
|
: (v < 128); |
|
if (black) pbm[x >> 3] |= (0x80 >> (x & 7)); |
|
} |
|
} |
|
fwrite(pbm, 1, rowbytes, out); |
|
} |
|
} while (cupsRasterReadHeader2(ras, &h)); |
|
|
|
free(line); free(pbm); |
|
fclose(out); |
|
cupsRasterClose(ras); |
|
if (fd) close(fd); |
|
|
|
int st = 0; waitpid(pid, &st, 0); |
|
fprintf(stderr, "INFO: rastertoxqx: %d page(s)\n", page); |
|
return (WIFEXITED(st) && WEXITSTATUS(st) == 0) ? 0 : 1; |
|
} |
|
RXEOF |
|
cc -O2 -Wall -o rastertoxqx rastertoxqx.c -lcupsimage -lcups |
|
|
|
echo ">> Installing filters into $FILTERDIR" |
|
install -m 755 -o root -g wheel rastertoxqx "$FILTERDIR/rastertoxqx" |
|
install -m 755 -o root -g wheel foo2xqx "$FILTERDIR/foo2xqx" |
|
|
|
echo ">> Installing firmware + hotplug daemon" |
|
install -d -m 755 "$FWDIR" |
|
install -m 644 sihpP1005.dl "$FWDIR/" |
|
install -m 755 osx-hplj-hotplug /usr/local/bin/ |
|
cat > /Library/LaunchDaemons/com.foo2zjs.hpljhotplug.plist <<'PLEOF' |
|
<?xml version="1.0" encoding="UTF-8"?> |
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> |
|
<plist version="1.0"><dict> |
|
<key>Label</key><string>com.foo2zjs.hpljhotplug</string> |
|
<key>ProgramArguments</key><array><string>/usr/local/bin/osx-hplj-hotplug</string></array> |
|
<key>RunAtLoad</key><true/> |
|
<key>KeepAlive</key><true/> |
|
<key>StandardOutPath</key><string>/var/log/osx-hplj-hotplug.log</string> |
|
<key>StandardErrorPath</key><string>/var/log/osx-hplj-hotplug.log</string> |
|
</dict></plist> |
|
PLEOF |
|
launchctl unload /Library/LaunchDaemons/com.foo2zjs.hpljhotplug.plist 2>/dev/null || true |
|
launchctl load /Library/LaunchDaemons/com.foo2zjs.hpljhotplug.plist |
|
|
|
echo ">> Writing PPD" |
|
cat > "$work/HP-LaserJet-P1005.ppd" <<'PPDEOF' |
|
*PPD-Adobe: "4.3" |
|
*FormatVersion: "4.3" |
|
*FileVersion: "1.0" |
|
*LanguageVersion: English |
|
*LanguageEncoding: ISOLatin1 |
|
*PCFileName: "P1005.PPD" |
|
*Manufacturer: "HP" |
|
*ModelName: "HP LaserJet P1005" |
|
*Product: "(HP LaserJet P1005)" |
|
*NickName: "HP LaserJet P1005 rastertoxqx" |
|
*ShortNickName: "HP LaserJet P1005 rastertoxqx" |
|
*PSVersion: "(3010.000) 0" |
|
*LanguageLevel: "3" |
|
*ColorDevice: False |
|
*DefaultColorSpace: Gray |
|
*FileSystem: False |
|
*cupsVersion: 2.3 |
|
*cupsModelNumber: 0 |
|
*cupsManualCopies: True |
|
*cupsFilter: "application/vnd.cups-raster 50 rastertoxqx" |
|
*OpenUI *PageSize/Media Size: PickOne |
|
*OrderDependency: 10 AnySetup *PageSize |
|
*DefaultPageSize: A4 |
|
*PageSize A4/A4: "<</PageSize[595 842]>>setpagedevice" |
|
*PageSize Letter/US Letter: "<</PageSize[612 792]>>setpagedevice" |
|
*CloseUI: *PageSize |
|
*OpenUI *PageRegion/Media Size: PickOne |
|
*DefaultPageRegion: A4 |
|
*PageRegion A4/A4: "<</PageSize[595 842]>>setpagedevice" |
|
*PageRegion Letter/US Letter: "<</PageSize[612 792]>>setpagedevice" |
|
*CloseUI: *PageRegion |
|
*DefaultImageableArea: A4 |
|
*ImageableArea A4/A4: "0 0 595 842" |
|
*ImageableArea Letter/US Letter: "0 0 612 792" |
|
*DefaultPaperDimension: A4 |
|
*PaperDimension A4/A4: "595 842" |
|
*PaperDimension Letter/US Letter: "612 792" |
|
*OpenUI *Resolution/Resolution: PickOne |
|
*OrderDependency: 20 AnySetup *Resolution |
|
*DefaultResolution: 1200x600dpi |
|
*Resolution 600x600dpi/600 dpi: "<</HWResolution[600 600]/cupsBitsPerColor 1/cupsColorSpace 3/cupsColorOrder 0>>setpagedevice" |
|
*Resolution 1200x600dpi/FastRes 1200: "<</HWResolution[1200 600]/cupsBitsPerColor 1/cupsColorSpace 3/cupsColorOrder 0>>setpagedevice" |
|
*CloseUI: *Resolution |
|
PPDEOF |
|
|
|
echo ">> Detecting printer on USB" |
|
URI="$(lpinfo -v 2>/dev/null | awk '/usb:.*[Pp]1005/{print $2; exit}')" |
|
[ -n "$URI" ] || URI="$(lpinfo -v 2>/dev/null | awk '/LaserJet.?P1005/{print $2; exit}')" |
|
if [ -z "$URI" ]; then |
|
echo "!! Printer not found on USB. Power it on / reconnect, then re-run." >&2 |
|
exit 1 |
|
fi |
|
echo " $URI" |
|
|
|
echo ">> Configuring queue '$QUEUE'" |
|
lpadmin -p "$QUEUE" -E -v "$URI" -P "$work/HP-LaserJet-P1005.ppd" |
|
cupsenable "$QUEUE"; cupsaccept "$QUEUE" |
|
|
|
echo ">> Loading firmware now (also auto-loads on every power-on)" |
|
lp -d "$QUEUE" -o raw "$FWDIR/sihpP1005.dl" >/dev/null 2>&1 || true |
|
|
|
echo |
|
echo "Done. Test with: lp -d $QUEUE /path/to/any.pdf" |