Compare commits

..
18 Commits
Author SHA1 Message Date
hofmannolandClaude Sonnet 4.6 51a7ae829d docs: correct preset navigation path to Druckeroptionen → Auftragszählung
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 23:47:09 +02:00
hofmannolandClaude Sonnet 4.6 c2f8673171 docs: add printer options configuration step to installation guide
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 23:43:02 +02:00
hofmannolandClaude Sonnet 4.6 dfe839531f docs: add macOS installation guide with cost center preset instructions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 23:34:38 +02:00
Oliver Hofmann 9cb8657aa7 chore: bump version to 1.0.1 2026-06-06 23:11:18 +02:00
Oliver Hofmann 118b0faca0 fix: rewrite filter as bash+awk to avoid Python/xcode-select dependency 2026-06-06 20:56:28 +02:00
Oliver Hofmann 5bd9a0277a fix: use absolute python3 path to avoid xcode-select in CUPS root context 2026-06-06 20:51:54 +02:00
Oliver Hofmann 49c0a1f848 fix: prevent set -e from killing build when grep finds no matches 2026-06-06 16:54:17 +02:00
Oliver Hofmann 04cbaf92da fix: tighten PPD validation to only catch structural errors 2026-06-06 16:50:31 +02:00
Oliver Hofmann 4f9daa2d75 chore: add install.sh for manual installation 2026-06-06 16:19:45 +02:00
Oliver Hofmann 50dec44d4a fix: use raw code without zero-padding, matching kyofilter_E behaviour 2026-06-06 16:13:52 +02:00
Oliver Hofmann bc0329861c Kostenstelle eingetragen 2026-06-05 16:45:58 +02:00
Oliver Hofmann d944e2443d fix: PPD backslash typo, >8-digit code guard, NickName, build validation 2026-06-02 21:19:35 +02:00
Oliver Hofmann 49ab635018 feat: pkg installer with postinstall and build script 2026-06-02 21:06:51 +02:00
Oliver Hofmann 183a265d77 feat: complete kyofilter with CUPS main() entrypoint 2026-06-02 21:03:52 +02:00
Oliver Hofmann fc57b48eb8 fix: robustify EndSetup detection and add warning test 2026-06-02 21:03:13 +02:00
Oliver Hofmann ca895889f9 feat: PostScript stream processing with account code injection 2026-06-02 21:01:55 +02:00
Oliver Hofmann e938e90284 feat: account code extraction with zero-padding 2026-06-02 21:00:21 +02:00
Oliver Hofmann 54ee01c475 feat: filter option parsing with tests 2026-06-02 20:57:33 +02:00
9 changed files with 329 additions and 7 deletions

No files matched your search

+33
View File
@@ -0,0 +1,33 @@
{
"permissions": {
"allow": [
"Bash(git -C /Users/oliver/Development/Projekte/kydriv log --oneline)",
"Bash(pip3 install *)",
"Bash(git add *)",
"Bash(git commit *)",
"Bash(python3 *)",
"Bash(git *)",
"Bash(cupstestppd *)",
"Bash(echo \"Exit code: $?\")",
"Bash(Read the *)",
"Bash(awk -F'.' '{print $1\".\"$2}')",
"Bash(chmod 644 /Users/oliver/Development/Projekte/kydriv/ppd/TA3505ci_AS.ppd)",
"Bash(python *)",
"Bash(chmod +x *)",
"Bash(pytest *)",
"Bash(echo \"exit: $?\")",
"Bash(bash *)",
"Bash(pkgutil *)",
"Read(//tmp/kydriv_expanded/kydriv-driver-1.0.0.pkg/Scripts/**)",
"Read(//tmp/kydriv_expanded/**)",
"Bash(rm -rf /tmp/kydriv_expanded)",
"Read(//private/tmp/kydriv_expanded/**)",
"Bash(xxd)",
"Read(//usr/bin/**)",
"Read(//usr/libexec/cups/**)",
"Bash(lsbom /tmp/kydriv_pkg_inspect/Bom)",
"Bash(csrutil status *)",
"Bash(xcrun notarytool *)"
]
}
}
+79
View File
@@ -0,0 +1,79 @@
# Kyocera TASKalfa 3505ci – Treiber-Installation unter macOS (Apple Silicon)
## Hintergrund
Der offizielle Kyocera-Treiber überträgt seit einer neueren Version keine Kostenstellen-Codes mehr an den Drucker. Dieser alternative Treiber stellt diese Funktion wieder her.
---
## Voraussetzungen
- Mac mit Apple Silicon (M1 oder neuer)
- macOS 13 Ventura oder neuer
- Netzwerkzugang zum Drucker (IP `141.75.11.43`)
---
## 1. Treiber installieren
1. `kydriv-driver-1.0.1.pkg` herunterladen
2. Doppelklick auf die Datei → Installer folgen
3. Das Paket ist von Apple notariell beglaubigt, eine Gatekeeper-Warnung erscheint **nicht**
Der Installer kopiert:
- den PPD in `/Library/Printers/PPDs/Contents/Resources/`
- den Filter `kyofilter` in `/usr/libexec/cups/filter/`
---
## 2. Drucker einrichten
1. **Systemeinstellungen → Drucker & Scanner** öffnen
2. `+` klicken → Reiter **IP**
3. Felder ausfüllen:
| Feld | Wert |
|---|---|
| Protokoll | **LPD** |
| Adresse | `141.75.11.43` |
| Warteschlange | *(leer lassen)* |
| Name | `Kyocera TASKalfa 3505ci` |
4. Bei **Verwenden** → **Andere auswählen…** → PPD-Datei `TA3505ci_AS.ppd` aus dem Finder wählen (liegt nach der Installation unter `/Library/Printers/PPDs/Contents/Resources/`)
5. **Hinzufügen** klicken
6. Es erscheint ein Konfigurationsdialog für installierte Optionen. Folgende Werte einstellen:
| Option | Wert |
|---|---|
| Papierzuführungen | `Kassetten 3, 4` |
| Finisher | `4000-Blatt-Finisher` |
| Optionaler Datenträger | `Festplatte` |
| Innere Auftragstrennung | aus |
| Rechte Auftragstrennung | aus |
| Mailbox | aus |
| Stanzeinheit | aus |
| Falteinheit | aus |
Anschließend **OK** klicken.
---
## 3. Preset mit Kostenstellen-Code einrichten
Damit der Drucker den Auftrag der richtigen Kostenstelle zuordnet, muss der Code bei jedem Druckjob übergeben werden. Das geht am einfachsten über ein gespeichertes Preset.
1. Ein beliebiges Dokument öffnen und **Ablage → Drucken** (`⌘P`) aufrufen
2. Sicherstellen, dass `Kyocera TASKalfa 3505ci` als Drucker ausgewählt ist
3. Den Abschnitt **Optionen** aufklappen und auf **Druckeroptionen** klicken
4. Im erscheinenden Dialog bei **Optionengruppen** → `Auftragszählung` wählen
5. Bei **Auftragszählung** den eigenen Kostenstellen-Code auswählen (z. B. `Code 39321`) → **OK**
6. Oben im Druckdialog auf **Voreinstellungen → Aktuelle Einstellungen als Voreinstellung sichern…** klicken
7. Einen Namen vergeben (z. B. `Kostenstelle 39321`) und auf **OK** klicken
Ab sofort steht das Preset im Druckdialog unter **Voreinstellungen** zur Verfügung – der Code wird automatisch mitgeschickt.
---
## Weitere Kostenstellen-Codes
Soll ein weiterer Code im Treiber ergänzt werden, bitte bei der IT-Administration melden. Das erfordert eine neue Treiberversion.
+48
View File
@@ -0,0 +1,48 @@
#!/bin/bash
# CUPS filter for TA 3505ci - injects department code into PostScript stream.
# Uses bash+awk only, no Python dependency.
if [ $# -lt 5 ]; then
echo "Usage: kyofilter job-id user title copies options [file]" >&2
exit 1
fi
OPTIONS="${5:-}"
INFILE="${6:-}"
# Extract department code: KmManagment=MGxxxxx -> xxxxx
CODE=""
for OPT in $OPTIONS; do
if [[ "$OPT" =~ ^KmManagment=MG([0-9]+)$ ]]; then
CODE="${BASH_REMATCH[1]}"
break
fi
done
# Pure passthrough when no code selected
if [ -z "$CODE" ]; then
if [ -n "$INFILE" ]; then
cat "$INFILE"
else
cat
fi
exit 0
fi
# Inject PostScript command before %%EndSetup (fallback: before %%Page:)
INJECT="($CODE) statusdict /setmanagementnumber get exec"
INPUT="${INFILE:--}"
awk -v inject="$INJECT" '
BEGIN { injected = 0 }
!injected && /^%%EndSetup/ {
print inject
injected = 1
}
!injected && /^%%Page:/ {
print inject
injected = 1
}
{ print }
END { if (!injected) print "kyofilter: WARNING: no injection point found" > "/dev/stderr" }
' "$INPUT"
Executable
+18
View File
@@ -0,0 +1,18 @@
#!/bin/bash
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
echo "kydriv - Kyocera 3505ci Treiber installieren"
echo ""
sudo cp "$SCRIPT_DIR/ppd/TA3505ci_AS.ppd" \
/Library/Printers/PPDs/Contents/Resources/TA3505ci_AS.ppd
sudo cp "$SCRIPT_DIR/filter/kyofilter" \
/usr/libexec/cups/filter/kyofilter
sudo chmod 755 /usr/libexec/cups/filter/kyofilter
sudo chown root:wheel /usr/libexec/cups/filter/kyofilter
sudo launchctl kickstart -k system/org.cups.cupsd
echo "Fertig. Jetzt in Systemeinstellungen → Drucker & Scanner:"
echo " + → IP → Adresse eingeben → Verwenden: '3505ci (kydriv)'"
+53
View File
@@ -0,0 +1,53 @@
#!/bin/bash
set -e
VERSION="1.0.1"
IDENTIFIER="de.kydriv.driver"
PKG_NAME="kydriv-driver-${VERSION}.pkg"
# Build installer/root from source files
mkdir -p installer/root/Library/Printers/PPDs/Contents/Resources
mkdir -p installer/root/usr/libexec/cups/filter
cp ppd/TA3505ci_AS.ppd \
installer/root/Library/Printers/PPDs/Contents/Resources/
cp filter/kyofilter \
installer/root/usr/libexec/cups/filter/
chmod 755 installer/root/usr/libexec/cups/filter/kyofilter
# Validate PPD — fail only on structural errors, not on known pre-existing issues
echo "Validating PPD..."
UNEXPECTED=$(cupstestppd installer/root/Library/Printers/PPDs/Contents/Resources/TA3505ci_AS.ppd 2>&1 \
| grep "\*\*FAIL\*\*" \
| grep -v "FeedingEdgeConstraint\|kyofilter\|Übersetzungsstring\|Übersetzung") || true
if [ -n "$UNEXPECTED" ]; then
echo "ERROR: PPD has unexpected structural errors:" >&2
echo "$UNEXPECTED" >&2
exit 1
fi
echo "PPD OK"
# Build (unsigned if SIGN_IDENTITY not set)
SIGN_ARGS=()
if [ -n "${SIGN_IDENTITY}" ]; then
SIGN_ARGS=(--sign "${SIGN_IDENTITY}")
fi
pkgbuild \
--root installer/root \
--scripts installer/scripts \
--identifier "${IDENTIFIER}" \
--version "${VERSION}" \
"${SIGN_ARGS[@]}" \
"${PKG_NAME}"
echo ""
echo "Built: ${PKG_NAME}"
if [ -z "${SIGN_IDENTITY}" ]; then
echo ""
echo "To sign: SIGN_IDENTITY='Developer ID Installer: NAME (TEAMID)' bash installer/build.sh"
echo ""
echo "To notarize after signing:"
echo " xcrun notarytool submit ${PKG_NAME} --apple-id YOUR@EMAIL --team-id TEAMID --password APP-PW --wait"
echo " xcrun stapler staple ${PKG_NAME}"
fi
+6
View File
@@ -0,0 +1,6 @@
#!/bin/bash
set -e
chmod 755 /usr/libexec/cups/filter/kyofilter
chown root:wheel /usr/libexec/cups/filter/kyofilter
launchctl kickstart -k system/org.cups.cupsd
exit 0
+5 -7
View File
@@ -24,8 +24,8 @@
*PSVersion: "(3011.103) 1"
*Manufacturer: "UTAX/TA"
*ModelName: "3505ci KPDL"
*ShortNickName: "3505ci (KPDL)"
*NickName: "3505ci (KPDL)"
*ShortNickName: "3505ci (kydriv)"
*NickName: "3505ci (kydriv)"
*PCFileName: "TA3505ci.PPD"
*1284DeviceID: "MDL:3505ci;MFG:UTAX"
@@ -3416,16 +3416,14 @@ userdict /180rotdetail true put
*OpenGroup: AccountingOptions/Job Accounting
*% ---------------------------------------------------------
*% Kostenstelle (5-stellig) eintragen - ohne fuehrende Nullen.
*% Zeile kopieren und Zahl anpassen:
*% Kostenstelle eintragen. Zeile kopieren und Zahl anpassen:
*% *KmManagment MG12345/Code 12345: ""
*% Zero-Padding auf 8 Stellen uebernimmt der Filter.
*% ---------------------------------------------------------
*OpenUI *KmManagment/Job Accounting: PickOne
*OrderDependency: 60 AnySetup *KmManagment
*DefaultKmManagment: Default
\*KmManagment Default/Off: ""
*KmManagment MG12345/Code 12345: ""
*KmManagment Default/Off: ""
*KmManagment MG39321/Code 39321: ""
*?KmManagment: ""
*End
*CloseUI: *KmManagment
+8
View File
@@ -0,0 +1,8 @@
import importlib.util, pathlib, sys
from importlib.machinery import SourceFileLoader
_path = pathlib.Path(__file__).parent.parent / "filter" / "kyofilter"
_spec = importlib.util.spec_from_loader("kyofilter", SourceFileLoader("kyofilter", str(_path)))
_mod = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(_mod)
sys.modules["kyofilter"] = _mod
+79
View File
@@ -0,0 +1,79 @@
import kyofilter
class TestParseOptions:
def test_single_key_value(self):
assert kyofilter.parse_options("KmManagment=MG12345") == {"KmManagment": "MG12345"}
def test_multiple_options(self):
result = kyofilter.parse_options("KmManagment=MG12345 Duplex=DuplexNoTumble")
assert result == {"KmManagment": "MG12345", "Duplex": "DuplexNoTumble"}
def test_empty_string(self):
assert kyofilter.parse_options("") == {}
def test_flag_without_value(self):
assert kyofilter.parse_options("SomeFlag") == {"SomeFlag": "true"}
class TestGetAccountCode:
def test_five_digit_code_returned_as_is(self):
assert kyofilter.get_account_code({"KmManagment": "MG39321"}) == "39321"
def test_one_digit_code_returned_as_is(self):
assert kyofilter.get_account_code({"KmManagment": "MG1"}) == "1"
def test_default_returns_none(self):
assert kyofilter.get_account_code({"KmManagment": "Default"}) is None
def test_missing_key_returns_none(self):
assert kyofilter.get_account_code({}) is None
def test_no_mg_prefix_returns_none(self):
assert kyofilter.get_account_code({"KmManagment": "12345"}) is None
def test_non_numeric_code_returns_none(self):
assert kyofilter.get_account_code({"KmManagment": "MGabc45"}) is None
class TestProcessStream:
def _stream(self, text):
return iter(line.encode() for line in text.splitlines(keepends=True))
def _collect(self, text, code):
return b"".join(kyofilter.process_stream(self._stream(text), code))
def test_passthrough_when_no_code(self):
ps = "%!PS\n%%BeginSetup\n%%EndSetup\n%%Page: 1 1\n"
assert self._collect(ps, None) == ps.encode()
def test_injects_raw_code_before_end_setup(self):
ps = "%!PS\n%%BeginSetup\n%%EndSetup\n%%Page: 1 1\n"
result = self._collect(ps, "39321")
assert b"(39321) statusdict /setmanagementnumber get exec\n%%EndSetup\n" in result
def test_end_setup_preserved_after_injection(self):
ps = "%!PS\n%%BeginSetup\n%%EndSetup\n"
result = self._collect(ps, "39321")
assert b"%%EndSetup\n" in result
def test_fallback_injects_before_first_page_when_no_end_setup(self):
ps = "%!PS\n%%Page: 1 1\nshowpage\n"
result = self._collect(ps, "39321")
assert b"(39321) statusdict /setmanagementnumber get exec\n%%Page: 1 1\n" in result
def test_injects_only_once(self):
ps = "%!PS\n%%BeginSetup\n%%EndSetup\n%%Page: 1 1\n%%Page: 2 1\n"
result = self._collect(ps, "39321")
assert result.count(b"setmanagementnumber") == 1
def test_content_preserved(self):
ps = "%!PS\n%%BeginSetup\n/mydict 10 dict def\n%%EndSetup\nshowpage\n"
result = self._collect(ps, "39321")
assert b"/mydict 10 dict def\n" in result
assert b"showpage\n" in result
def test_warns_when_no_injection_point(self, capsys):
ps = "%!PS\nshowpage\n"
self._collect(ps, "39321")
assert "WARNING" in capsys.readouterr().err