Compare commits
18
Commits
db60fdd635
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
51a7ae829d | ||
|
|
c2f8673171 | ||
|
|
dfe839531f | ||
|
|
9cb8657aa7 | ||
|
|
118b0faca0 | ||
|
|
5bd9a0277a | ||
|
|
49c0a1f848 | ||
|
|
04cbaf92da | ||
|
|
4f9daa2d75 | ||
|
|
50dec44d4a | ||
|
|
bc0329861c | ||
|
|
d944e2443d | ||
|
|
49ab635018 | ||
|
|
183a265d77 | ||
|
|
fc57b48eb8 | ||
|
|
ca895889f9 | ||
|
|
e938e90284 | ||
|
|
54ee01c475 |
No files matched your search
@@ -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 *)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
Executable
+48
@@ -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
@@ -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)'"
|
||||||
Executable
+53
@@ -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
|
||||||
Executable
+6
@@ -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
@@ -24,8 +24,8 @@
|
|||||||
*PSVersion: "(3011.103) 1"
|
*PSVersion: "(3011.103) 1"
|
||||||
*Manufacturer: "UTAX/TA"
|
*Manufacturer: "UTAX/TA"
|
||||||
*ModelName: "3505ci KPDL"
|
*ModelName: "3505ci KPDL"
|
||||||
*ShortNickName: "3505ci (KPDL)"
|
*ShortNickName: "3505ci (kydriv)"
|
||||||
*NickName: "3505ci (KPDL)"
|
*NickName: "3505ci (kydriv)"
|
||||||
*PCFileName: "TA3505ci.PPD"
|
*PCFileName: "TA3505ci.PPD"
|
||||||
|
|
||||||
*1284DeviceID: "MDL:3505ci;MFG:UTAX"
|
*1284DeviceID: "MDL:3505ci;MFG:UTAX"
|
||||||
@@ -3416,16 +3416,14 @@ userdict /180rotdetail true put
|
|||||||
*OpenGroup: AccountingOptions/Job Accounting
|
*OpenGroup: AccountingOptions/Job Accounting
|
||||||
|
|
||||||
*% ---------------------------------------------------------
|
*% ---------------------------------------------------------
|
||||||
*% Kostenstelle (5-stellig) eintragen - ohne fuehrende Nullen.
|
*% Kostenstelle eintragen. Zeile kopieren und Zahl anpassen:
|
||||||
*% Zeile kopieren und Zahl anpassen:
|
|
||||||
*% *KmManagment MG12345/Code 12345: ""
|
*% *KmManagment MG12345/Code 12345: ""
|
||||||
*% Zero-Padding auf 8 Stellen uebernimmt der Filter.
|
|
||||||
*% ---------------------------------------------------------
|
*% ---------------------------------------------------------
|
||||||
*OpenUI *KmManagment/Job Accounting: PickOne
|
*OpenUI *KmManagment/Job Accounting: PickOne
|
||||||
*OrderDependency: 60 AnySetup *KmManagment
|
*OrderDependency: 60 AnySetup *KmManagment
|
||||||
*DefaultKmManagment: Default
|
*DefaultKmManagment: Default
|
||||||
\*KmManagment Default/Off: ""
|
*KmManagment Default/Off: ""
|
||||||
*KmManagment MG12345/Code 12345: ""
|
*KmManagment MG39321/Code 39321: ""
|
||||||
*?KmManagment: ""
|
*?KmManagment: ""
|
||||||
*End
|
*End
|
||||||
*CloseUI: *KmManagment
|
*CloseUI: *KmManagment
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
Reference in new issue
Block a user