Skip to main content

oxihuman_export/
pdf_stub_export.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! PDF generation stub: cross-reference table + content stream.
6
7/// A minimal PDF object (number + content bytes).
8#[allow(dead_code)]
9#[derive(Debug, Clone)]
10pub struct PdfObject {
11    pub number: u32,
12    pub content: Vec<u8>,
13}
14
15/// A minimal PDF document stub.
16#[allow(dead_code)]
17pub struct PdfStub {
18    pub title: String,
19    pub author: String,
20    pub objects: Vec<PdfObject>,
21    pub page_width_pt: f32,
22    pub page_height_pt: f32,
23}
24
25/// Create a new PDF stub.
26#[allow(dead_code)]
27pub fn new_pdf_stub(title: &str, author: &str) -> PdfStub {
28    PdfStub {
29        title: title.to_string(),
30        author: author.to_string(),
31        objects: Vec::new(),
32        page_width_pt: 595.0,
33        page_height_pt: 842.0,
34    }
35}
36
37/// Add a raw content stream (e.g. drawing commands) as a PDF object.
38#[allow(dead_code)]
39pub fn add_content_stream(stub: &mut PdfStub, content: &str) -> u32 {
40    let number = (stub.objects.len() + 1) as u32;
41    stub.objects.push(PdfObject {
42        number,
43        content: content.as_bytes().to_vec(),
44    });
45    number
46}
47
48/// Serialize the stub to a minimal PDF byte sequence.
49#[allow(dead_code)]
50pub fn export_pdf_stub(stub: &PdfStub) -> Vec<u8> {
51    let mut out: Vec<u8> = Vec::new();
52    out.extend_from_slice(b"%PDF-1.4\n");
53    let mut offsets: Vec<usize> = Vec::new();
54    for obj in &stub.objects {
55        offsets.push(out.len());
56        let header = format!(
57            "{} 0 obj\n<< /Length {} >>\nstream\n",
58            obj.number,
59            obj.content.len()
60        );
61        out.extend_from_slice(header.as_bytes());
62        out.extend_from_slice(&obj.content);
63        out.extend_from_slice(b"\nendstream\nendobj\n");
64    }
65    let xref_offset = out.len();
66    out.extend_from_slice(b"xref\n");
67    let total = stub.objects.len() + 1;
68    out.extend_from_slice(format!("0 {}\n", total).as_bytes());
69    out.extend_from_slice(b"0000000000 65535 f \n");
70    for &off in &offsets {
71        out.extend_from_slice(format!("{:010} 00000 n \n", off).as_bytes());
72    }
73    let trailer = format!(
74        "trailer\n<< /Size {} /Root 1 0 R /Info << /Title ({}) /Author ({}) >> >>\nstartxref\n{}\n%%EOF\n",
75        total, stub.title, stub.author, xref_offset
76    );
77    out.extend_from_slice(trailer.as_bytes());
78    out
79}
80
81/// Object count.
82#[allow(dead_code)]
83pub fn pdf_object_count(stub: &PdfStub) -> usize {
84    stub.objects.len()
85}
86
87/// Estimated file size in bytes (of the serialized output).
88#[allow(dead_code)]
89pub fn pdf_estimated_size(stub: &PdfStub) -> usize {
90    export_pdf_stub(stub).len()
91}
92
93/// Add a simple text drawing command stream.
94#[allow(dead_code)]
95pub fn add_text_stream(stub: &mut PdfStub, text: &str, x: f32, y: f32) -> u32 {
96    let stream = format!("BT /F1 12 Tf {:.2} {:.2} Td ({}) Tj ET", x, y, text);
97    add_content_stream(stub, &stream)
98}
99
100/// Check that the PDF output starts with the PDF magic bytes.
101#[allow(dead_code)]
102pub fn is_valid_pdf_header(bytes: &[u8]) -> bool {
103    bytes.starts_with(b"%PDF-")
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    #[test]
111    fn pdf_starts_with_magic() {
112        let stub = new_pdf_stub("Test", "Author");
113        let bytes = export_pdf_stub(&stub);
114        assert!(is_valid_pdf_header(&bytes));
115    }
116
117    #[test]
118    fn pdf_ends_with_eof() {
119        let stub = new_pdf_stub("Test", "Author");
120        let bytes = export_pdf_stub(&stub);
121        let s = String::from_utf8_lossy(&bytes);
122        assert!(s.contains("%%EOF"));
123    }
124
125    #[test]
126    fn add_content_increases_count() {
127        let mut stub = new_pdf_stub("Test", "Author");
128        add_content_stream(&mut stub, "q Q");
129        assert_eq!(pdf_object_count(&stub), 1);
130    }
131
132    #[test]
133    fn object_numbers_sequential() {
134        let mut stub = new_pdf_stub("Test", "Author");
135        let n1 = add_content_stream(&mut stub, "q Q");
136        let n2 = add_content_stream(&mut stub, "q Q");
137        assert_eq!(n2, n1 + 1);
138    }
139
140    #[test]
141    fn pdf_contains_xref() {
142        let stub = new_pdf_stub("Test", "Author");
143        let bytes = export_pdf_stub(&stub);
144        let s = String::from_utf8_lossy(&bytes);
145        assert!(s.contains("xref"));
146    }
147
148    #[test]
149    fn pdf_contains_title() {
150        let stub = new_pdf_stub("MyTitle", "Author");
151        let bytes = export_pdf_stub(&stub);
152        let s = String::from_utf8_lossy(&bytes);
153        assert!(s.contains("MyTitle"));
154    }
155
156    #[test]
157    fn pdf_estimated_size_positive() {
158        let stub = new_pdf_stub("Test", "Author");
159        assert!(pdf_estimated_size(&stub) > 0);
160    }
161
162    #[test]
163    fn add_text_stream_creates_object() {
164        let mut stub = new_pdf_stub("Test", "Author");
165        add_text_stream(&mut stub, "Hello", 100.0, 700.0);
166        assert_eq!(pdf_object_count(&stub), 1);
167    }
168
169    #[test]
170    fn stream_content_in_output() {
171        let mut stub = new_pdf_stub("Test", "Author");
172        add_content_stream(&mut stub, "q Q");
173        let bytes = export_pdf_stub(&stub);
174        let s = String::from_utf8_lossy(&bytes);
175        assert!(s.contains("q Q"));
176    }
177
178    #[test]
179    fn empty_stub_valid_header() {
180        let stub = new_pdf_stub("Empty", "Nobody");
181        let bytes = export_pdf_stub(&stub);
182        assert!(is_valid_pdf_header(&bytes));
183    }
184}