paperdown 0.2.0

A fast CLI tool to batch convert PDFs into Markdown using GLM-OCR.
Documentation
use anyhow::{Context, Result, anyhow};
use base64::Engine;
use base64::engine::general_purpose::STANDARD;
use serde_json::{Value, json};
use std::path::Path;
use tokio::fs;

const API_URL: &str = "https://api.z.ai/api/paas/v4/layout_parsing";

pub(crate) async fn build_payload(pdf_path: &Path) -> Result<Value> {
    let bytes = fs::read(pdf_path).await?;
    let encoded = STANDARD.encode(bytes);
    Ok(json!({
        "model": "glm-ocr",
        "file": format!("data:application/pdf;base64,{encoded}"),
        "return_crop_images": true
    }))
}

pub(crate) async fn call_layout_parsing(
    client: &reqwest::Client,
    api_key: &str,
    payload: Value,
) -> Result<Value> {
    let response = client
        .post(API_URL)
        .header("Authorization", format!("Bearer {api_key}"))
        .json(&payload)
        .send()
        .await
        .context("Could not reach Z.AI OCR API")?;

    let status = response.status();
    let text = response.text().await?;
    if !status.is_success() {
        return Err(anyhow!(
            "Z.AI OCR request failed with HTTP {}: {}",
            status.as_u16(),
            text
        ));
    }

    let parsed: Value =
        serde_json::from_str(&text).context("Z.AI OCR API returned invalid JSON")?;
    if !parsed.is_object() {
        return Err(anyhow!("Z.AI OCR API returned an unexpected response type"));
    }
    Ok(parsed)
}

pub(crate) fn validate_layout_response(data: Value) -> Result<(String, Vec<Value>, Option<Value>)> {
    let markdown = data
        .get("md_results")
        .and_then(Value::as_str)
        .ok_or_else(|| anyhow!("Z.AI OCR response is missing string field 'md_results'"))?
        .to_string();

    let layout_details = data
        .get("layout_details")
        .and_then(Value::as_array)
        .ok_or_else(|| anyhow!("Z.AI OCR response is missing list field 'layout_details'"))?
        .clone();

    let usage = data.get("usage").filter(|v| v.is_object()).cloned();
    Ok((markdown, layout_details, usage))
}