Skip to main content

generate_samples/
generate_samples.rs

1//! Generates a single comprehensive sample document exercising every rdocx feature.
2//!
3//! Run with: cargo run --example generate_samples
4
5use std::collections::HashMap;
6use std::path::Path;
7
8use rdocx::{
9    Alignment, BorderStyle, Document, Length, SectionBreak, TabAlignment, TabLeader,
10    VerticalAlignment,
11};
12
13fn main() {
14    let samples_dir = Path::new(env!("CARGO_MANIFEST_DIR"))
15        .parent()
16        .unwrap()
17        .parent()
18        .unwrap()
19        .join("samples");
20    std::fs::create_dir_all(&samples_dir).unwrap();
21
22    println!(
23        "Generating comprehensive sample document in {}",
24        samples_dir.display()
25    );
26
27    let out = samples_dir.join("feature_showcase.docx");
28    generate_feature_showcase(&out);
29    println!("  feature_showcase.docx — all rdocx features in one document");
30
31    println!("\nDone!");
32}
33
34fn generate_feature_showcase(path: &Path) {
35    let mut doc = Document::new();
36
37    // =========================================================================
38    // PAGE SETUP & METADATA
39    // =========================================================================
40    doc.set_page_size(Length::inches(8.5), Length::inches(11.0));
41    doc.set_margins(
42        Length::inches(1.0), // top
43        Length::inches(1.0), // right
44        Length::inches(1.0), // bottom
45        Length::inches(1.0), // left
46    );
47    doc.set_header_footer_distance(Length::twips(720), Length::twips(432));
48    doc.set_gutter(Length::twips(0));
49
50    doc.set_title("rdocx Feature Showcase");
51    doc.set_author("rdocx Sample Generator");
52    doc.set_subject("Comprehensive feature demonstration");
53    doc.set_keywords("rdocx, docx, rust, sample");
54
55    // Header & Footer
56    doc.set_header("rdocx Feature Showcase");
57    doc.set_footer("Generated by rdocx — Page");
58
59    // Different first page header
60    doc.set_different_first_page(true);
61    doc.set_first_page_header("rdocx");
62    doc.set_first_page_footer("Feature Showcase — Cover Page");
63
64    // =========================================================================
65    // PAGE 1: COVER PAGE — background image, run formatting
66    // =========================================================================
67    let bg_cover = create_sample_png(612, 792, [30, 60, 120]);
68    doc.add_background_image(&bg_cover, "cover_bg.png");
69
70    doc.add_paragraph(""); // spacer
71    doc.add_paragraph(""); // spacer
72    doc.add_paragraph(""); // spacer
73
74    {
75        let mut p = doc.add_paragraph("").alignment(Alignment::Center);
76        p.add_run("rdocx").bold(true).size(72.0).color("FFFFFF");
77    }
78    {
79        let mut p = doc.add_paragraph("").alignment(Alignment::Center);
80        p.add_run("Feature Showcase")
81            .size(28.0)
82            .color("FFFFFF")
83            .italic(true);
84    }
85
86    doc.add_paragraph(""); // spacer
87
88    {
89        let mut p = doc.add_paragraph("").alignment(Alignment::Center);
90        p.add_run("A comprehensive demonstration of every feature")
91            .size(14.0)
92            .color("CCDDFF");
93    }
94    {
95        let mut p = doc.add_paragraph("").alignment(Alignment::Center);
96        p.add_run("provided by the rdocx Rust crate for DOCX generation.")
97            .size(14.0)
98            .color("CCDDFF");
99    }
100
101    // =========================================================================
102    // PAGE 2: TEXT FORMATTING
103    // =========================================================================
104    doc.add_paragraph("").page_break_before(true);
105
106    doc.add_paragraph("1. Text Formatting").style("Heading1");
107
108    doc.add_paragraph("This section demonstrates paragraph and run-level formatting options.");
109    doc.add_paragraph("");
110
111    // --- Paragraph alignment ---
112    doc.add_paragraph("Paragraph Alignment").style("Heading2");
113
114    doc.add_paragraph("This paragraph is left-aligned (the default).")
115        .alignment(Alignment::Left);
116    doc.add_paragraph("This paragraph is center-aligned.")
117        .alignment(Alignment::Center);
118    doc.add_paragraph("This paragraph is right-aligned.")
119        .alignment(Alignment::Right);
120    doc.add_paragraph(
121        "This paragraph is justified. To demonstrate justified text properly, it needs \
122         to be long enough to span multiple lines so the word spacing adjustment is visible \
123         across the full width of the text area on the page.",
124    )
125    .alignment(Alignment::Justify);
126
127    doc.add_paragraph("");
128
129    // --- Run formatting ---
130    doc.add_paragraph("Run Formatting").style("Heading2");
131
132    {
133        let mut p = doc.add_paragraph("");
134        p.add_run("Bold text").bold(true);
135        p.add_run(" | ");
136        p.add_run("Italic text").italic(true);
137        p.add_run(" | ");
138        p.add_run("Bold + Italic").bold(true).italic(true);
139    }
140    {
141        let mut p = doc.add_paragraph("");
142        p.add_run("Single underline").underline(true);
143        p.add_run(" | ");
144        p.add_run("Strikethrough").strike(true);
145        p.add_run(" | ");
146        p.add_run("Double strikethrough").double_strike(true);
147    }
148    {
149        let mut p = doc.add_paragraph("");
150        p.add_run("Red text").color("FF0000");
151        p.add_run(" | ");
152        p.add_run("Blue text").color("0000FF");
153        p.add_run(" | ");
154        p.add_run("Green text").color("00AA00");
155        p.add_run(" | ");
156        p.add_run("Highlighted").highlight("FFFF00");
157    }
158    {
159        let mut p = doc.add_paragraph("");
160        p.add_run("8pt small").size(8.0);
161        p.add_run(" | ");
162        p.add_run("11pt normal").size(11.0);
163        p.add_run(" | ");
164        p.add_run("16pt large").size(16.0);
165        p.add_run(" | ");
166        p.add_run("24pt extra-large").size(24.0);
167    }
168    {
169        let mut p = doc.add_paragraph("");
170        p.add_run("Arial font").font("Arial");
171        p.add_run(" | ");
172        p.add_run("Times New Roman font").font("Times New Roman");
173        p.add_run(" | ");
174        p.add_run("Courier New font").font("Courier New");
175    }
176    {
177        let mut p = doc.add_paragraph("");
178        p.add_run("Normal");
179        p.add_run(" H").size(11.0);
180        p.add_run("2").subscript();
181        p.add_run("O (subscript)").size(11.0);
182        p.add_run(" | E = mc").size(11.0);
183        p.add_run("2").superscript();
184        p.add_run(" (superscript)").size(11.0);
185    }
186    {
187        let mut p = doc.add_paragraph("");
188        p.add_run("ALL CAPS").all_caps(true);
189        p.add_run(" | ");
190        p.add_run("Small Caps").small_caps(true);
191        p.add_run(" | ");
192        p.add_run("Expanded spacing")
193            .character_spacing(Length::twips(40));
194    }
195
196    doc.add_paragraph("");
197
198    // --- Paragraph formatting ---
199    doc.add_paragraph("Paragraph Formatting").style("Heading2");
200
201    doc.add_paragraph("Paragraph with shading (light green background)")
202        .shading("E2EFDA");
203
204    doc.add_paragraph("Paragraph with bottom border")
205        .border_bottom(BorderStyle::Single, 6, "2E75B6");
206
207    doc.add_paragraph("Paragraph with all borders")
208        .border_all(BorderStyle::Single, 4, "FF0000");
209
210    doc.add_paragraph("Paragraph with 1-inch left indent and hanging indent")
211        .indent_left(Length::inches(1.0))
212        .hanging_indent(Length::inches(0.5));
213
214    doc.add_paragraph("Paragraph with first-line indent of 0.5 inches")
215        .first_line_indent(Length::inches(0.5));
216
217    doc.add_paragraph("Paragraph with extra space before (24pt) and after (12pt)")
218        .space_before(Length::pt(24.0))
219        .space_after(Length::pt(12.0));
220
221    doc.add_paragraph(
222        "Paragraph with double line spacing. This text should have extra vertical \
223         space between lines to demonstrate the line_spacing_multiple setting.",
224    )
225    .line_spacing_multiple(2.0);
226
227    doc.add_paragraph("Paragraph with keep-with-next (won't break from the next paragraph)")
228        .keep_with_next(true);
229    doc.add_paragraph("(This stays with the paragraph above.)");
230
231    // =========================================================================
232    // PAGE 3: LISTS & TAB STOPS
233    // =========================================================================
234    doc.add_paragraph("").page_break_before(true);
235
236    doc.add_paragraph("2. Lists").style("Heading1");
237
238    doc.add_paragraph("Bullet List").style("Heading2");
239
240    doc.add_bullet_list_item("First bullet item", 0);
241    doc.add_bullet_list_item("Second bullet item", 0);
242    doc.add_bullet_list_item("Nested level 1", 1);
243    doc.add_bullet_list_item("Nested level 2", 2);
244    doc.add_bullet_list_item("Back to level 1", 1);
245    doc.add_bullet_list_item("Third bullet item", 0);
246
247    doc.add_paragraph("");
248
249    doc.add_paragraph("Numbered List").style("Heading2");
250
251    doc.add_numbered_list_item("First numbered item", 0);
252    doc.add_numbered_list_item("Second numbered item", 0);
253    doc.add_numbered_list_item("Sub-item A", 1);
254    doc.add_numbered_list_item("Sub-item B", 1);
255    doc.add_numbered_list_item("Third numbered item", 0);
256
257    doc.add_paragraph("");
258
259    // --- Tab stops ---
260    doc.add_paragraph("Tab Stops").style("Heading2");
261
262    doc.add_paragraph("Left\tCenter\tRight\tDecimal")
263        .add_tab_stop(TabAlignment::Left, Length::inches(0.0))
264        .add_tab_stop(TabAlignment::Center, Length::inches(2.5))
265        .add_tab_stop(TabAlignment::Right, Length::inches(5.0))
266        .add_tab_stop(TabAlignment::Decimal, Length::inches(6.5));
267
268    doc.add_paragraph("Item\t........\tPrice")
269        .add_tab_stop_with_leader(TabAlignment::Left, Length::inches(0.0), TabLeader::None)
270        .add_tab_stop_with_leader(TabAlignment::Right, Length::inches(4.0), TabLeader::Dot)
271        .add_tab_stop_with_leader(TabAlignment::Right, Length::inches(5.0), TabLeader::None);
272
273    doc.add_paragraph("Widget A\t........\t$19.99")
274        .add_tab_stop_with_leader(TabAlignment::Left, Length::inches(0.0), TabLeader::None)
275        .add_tab_stop_with_leader(TabAlignment::Right, Length::inches(4.0), TabLeader::Dot)
276        .add_tab_stop_with_leader(TabAlignment::Right, Length::inches(5.0), TabLeader::None);
277
278    doc.add_paragraph("Gadget B\t________\t$249.50")
279        .add_tab_stop_with_leader(TabAlignment::Left, Length::inches(0.0), TabLeader::None)
280        .add_tab_stop_with_leader(
281            TabAlignment::Right,
282            Length::inches(4.0),
283            TabLeader::Underscore,
284        )
285        .add_tab_stop_with_leader(TabAlignment::Right, Length::inches(5.0), TabLeader::None);
286
287    // =========================================================================
288    // PAGE 4: TABLES
289    // =========================================================================
290    doc.add_paragraph("").page_break_before(true);
291
292    doc.add_paragraph("3. Tables").style("Heading1");
293
294    // --- Basic table with borders ---
295    doc.add_paragraph("Basic Table with Borders")
296        .style("Heading2");
297
298    {
299        let mut tbl = doc.add_table(4, 3);
300        tbl = tbl.borders(BorderStyle::Single, 4, "000000");
301
302        // Header row
303        for col in 0..3 {
304            tbl.cell(0, col).unwrap().shading("2E75B6");
305        }
306        tbl.cell(0, 0).unwrap().set_text("Name");
307        tbl.cell(0, 1).unwrap().set_text("Role");
308        tbl.cell(0, 2).unwrap().set_text("Location");
309
310        tbl.cell(1, 0).unwrap().set_text("Alice Johnson");
311        tbl.cell(1, 1).unwrap().set_text("Engineering Lead");
312        tbl.cell(1, 2).unwrap().set_text("New York");
313
314        tbl.cell(2, 0).unwrap().set_text("Bob Smith");
315        tbl.cell(2, 1).unwrap().set_text("Product Manager");
316        tbl.cell(2, 2).unwrap().set_text("San Francisco");
317
318        tbl.cell(3, 0).unwrap().set_text("Carol Davis");
319        tbl.cell(3, 1).unwrap().set_text("Designer");
320        tbl.cell(3, 2).unwrap().set_text("London");
321    }
322
323    doc.add_paragraph("");
324
325    // --- Table with cell merging ---
326    doc.add_paragraph("Table with Cell Merging & Vertical Alignment")
327        .style("Heading2");
328
329    {
330        let mut tbl = doc.add_table(4, 4);
331        tbl = tbl.borders(BorderStyle::Single, 4, "000000");
332        tbl = tbl.width_pct(100.0);
333
334        // Header spanning all columns
335        tbl.cell(0, 0).unwrap().set_text("Quarterly Revenue Report");
336        tbl.cell(0, 0).unwrap().shading("1F4E79");
337        tbl.cell(0, 0).unwrap().grid_span(4);
338
339        // Sub-header
340        tbl.cell(1, 0).unwrap().set_text("Region");
341        tbl.cell(1, 0).unwrap().shading("D6E4F0");
342        tbl.cell(1, 1).unwrap().set_text("Q1");
343        tbl.cell(1, 1).unwrap().shading("D6E4F0");
344        tbl.cell(1, 2).unwrap().set_text("Q2");
345        tbl.cell(1, 2).unwrap().shading("D6E4F0");
346        tbl.cell(1, 3).unwrap().set_text("Total");
347        tbl.cell(1, 3).unwrap().shading("D6E4F0");
348
349        // Data
350        tbl.cell(2, 0).unwrap().set_text("North America");
351        tbl.cell(2, 1).unwrap().set_text("$2.4M");
352        tbl.cell(2, 2).unwrap().set_text("$2.7M");
353        tbl.cell(2, 3).unwrap().set_text("$5.1M");
354
355        tbl.cell(3, 0).unwrap().set_text("Europe");
356        tbl.cell(3, 1).unwrap().set_text("$1.8M");
357        tbl.cell(3, 2).unwrap().set_text("$2.0M");
358        tbl.cell(3, 3).unwrap().set_text("$3.8M");
359
360        // Vertical alignment on data cells
361        tbl.cell(2, 3)
362            .unwrap()
363            .vertical_alignment(VerticalAlignment::Center);
364        tbl.cell(3, 3)
365            .unwrap()
366            .vertical_alignment(VerticalAlignment::Bottom);
367    }
368
369    doc.add_paragraph("");
370
371    // --- Table with vertical merge ---
372    doc.add_paragraph("Table with Vertical Merge")
373        .style("Heading2");
374
375    {
376        let mut tbl = doc.add_table(4, 3);
377        tbl = tbl.borders(BorderStyle::Single, 4, "000000");
378
379        tbl.cell(0, 0).unwrap().set_text("Category");
380        tbl.cell(0, 0).unwrap().shading("E2EFDA");
381        tbl.cell(0, 1).unwrap().set_text("Item");
382        tbl.cell(0, 1).unwrap().shading("E2EFDA");
383        tbl.cell(0, 2).unwrap().set_text("Price");
384        tbl.cell(0, 2).unwrap().shading("E2EFDA");
385
386        // "Hardware" spans rows 1-2
387        tbl.cell(1, 0).unwrap().set_text("Hardware");
388        tbl.cell(1, 0).unwrap().v_merge_restart();
389        tbl.cell(1, 1).unwrap().set_text("Laptop");
390        tbl.cell(1, 2).unwrap().set_text("$1,200");
391
392        tbl.cell(2, 0).unwrap().v_merge_continue();
393        tbl.cell(2, 1).unwrap().set_text("Monitor");
394        tbl.cell(2, 2).unwrap().set_text("$450");
395
396        // "Software" on row 3
397        tbl.cell(3, 0).unwrap().set_text("Software");
398        tbl.cell(3, 1).unwrap().set_text("IDE License");
399        tbl.cell(3, 2).unwrap().set_text("$200/yr");
400    }
401
402    doc.add_paragraph("");
403
404    // --- Nested table ---
405    doc.add_paragraph("Nested Table").style("Heading2");
406
407    {
408        let mut tbl = doc.add_table(2, 2);
409        tbl = tbl.borders(BorderStyle::Single, 6, "2E75B6");
410
411        tbl.cell(0, 0).unwrap().set_text("Outer Cell (0,0)");
412        tbl.cell(0, 1).unwrap().set_text("Outer Cell (0,1)");
413        tbl.cell(1, 0).unwrap().set_text("Outer Cell (1,0)");
414
415        // Nested table inside cell (1,1)
416        {
417            let mut cell = tbl.cell(1, 1).unwrap();
418            cell.set_text("Contains nested table:");
419            let mut nested = cell.add_table(2, 2);
420            nested = nested.borders(BorderStyle::Single, 2, "FF6600");
421            nested.cell(0, 0).unwrap().set_text("Inner A");
422            nested.cell(0, 1).unwrap().set_text("Inner B");
423            nested.cell(1, 0).unwrap().set_text("Inner C");
424            nested.cell(1, 1).unwrap().set_text("Inner D");
425        }
426    }
427
428    // =========================================================================
429    // PAGE 5: IMAGES
430    // =========================================================================
431    doc.add_paragraph("").page_break_before(true);
432
433    doc.add_paragraph("4. Images").style("Heading1");
434
435    doc.add_paragraph("Inline Image").style("Heading2");
436
437    doc.add_paragraph("Below is an inline image (200x50 pixels, blue gradient):");
438    let inline_img = create_sample_png(200, 50, [0, 80, 200]);
439    doc.add_picture(
440        &inline_img,
441        "inline_chart.png",
442        Length::inches(3.0),
443        Length::inches(0.75),
444    );
445
446    doc.add_paragraph("");
447
448    doc.add_paragraph("Header Image").style("Heading2");
449
450    // Replace the text-only header with an image header
451    let header_img = create_sample_png(400, 40, [40, 40, 40]);
452    doc.set_header_image(
453        &header_img,
454        "header_logo.png",
455        Length::inches(2.0),
456        Length::inches(0.2),
457    );
458
459    doc.add_paragraph(
460        "The document header has been replaced with an inline image. \
461         Check the header area at the top of this page.",
462    );
463
464    doc.add_paragraph("");
465    doc.add_paragraph(
466        "Note: The cover page uses a full-page background image behind the text, \
467         demonstrated on page 1 via add_background_image().",
468    );
469
470    // =========================================================================
471    // PAGE 6: CONTENT MANIPULATION — placeholder replacement, insertion
472    // =========================================================================
473    doc.add_paragraph("").page_break_before(true);
474
475    doc.add_paragraph("5. Content Manipulation")
476        .style("Heading1");
477
478    // --- Placeholder replacement ---
479    doc.add_paragraph("Placeholder Replacement")
480        .style("Heading2");
481
482    doc.add_paragraph(
483        "Before replacement, this document contained {{customer}} and {{date}} placeholders.",
484    );
485
486    {
487        let mut p = doc.add_paragraph("");
488        p.add_run("Customer: ").bold(true);
489        p.add_run("{{customer}}");
490    }
491    {
492        let mut p = doc.add_paragraph("");
493        p.add_run("Date: ").bold(true);
494        p.add_run("{{date}}");
495    }
496    doc.add_paragraph("Reference: {{ref_number}}");
497
498    // Table with placeholders
499    {
500        let mut tbl = doc.add_table(3, 2);
501        tbl = tbl.borders(BorderStyle::Single, 4, "000000");
502        tbl.cell(0, 0).unwrap().set_text("Field");
503        tbl.cell(0, 0).unwrap().shading("D6E4F0");
504        tbl.cell(0, 1).unwrap().set_text("Value");
505        tbl.cell(0, 1).unwrap().shading("D6E4F0");
506        tbl.cell(1, 0).unwrap().set_text("Project");
507        tbl.cell(1, 1).unwrap().set_text("{{project}}");
508        tbl.cell(2, 0).unwrap().set_text("Status");
509        tbl.cell(2, 1).unwrap().set_text("{{status}}");
510    }
511
512    // Perform replacements
513    let mut replacements = HashMap::new();
514    replacements.insert("{{customer}}", "Acme Corporation");
515    replacements.insert("{{date}}", "February 22, 2026");
516    replacements.insert("{{ref_number}}", "REF-2026-001");
517    replacements.insert("{{project}}", "Infrastructure Upgrade");
518    replacements.insert("{{status}}", "In Progress");
519    let replace_count = doc.replace_all(&replacements);
520
521    doc.add_paragraph("");
522    doc.add_paragraph(&format!(
523        "(Replaced {} placeholders above — in body text and table cells)",
524        replace_count
525    ));
526
527    doc.add_paragraph("");
528
529    // --- Content insertion ---
530    doc.add_paragraph("Content Insertion").style("Heading2");
531
532    doc.add_paragraph("Section A: First section of content.");
533    doc.add_paragraph("Section C: Third section of content.");
534
535    // Insert "Section B" between A and C
536    if let Some(idx) = doc.find_content_index("Section C") {
537        doc.insert_paragraph(
538            idx,
539            "Section B: Inserted between A and C using find_content_index().",
540        );
541    }
542
543    doc.add_paragraph("");
544    doc.add_paragraph(
545        "The paragraph above ('Section B') was inserted at a specific position \
546         using find_content_index() + insert_paragraph().",
547    );
548
549    // =========================================================================
550    // PAGE 7: LANDSCAPE — section break, wide table
551    // =========================================================================
552    doc.add_paragraph("").section_break(SectionBreak::NextPage);
553
554    doc.add_paragraph("6. Mixed Page Orientation")
555        .style("Heading1");
556
557    doc.add_paragraph(
558        "This page is in LANDSCAPE orientation. It was created using a section break \
559         followed by section_landscape(). This is useful for wide tables or charts.",
560    );
561
562    doc.add_paragraph("");
563
564    // Wide table for landscape
565    {
566        let mut tbl = doc.add_table(4, 7);
567        tbl = tbl.borders(BorderStyle::Single, 4, "2E75B6");
568
569        let headers = ["Region", "Jan", "Feb", "Mar", "Apr", "May", "Total"];
570        for (col, h) in headers.iter().enumerate() {
571            tbl.cell(0, col).unwrap().set_text(h);
572            tbl.cell(0, col).unwrap().shading("2E75B6");
573        }
574
575        let data = [
576            [
577                "North America",
578                "$1.2M",
579                "$1.3M",
580                "$1.4M",
581                "$1.5M",
582                "$1.6M",
583                "$7.0M",
584            ],
585            [
586                "Europe", "$0.8M", "$0.9M", "$0.9M", "$1.0M", "$1.1M", "$4.7M",
587            ],
588            [
589                "Asia Pacific",
590                "$0.5M",
591                "$0.6M",
592                "$0.7M",
593                "$0.7M",
594                "$0.8M",
595                "$3.3M",
596            ],
597        ];
598        for (row_idx, row_data) in data.iter().enumerate() {
599            for (col, val) in row_data.iter().enumerate() {
600                tbl.cell(row_idx + 1, col).unwrap().set_text(val);
601            }
602        }
603    }
604
605    // End landscape, return to portrait
606    doc.add_paragraph("")
607        .section_break(SectionBreak::NextPage)
608        .section_landscape();
609
610    // =========================================================================
611    // PAGE 8: BACK TO PORTRAIT — styles, final notes
612    // =========================================================================
613    doc.add_paragraph("7. Custom Styles & Summary")
614        .style("Heading1");
615
616    doc.add_paragraph(
617        "This final page is back in portrait orientation after a section break. \
618         The document has demonstrated:",
619    );
620
621    doc.add_bullet_list_item(
622        "Page setup: size, margins, header/footer distance, gutter",
623        0,
624    );
625    doc.add_bullet_list_item("Document metadata: title, author, subject, keywords", 0);
626    doc.add_bullet_list_item("Headers and footers: text, images, different first page", 0);
627    doc.add_bullet_list_item("Background images: full-page behind text", 0);
628    doc.add_bullet_list_item(
629        "Text formatting: bold, italic, underline, strike, color, size, font",
630        0,
631    );
632    doc.add_bullet_list_item(
633        "Advanced run formatting: superscript, subscript, caps, spacing",
634        0,
635    );
636    doc.add_bullet_list_item(
637        "Paragraph formatting: alignment, borders, shading, spacing, indentation",
638        0,
639    );
640    doc.add_bullet_list_item("Bullet and numbered lists with nesting levels", 0);
641    doc.add_bullet_list_item("Tab stops with dot/underscore leaders", 0);
642    doc.add_bullet_list_item(
643        "Tables: borders, shading, column spans, row spans, nesting",
644        0,
645    );
646    doc.add_bullet_list_item("Vertical alignment in table cells", 0);
647    doc.add_bullet_list_item("Inline images", 0);
648    doc.add_bullet_list_item("Placeholder replacement in body and table cells", 0);
649    doc.add_bullet_list_item("Content insertion at specific positions", 0);
650    doc.add_bullet_list_item(
651        "Section breaks with mixed portrait/landscape orientation",
652        0,
653    );
654
655    doc.add_paragraph("");
656    doc.add_paragraph("All features above were built entirely from scratch using the rdocx API.")
657        .alignment(Alignment::Center)
658        .shading("E2EFDA")
659        .border_all(BorderStyle::Single, 2, "00AA00");
660
661    doc.save(path).unwrap();
662}
663
664// ─────────────────────────────────────────────────────────────────────────────
665// Helper: create a minimal valid PNG (solid color with slight gradient)
666// ─────────────────────────────────────────────────────────────────────────────
667fn create_sample_png(width: u32, height: u32, base_color: [u8; 3]) -> Vec<u8> {
668    let mut pixels = Vec::with_capacity((width * height * 4) as usize);
669    for y in 0..height {
670        for x in 0..width {
671            let fy = y as f64 / height as f64;
672            let fx = x as f64 / width as f64;
673            let r = (base_color[0] as f64 + (1.0 - fy) * 40.0).min(255.0) as u8;
674            let g = (base_color[1] as f64 + fx * 30.0).min(255.0) as u8;
675            let b = (base_color[2] as f64 + fy * 60.0).min(255.0) as u8;
676            pixels.extend_from_slice(&[r, g, b, 255]);
677        }
678    }
679
680    let mut png_data = Vec::new();
681    {
682        use std::io::Write;
683
684        // PNG Signature
685        png_data
686            .write_all(&[137, 80, 78, 71, 13, 10, 26, 10])
687            .unwrap();
688
689        // IHDR chunk
690        let mut ihdr = Vec::new();
691        ihdr.extend_from_slice(&width.to_be_bytes());
692        ihdr.extend_from_slice(&height.to_be_bytes());
693        ihdr.push(8); // bit depth
694        ihdr.push(6); // color type: RGBA
695        ihdr.push(0); // compression
696        ihdr.push(0); // filter
697        ihdr.push(0); // interlace
698        write_png_chunk(&mut png_data, b"IHDR", &ihdr);
699
700        // IDAT chunk
701        let mut raw_data = Vec::new();
702        for y in 0..height {
703            raw_data.push(0); // filter: None
704            let row_start = (y * width * 4) as usize;
705            let row_end = row_start + (width * 4) as usize;
706            raw_data.extend_from_slice(&pixels[row_start..row_end]);
707        }
708
709        let compressed = zlib_store(&raw_data);
710        write_png_chunk(&mut png_data, b"IDAT", &compressed);
711
712        // IEND chunk
713        write_png_chunk(&mut png_data, b"IEND", &[]);
714    }
715
716    png_data
717}
718
719fn write_png_chunk(out: &mut Vec<u8>, chunk_type: &[u8; 4], data: &[u8]) {
720    use std::io::Write;
721    let len = data.len() as u32;
722    out.write_all(&len.to_be_bytes()).unwrap();
723    out.write_all(chunk_type).unwrap();
724    out.write_all(data).unwrap();
725    let crc = crc32(chunk_type, data);
726    out.write_all(&crc.to_be_bytes()).unwrap();
727}
728
729fn crc32(chunk_type: &[u8], data: &[u8]) -> u32 {
730    static CRC_TABLE: std::sync::LazyLock<[u32; 256]> = std::sync::LazyLock::new(|| {
731        let mut table = [0u32; 256];
732        for n in 0..256u32 {
733            let mut c = n;
734            for _ in 0..8 {
735                if c & 1 != 0 {
736                    c = 0xEDB88320 ^ (c >> 1);
737                } else {
738                    c >>= 1;
739                }
740            }
741            table[n as usize] = c;
742        }
743        table
744    });
745
746    let mut crc = 0xFFFFFFFF_u32;
747    for &byte in chunk_type.iter().chain(data.iter()) {
748        let index = ((crc ^ byte as u32) & 0xFF) as usize;
749        crc = CRC_TABLE[index] ^ (crc >> 8);
750    }
751    crc ^ 0xFFFFFFFF
752}
753
754fn zlib_store(data: &[u8]) -> Vec<u8> {
755    let mut out = Vec::new();
756    out.push(0x78);
757    out.push(0x01);
758
759    let chunks: Vec<&[u8]> = data.chunks(65535).collect();
760    for (i, chunk) in chunks.iter().enumerate() {
761        let is_last = i == chunks.len() - 1;
762        out.push(if is_last { 0x01 } else { 0x00 });
763        let len = chunk.len() as u16;
764        let nlen = !len;
765        out.extend_from_slice(&len.to_le_bytes());
766        out.extend_from_slice(&nlen.to_le_bytes());
767        out.extend_from_slice(chunk);
768    }
769
770    let adler = adler32(data);
771    out.extend_from_slice(&adler.to_be_bytes());
772    out
773}
774
775fn adler32(data: &[u8]) -> u32 {
776    let mut a: u32 = 1;
777    let mut b: u32 = 0;
778    for &byte in data {
779        a = (a + byte as u32) % 65521;
780        b = (b + a) % 65521;
781    }
782    (b << 16) | a
783}