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
+61 -2
View File
@@ -2,7 +2,7 @@ use axum::{
extract::State,
http::StatusCode,
response::Json,
routing::{get, post},
routing::{get, post, put},
Router,
};
use serde::{Deserialize, Serialize};
@@ -30,6 +30,10 @@ pub struct HealthResponse {
pub struct SendFaxRequest {
pub recipient: String,
pub document_path: String,
pub cover_to: Option<String>,
pub cover_from: Option<String>,
pub cover_subject: Option<String>,
pub cover_notes: Option<String>,
}
#[derive(Serialize)]
@@ -37,6 +41,23 @@ pub struct SendFaxResponse {
pub job_id: JobId,
}
#[derive(Deserialize)]
pub struct UpdateCoverRequest {
pub to: Option<String>,
pub from: Option<String>,
pub subject: Option<String>,
pub notes: Option<String>,
}
#[derive(Serialize)]
pub struct UpdateCoverResponse {
pub job_id: JobId,
pub cover_to: Option<String>,
pub cover_from: Option<String>,
pub cover_subject: Option<String>,
pub cover_notes: Option<String>,
}
#[derive(Serialize)]
pub struct JobResponse {
pub id: JobId,
@@ -44,6 +65,10 @@ pub struct JobResponse {
pub status: String,
pub pages: u32,
pub created_at: String,
pub cover_to: Option<String>,
pub cover_from: Option<String>,
pub cover_subject: Option<String>,
pub cover_notes: Option<String>,
}
async fn health(State(state): State<AppState>) -> Json<HealthResponse> {
@@ -57,7 +82,11 @@ async fn send_fax(
State(state): State<AppState>,
Json(req): Json<SendFaxRequest>,
) -> Result<Json<SendFaxResponse>, StatusCode> {
let job = FaxJob::new(req.recipient, req.document_path);
let mut job = FaxJob::new(req.recipient, req.document_path);
job.cover_to = req.cover_to;
job.cover_from = req.cover_from;
job.cover_subject = req.cover_subject;
job.cover_notes = req.cover_notes;
let job_id = job.id;
let mut queue = state.queue.lock().unwrap();
@@ -78,6 +107,10 @@ async fn list_jobs(State(state): State<AppState>) -> Json<Vec<JobResponse>> {
status: format!("{:?}", j.status),
pages: j.pages,
created_at: j.created_at.to_rfc3339(),
cover_to: j.cover_to,
cover_from: j.cover_from,
cover_subject: j.cover_subject,
cover_notes: j.cover_notes,
})
.collect();
Json(responses)
@@ -94,9 +127,34 @@ async fn get_job(
status: format!("{:?}", j.status),
pages: j.pages,
created_at: j.created_at.to_rfc3339(),
cover_to: j.cover_to,
cover_from: j.cover_from,
cover_subject: j.cover_subject,
cover_notes: j.cover_notes,
})).ok_or(StatusCode::NOT_FOUND)
}
async fn update_job_cover(
State(state): State<AppState>,
axum::extract::Path(id): axum::extract::Path<JobId>,
Json(req): Json<UpdateCoverRequest>,
) -> Result<Json<UpdateCoverResponse>, StatusCode> {
let mut queue = state.queue.lock().unwrap();
queue
.update_cover(&id, req.to.clone(), req.from.clone(), req.subject.clone(), req.notes.clone())
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
// Return updated fields
let job = queue.get(&id).ok_or(StatusCode::NOT_FOUND)?;
Ok(Json(UpdateCoverResponse {
job_id: job.id,
cover_to: job.cover_to,
cover_from: job.cover_from,
cover_subject: job.cover_subject,
cover_notes: job.cover_notes,
}))
}
pub async fn start_server(config: FaxConfig, queue: Arc<Mutex<FaxQueue>>) -> FaxResult<()> {
let state = AppState {
config: config.clone(),
@@ -108,6 +166,7 @@ pub async fn start_server(config: FaxConfig, queue: Arc<Mutex<FaxQueue>>) -> Fax
.route("/api/fax/send", post(send_fax))
.route("/api/fax/jobs", get(list_jobs))
.route("/api/fax/jobs/{id}", get(get_job))
.route("/api/fax/jobs/{id}/cover", put(update_job_cover))
.with_state(state);
let addr = config