Skip to main content

header_banner/
header_banner.rs

1//! Custom header banner with DrawingML group shapes.
2//!
3//! This example demonstrates how to build a professional header banner
4//! using raw DrawingML XML with set_raw_header_with_images(). The banner
5//! consists of a colored rectangle background with a logo image overlaid.
6//!
7//! Run with: cargo run --example header_banner
8
9use std::fmt::Write;
10use std::path::Path;
11
12use rdocx::{Document, Length};
13
14fn main() {
15    let samples_dir = Path::new(env!("CARGO_MANIFEST_DIR"))
16        .parent()
17        .unwrap()
18        .parent()
19        .unwrap()
20        .join("samples");
21    std::fs::create_dir_all(&samples_dir).unwrap();
22
23    let out = samples_dir.join("header_banner.docx");
24    generate_header_banner_doc(&out);
25    println!("  Created: header_banner.docx");
26    println!("\nDone!");
27}
28
29fn generate_header_banner_doc(path: &Path) {
30    let mut doc = Document::new();
31
32    // Page setup with extra top margin for the banner
33    doc.set_page_size(Length::inches(8.5), Length::inches(11.0));
34    doc.set_margins(
35        Length::twips(2292), // top — extra tall for header banner
36        Length::twips(1440), // right
37        Length::twips(1440), // bottom
38        Length::twips(1440), // left
39    );
40    doc.set_header_footer_distance(Length::twips(720), Length::twips(432));
41
42    // Generate a simple logo image (white text on transparent background)
43    let logo_img = create_logo_png(220, 48);
44
45    // ── Dark blue banner ──
46    let banner = build_header_banner_xml(
47        "rId1",
48        &BannerOpts {
49            bg_color: "1A3C6E",
50            banner_width: 7772400, // full page width in EMU (~8.5")
51            banner_height: 969026, // banner height in EMU (~1.06")
52            logo_width: 2011680,   // logo display width (~2.2")
53            logo_height: 438912,   // logo display height (~0.48")
54            logo_x_offset: 295125, // left padding
55            logo_y_offset: 265057, // vertical centering
56        },
57    );
58
59    doc.set_raw_header_with_images(
60        banner.clone(),
61        &[("rId1", &logo_img, "logo.png")],
62        rdocx_oxml::header_footer::HdrFtrType::Default,
63    );
64
65    // Use a different first page header (same banner, different color)
66    doc.set_different_first_page(true);
67    let first_page_banner = build_header_banner_xml(
68        "rId1",
69        &BannerOpts {
70            bg_color: "2E75B6", // lighter blue for cover
71            banner_width: 7772400,
72            banner_height: 969026,
73            logo_width: 2011680,
74            logo_height: 438912,
75            logo_x_offset: 295125,
76            logo_y_offset: 265057,
77        },
78    );
79    doc.set_raw_header_with_images(
80        first_page_banner,
81        &[("rId1", &logo_img, "logo.png")],
82        rdocx_oxml::header_footer::HdrFtrType::First,
83    );
84
85    // Footer
86    doc.set_footer("Confidential — Internal Use Only");
87
88    // ── Page 1: Cover ──
89    doc.add_paragraph("Company Report").style("Heading1");
90
91    doc.add_paragraph(
92        "This document demonstrates a custom header banner built with DrawingML \
93         group shapes. The banner uses a colored rectangle with a logo image overlaid, \
94         positioned at the top of each page.",
95    );
96
97    doc.add_paragraph("");
98
99    doc.add_paragraph("How the Header Banner Works")
100        .style("Heading2");
101
102    doc.add_paragraph(
103        "The header banner is built using set_raw_header_with_images(), which \
104         accepts raw XML and a list of (rel_id, image_data, filename) tuples. \
105         The XML uses a DrawingML group shape (wpg:wgp) containing:",
106    );
107
108    doc.add_bullet_list_item(
109        "A wps:wsp rectangle shape with a solid color fill (the background bar)",
110        0,
111    );
112    doc.add_bullet_list_item(
113        "A pic:pic image element positioned within the group (the logo)",
114        0,
115    );
116    doc.add_bullet_list_item(
117        "The group is wrapped in a wp:anchor element for absolute page positioning",
118        0,
119    );
120
121    doc.add_paragraph("");
122
123    doc.add_paragraph("Customization").style("Heading2");
124
125    doc.add_paragraph(
126        "All dimensions are in EMU (English Metric Units) where 914400 EMU = 1 inch. \
127         You can customize:",
128    );
129
130    doc.add_bullet_list_item("bg_color — any hex color for the rectangle background", 0);
131    doc.add_bullet_list_item("banner_width / banner_height — size of the full banner", 0);
132    doc.add_bullet_list_item("logo_width / logo_height — display size of the logo", 0);
133    doc.add_bullet_list_item(
134        "logo_x_offset / logo_y_offset — logo position within the banner",
135        0,
136    );
137
138    doc.add_paragraph("");
139
140    doc.add_paragraph("Different First Page").style("Heading2");
141
142    doc.add_paragraph(
143        "This page uses a lighter blue banner (first page header). \
144         Subsequent pages use a darker blue banner (default header). \
145         Use set_different_first_page(true) to enable this.",
146    );
147
148    // ── Page 2 ──
149    doc.add_paragraph("").page_break_before(true);
150
151    doc.add_paragraph("Second Page").style("Heading1");
152
153    doc.add_paragraph(
154        "This page shows the default header banner (dark blue). The first page \
155         had a lighter blue banner because we set a different first-page header.",
156    );
157
158    doc.add_paragraph("");
159
160    doc.add_paragraph(
161        "The banner repeats on every page because it is placed in the header part. \
162         You can have different banners for default, first-page, and even-page headers.",
163    );
164
165    doc.set_title("Header Banner Example");
166    doc.set_author("rdocx");
167
168    doc.save(path).unwrap();
169}
170
171// ─────────────────────────────────────────────────────────────────────────────
172// Header banner builder
173// ─────────────────────────────────────────────────────────────────────────────
174
175struct BannerOpts<'a> {
176    bg_color: &'a str,
177    banner_width: i64,
178    banner_height: i64,
179    logo_width: i64,
180    logo_height: i64,
181    logo_x_offset: i64,
182    logo_y_offset: i64,
183}
184
185/// Build complete `<w:hdr>` XML for a banner header (colored rect + logo image).
186///
187/// `image_rel_id` is the rId that will reference the logo image (e.g. "rId1").
188fn build_header_banner_xml(image_rel_id: &str, opts: &BannerOpts) -> Vec<u8> {
189    let mut xml = String::with_capacity(2048);
190
191    write!(
192        xml,
193        r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>"#
194    )
195    .unwrap();
196    write!(xml, r#"<w:hdr "#).unwrap();
197    write!(
198        xml,
199        r#"xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" "#
200    )
201    .unwrap();
202    write!(
203        xml,
204        r#"xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" "#
205    )
206    .unwrap();
207    write!(
208        xml,
209        r#"xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" "#
210    )
211    .unwrap();
212    write!(
213        xml,
214        r#"xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" "#
215    )
216    .unwrap();
217    write!(
218        xml,
219        r#"xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture" "#
220    )
221    .unwrap();
222    write!(
223        xml,
224        r#"xmlns:wpg="http://schemas.microsoft.com/office/word/2010/wordprocessingGroup" "#
225    )
226    .unwrap();
227    write!(
228        xml,
229        r#"xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape" "#
230    )
231    .unwrap();
232    write!(
233        xml,
234        r#"xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006">"#
235    )
236    .unwrap();
237
238    write!(xml, r#"<w:p><w:pPr><w:pStyle w:val="Header"/></w:pPr>"#).unwrap();
239    write!(xml, r#"<w:r><w:rPr><w:noProof/></w:rPr>"#).unwrap();
240    write!(xml, r#"<mc:AlternateContent><mc:Choice Requires="wpg">"#).unwrap();
241    write!(xml, r#"<w:drawing>"#).unwrap();
242
243    // Anchor at top-left of page
244    write!(
245        xml,
246        r#"<wp:anchor distT="0" distB="0" distL="0" distR="0" "#
247    )
248    .unwrap();
249    write!(xml, r#"simplePos="0" relativeHeight="251658240" "#).unwrap();
250    write!(xml, r#"behindDoc="0" locked="0" layoutInCell="1" "#).unwrap();
251    write!(xml, r#"hidden="0" allowOverlap="1">"#).unwrap();
252    write!(xml, r#"<wp:simplePos x="0" y="0"/>"#).unwrap();
253    write!(
254        xml,
255        r#"<wp:positionH relativeFrom="page"><wp:posOffset>0</wp:posOffset></wp:positionH>"#
256    )
257    .unwrap();
258    write!(
259        xml,
260        r#"<wp:positionV relativeFrom="page"><wp:posOffset>0</wp:posOffset></wp:positionV>"#
261    )
262    .unwrap();
263    write!(
264        xml,
265        r#"<wp:extent cx="{}" cy="{}"/>"#,
266        opts.banner_width, opts.banner_height
267    )
268    .unwrap();
269    write!(xml, r#"<wp:effectExtent l="0" t="0" r="0" b="0"/>"#).unwrap();
270    write!(xml, r#"<wp:wrapNone/>"#).unwrap();
271    write!(xml, r#"<wp:docPr id="1" name="Header Banner"/>"#).unwrap();
272    write!(xml, r#"<wp:cNvGraphicFramePr/>"#).unwrap();
273
274    // Group shape containing rect + image
275    write!(xml, r#"<a:graphic>"#).unwrap();
276    write!(
277        xml,
278        r#"<a:graphicData uri="http://schemas.microsoft.com/office/word/2010/wordprocessingGroup">"#
279    )
280    .unwrap();
281    write!(xml, r#"<wpg:wgp><wpg:cNvGrpSpPr/><wpg:grpSpPr><a:xfrm>"#).unwrap();
282    write!(
283        xml,
284        r#"<a:off x="0" y="0"/><a:ext cx="{w}" cy="{h}"/>"#,
285        w = opts.banner_width,
286        h = opts.banner_height
287    )
288    .unwrap();
289    write!(
290        xml,
291        r#"<a:chOff x="0" y="0"/><a:chExt cx="{w}" cy="{h}"/>"#,
292        w = opts.banner_width,
293        h = opts.banner_height
294    )
295    .unwrap();
296    write!(xml, r#"</a:xfrm></wpg:grpSpPr>"#).unwrap();
297
298    // Background rectangle
299    write!(
300        xml,
301        r#"<wps:wsp><wps:cNvPr id="2" name="Background"/><wps:cNvSpPr/><wps:spPr>"#
302    )
303    .unwrap();
304    write!(
305        xml,
306        r#"<a:xfrm><a:off x="0" y="0"/><a:ext cx="{w}" cy="{h}"/></a:xfrm>"#,
307        w = opts.banner_width,
308        h = opts.banner_height
309    )
310    .unwrap();
311    write!(xml, r#"<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>"#).unwrap();
312    write!(
313        xml,
314        r#"<a:solidFill><a:srgbClr val="{}"/></a:solidFill>"#,
315        opts.bg_color
316    )
317    .unwrap();
318    write!(xml, r#"<a:ln><a:noFill/></a:ln>"#).unwrap();
319    write!(xml, r#"</wps:spPr><wps:bodyPr/></wps:wsp>"#).unwrap();
320
321    // Logo image
322    write!(
323        xml,
324        r#"<pic:pic><pic:nvPicPr><pic:cNvPr id="3" name="Logo"/><pic:cNvPicPr/></pic:nvPicPr>"#
325    )
326    .unwrap();
327    write!(xml, r#"<pic:blipFill><a:blip r:embed="{}"/><a:stretch><a:fillRect/></a:stretch></pic:blipFill>"#, image_rel_id).unwrap();
328    write!(
329        xml,
330        r#"<pic:spPr><a:xfrm><a:off x="{}" y="{}"/><a:ext cx="{}" cy="{}"/></a:xfrm>"#,
331        opts.logo_x_offset, opts.logo_y_offset, opts.logo_width, opts.logo_height
332    )
333    .unwrap();
334    write!(xml, r#"<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>"#).unwrap();
335    write!(
336        xml,
337        r#"<a:noFill/><a:ln><a:noFill/></a:ln></pic:spPr></pic:pic>"#
338    )
339    .unwrap();
340
341    // Close all
342    write!(xml, r#"</wpg:wgp></a:graphicData></a:graphic>"#).unwrap();
343    write!(xml, r#"</wp:anchor></w:drawing>"#).unwrap();
344    write!(xml, r#"</mc:Choice></mc:AlternateContent>"#).unwrap();
345    write!(xml, r#"</w:r></w:p></w:hdr>"#).unwrap();
346
347    xml.into_bytes()
348}
349
350// ─────────────────────────────────────────────────────────────────────────────
351// PNG generation helpers
352// ─────────────────────────────────────────────────────────────────────────────
353
354/// Create a simple logo PNG (white text area on dark background).
355fn create_logo_png(width: u32, height: u32) -> Vec<u8> {
356    let mut pixels = Vec::with_capacity((width * height * 4) as usize);
357    for y in 0..height {
358        for x in 0..width {
359            // White rectangle in the center area (simulates logo text)
360            let in_text_area =
361                x > width / 8 && x < width * 7 / 8 && y > height / 4 && y < height * 3 / 4;
362            if in_text_area {
363                pixels.extend_from_slice(&[255, 255, 255, 255]);
364            } else {
365                pixels.extend_from_slice(&[255, 255, 255, 40]); // mostly transparent
366            }
367        }
368    }
369    encode_png(width, height, &pixels)
370}
371
372fn encode_png(width: u32, height: u32, pixels: &[u8]) -> Vec<u8> {
373    let mut png = Vec::new();
374    {
375        use std::io::Write as _;
376        png.write_all(&[137, 80, 78, 71, 13, 10, 26, 10]).unwrap();
377
378        let mut ihdr = Vec::new();
379        ihdr.extend_from_slice(&width.to_be_bytes());
380        ihdr.extend_from_slice(&height.to_be_bytes());
381        ihdr.extend_from_slice(&[8, 6, 0, 0, 0]); // 8-bit RGBA
382        write_chunk(&mut png, b"IHDR", &ihdr);
383
384        let mut raw = Vec::new();
385        for y in 0..height {
386            raw.push(0);
387            let s = (y * width * 4) as usize;
388            raw.extend_from_slice(&pixels[s..s + (width * 4) as usize]);
389        }
390        write_chunk(&mut png, b"IDAT", &zlib_store(&raw));
391        write_chunk(&mut png, b"IEND", &[]);
392    }
393    png
394}
395
396fn write_chunk(out: &mut Vec<u8>, ct: &[u8; 4], data: &[u8]) {
397    use std::io::Write as _;
398    out.write_all(&(data.len() as u32).to_be_bytes()).unwrap();
399    out.write_all(ct).unwrap();
400    out.write_all(data).unwrap();
401    out.write_all(&crc32(ct, data).to_be_bytes()).unwrap();
402}
403
404fn crc32(ct: &[u8], data: &[u8]) -> u32 {
405    static T: std::sync::LazyLock<[u32; 256]> = std::sync::LazyLock::new(|| {
406        let mut t = [0u32; 256];
407        for n in 0..256u32 {
408            let mut c = n;
409            for _ in 0..8 {
410                c = if c & 1 != 0 {
411                    0xEDB88320 ^ (c >> 1)
412                } else {
413                    c >> 1
414                };
415            }
416            t[n as usize] = c;
417        }
418        t
419    });
420    let mut c = 0xFFFFFFFF_u32;
421    for &b in ct.iter().chain(data) {
422        c = T[((c ^ b as u32) & 0xFF) as usize] ^ (c >> 8);
423    }
424    c ^ 0xFFFFFFFF
425}
426
427fn zlib_store(data: &[u8]) -> Vec<u8> {
428    let mut out = vec![0x78, 0x01];
429    for (i, chunk) in data.chunks(65535).enumerate() {
430        let last = i == data.chunks(65535).count() - 1;
431        out.push(if last { 0x01 } else { 0x00 });
432        let len = chunk.len() as u16;
433        out.extend_from_slice(&len.to_le_bytes());
434        out.extend_from_slice(&(!len).to_le_bytes());
435        out.extend_from_slice(chunk);
436    }
437    let (mut a, mut b) = (1u32, 0u32);
438    for &byte in data {
439        a = (a + byte as u32) % 65521;
440        b = (b + a) % 65521;
441    }
442    out.extend_from_slice(&((b << 16) | a).to_be_bytes());
443    out
444}