1use 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 doc.set_page_size(Length::inches(8.5), Length::inches(11.0));
34 doc.set_margins(
35 Length::twips(2292), Length::twips(1440), Length::twips(1440), Length::twips(1440), );
40 doc.set_header_footer_distance(Length::twips(720), Length::twips(432));
41
42 let logo_img = create_logo_png(220, 48);
44
45 let banner = build_header_banner_xml(
47 "rId1",
48 &BannerOpts {
49 bg_color: "1A3C6E",
50 banner_width: 7772400, banner_height: 969026, logo_width: 2011680, logo_height: 438912, logo_x_offset: 295125, logo_y_offset: 265057, },
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 doc.set_different_first_page(true);
67 let first_page_banner = build_header_banner_xml(
68 "rId1",
69 &BannerOpts {
70 bg_color: "2E75B6", 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 doc.set_footer("Confidential — Internal Use Only");
87
88 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 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
171struct 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
185fn 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 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 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 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 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 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
350fn 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 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]); }
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]); 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}