Add cover page support with Chinese font rendering

Features:
- Cover page generation using Python/Pillow with Apple PingFang font
- CLI commands: cover-create, job-cover for cover page management
- API endpoints: PUT /api/fax/jobs/{id}/cover for cover modification
- Send fax with cover page: --to, --from, --subject, --note options
- Multi-page TIFF parsing for PDF conversion (fix: all pages now sent)
- Preview HTML generation with pan/zoom support
- OCR verification for cover page content
- Class 2 receive support for incoming faxes

Fixes:
- Modem initialization before Class 2 mode entry
- Dial timeout handling with retry logic
- Multi-page fax transmission between pages
- Chinese character rendering in cover pages (Apple native fonts)
This commit is contained in:
Warren
2026-07-02 18:43:16 +08:00
parent b102917644
commit d1e92b32fb
20 changed files with 1849 additions and 83 deletions
+177
View File
@@ -0,0 +1,177 @@
#!/usr/bin/env python3
"""Class 1 Fax Receiver - for testing with USR sender"""
import termios
import os
import time
import sys
def main():
if len(sys.argv) < 2:
print("Usage: class1_receive.py <device>")
sys.exit(1)
device = sys.argv[1]
fd = os.open(device, os.O_RDWR | os.O_NOCTTY | os.O_NONBLOCK)
attrs = termios.tcgetattr(fd)
attrs[4] = termios.B115200
attrs[5] = termios.B115200
attrs[2] |= termios.CLOCAL | termios.CREAD
attrs[3] &= ~termios.ICANON
attrs[3] &= ~termios.ECHO
attrs[3] &= ~termios.ECHOE
attrs[3] &= ~termios.ISIG
termios.tcsetattr(fd, termios.TCSANOW, attrs)
termios.tcflush(fd, termios.TCIOFLUSH)
def drain():
try:
while True:
os.read(fd, 4096)
except BlockingIOError:
pass
def send_cmd(cmd, wait=0.5):
drain()
os.write(fd, cmd.encode() + b"\r")
time.sleep(wait)
result = b""
try:
while True:
data = os.read(fd, 4096)
result += data
except BlockingIOError:
pass
return result.decode(errors="replace")
def wait_for(pattern, timeout=30):
start = time.time()
result = b""
while time.time() - start < timeout:
try:
data = os.read(fd, 4096)
result += data
decoded = result.decode(errors="replace")
for p in pattern if isinstance(pattern, list) else [pattern]:
if p in decoded:
return decoded
except BlockingIOError:
time.sleep(0.1)
return result.decode(errors="replace")
print("[RECV] Initializing Class 1...")
send_cmd("AT", 0.3)
send_cmd("AT+FCLASS=1", 0.3)
print("[RECV] Waiting for rings...")
ring_count = 0
while ring_count < 2:
try:
data = os.read(fd, 4096)
text = data.decode(errors="replace")
if "RING" in text:
ring_count += text.count("RING")
print(f"[RECV] Ring {ring_count}")
except BlockingIOError:
time.sleep(0.3)
print("[RECV] Answering...")
answer_result = send_cmd("ATA", 3)
print(f"[RECV] ATA result: {answer_result.strip()}")
if "CONNECT" not in answer_result:
drain()
os.write(fd, b"ATA\r")
wait_result = wait_for("CONNECT", 10)
print(f"[RECV] Wait result: {wait_result.strip()}")
print("[RECV] Connected! Sending CED tone...")
time.sleep(0.5)
# Send CED tone (2100Hz for 2.6-4 seconds)
ced_result = send_cmd("AT+FTS=8", 4) # FTS=8 sends 2100Hz
print(f"[RECV] CED result: {ced_result.strip()}")
# Wait 75ms then send DIS
time.sleep(0.1)
print("[RECV] Sending DIS frame...")
# Start HDLC transmit
os.write(fd, b"AT+FTH=3\r")
fth_result = wait_for("CONNECT", 5)
print(f"[RECV] FTH=3: {fth_result.strip()}")
if "CONNECT" in fth_result:
# DIS frame: preamble+flags+DCS+data+RTC
# Address: 0xFF (broadcast), Control: 0x80 (DIS from called)
# DIS data: minimum capabilities
dis_data = bytes([
0xFF, 0x13, 0x80, # Address + Control (DIS from called)
0x00, 0x50, 0x1F, # DIS parameters
])
# Add FCS (we'll let modem calculate)
os.write(fd, dis_data)
os.write(fd, b"\x00\x00\x00") # Padding
time.sleep(0.5)
# Wait for OK
dis_ok = wait_for("OK", 5)
print(f"[RECV] DIS sent: {dis_ok.strip()}")
print("[RECV] Waiting for DCS...")
drain()
os.write(fd, b"AT+FRH=3\r")
dcs_result = wait_for("OK", 10)
print(f"[RECV] DCS received: {len(dcs_result)} bytes")
print("[RECV] Waiting for TCF...")
drain()
os.write(fd, b"AT+FRM=96\r") # Receive at 14400 bps
tcf_result = wait_for("CONNECT", 5)
print(f"[RECV] TCF: {tcf_result.strip()}")
# Send CFR
print("[RECV] Sending CFR...")
drain()
os.write(fd, b"AT+FTH=3\r")
wait_for("CONNECT", 5)
cfr_data = bytes([0xFF, 0x13, 0x21]) # CFR frame
os.write(fd, cfr_data)
os.write(fd, b"\x00\x00\x00")
time.sleep(0.5)
wait_for("OK", 5)
print("[RECV] CFR sent")
print("[RECV] Waiting for page data...")
# Receive page
drain()
os.write(fd, b"AT+FRM=96\r")
page_result = wait_for("OK", 60)
print(f"[RECV] Page received!")
# Send MCF
print("[RECV] Sending MCF...")
drain()
os.write(fd, b"AT+FTH=3\r")
wait_for("CONNECT", 5)
mcf_data = bytes([0xFF, 0x13, 0x31]) # MCF frame
os.write(fd, mcf_data)
os.write(fd, b"\x00\x00\x00")
time.sleep(0.5)
wait_for("OK", 5)
print("[RECV] MCF sent")
# Wait for DCN or hangup
print("[RECV] Waiting for DCN...")
drain()
os.write(fd, b"AT+FRH=3\r")
dcn_result = wait_for("OK", 10)
print(f"[RECV] Session ended: {dcn_result[:50]}")
os.close(fd)
print("[RECV] Done!")
if __name__ == "__main__":
main()