1use 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 doc.set_page_size(Length::inches(8.5), Length::inches(11.0));
41 doc.set_margins(
42 Length::inches(1.0), Length::inches(1.0), Length::inches(1.0), Length::inches(1.0), );
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 doc.set_header("rdocx Feature Showcase");
57 doc.set_footer("Generated by rdocx — Page");
58
59 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 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(""); doc.add_paragraph(""); doc.add_paragraph(""); {
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(""); {
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 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 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 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 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 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 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 doc.add_paragraph("").page_break_before(true);
291
292 doc.add_paragraph("3. Tables").style("Heading1");
293
294 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 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 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 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 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 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 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 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 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 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 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 {
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 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 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 doc.add_paragraph("").page_break_before(true);
474
475 doc.add_paragraph("5. Content Manipulation")
476 .style("Heading1");
477
478 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 {
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 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 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 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 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 {
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 doc.add_paragraph("")
607 .section_break(SectionBreak::NextPage)
608 .section_landscape();
609
610 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
664fn 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_data
686 .write_all(&[137, 80, 78, 71, 13, 10, 26, 10])
687 .unwrap();
688
689 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); ihdr.push(6); ihdr.push(0); ihdr.push(0); ihdr.push(0); write_png_chunk(&mut png_data, b"IHDR", &ihdr);
699
700 let mut raw_data = Vec::new();
702 for y in 0..height {
703 raw_data.push(0); 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 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}