Your invoice logo disappears from the page header after Delphi code copies the sheet or re-saves the workbook, yet the header text still contains &G. Excel draws a header picture only when the &G code in a header section matches an image part, legacyDrawingHF VML in XLSX or HFPicture records in XLS, and the HotXLS header picture API maintains both halves
Header pictures do not live where ordinary worksheet images live. A logo dropped onto the grid is a DrawingML shape anchored to cells, the machinery described in how HotXLS handles charts, images and Excel drawings. A header picture belongs to the printed page instead, and Excel still stores it as VML. The worksheet XML points at that VML part through one <legacyDrawingHF r:id="…"/> element, while the text that asks for the picture sits in the headerFooter element (ECMA-376 Part 1, §18.3.1.46). Code that copies the sheet XML but treats VML as baggage drops the relationship and leaves a &G pointing at nothing. Header pictures appear only in Page Layout view, print preview and on paper, so the loss usually goes unnoticed until someone prints
Adding a header logo to an XLSX worksheet in Delphi
On the XLSX side the whole job goes through TXLSXWorksheet.HeaderFooterImages, a collection addressed by a TXLSXHeaderFooterPageKind (odd, even or first page) and a TXLSXHeaderFooterPicturePosition (left, center or right, in the header or the footer). Add and AddFromFile do the bookkeeping that hand-written XML tends to get wrong: they insert &G into the matching section of the matching header string, and for an even-page or first-page slot they also switch on DifferentOddEven or DifferentFirst. AddFromFile accepts PNG, JPEG, GIF, BMP, EMF and WMF files and uses the file name as the picture title. Every new picture starts at 72 × 72 points, so set WidthPt and HeightPt to the logo's real proportions, or a wide banner gets squeezed into a square
uses
SysUtils, lxHandleX;
procedure BuildQuoteWithHeaderLogos(const AFileName: WideString);
var
Book: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
Logo: TXLSXHeaderFooterImage;
begin
Book := TXLSXWorkbook.Create;
try
// A new workbook has no sheets, and Sheets is 1-based: add one first
Sheet := Book.Sheets.Add('Quote');
Sheet.Cells[1, 1].Value := 'Quote 2026-118';
// Text first: the section setters rebuild the whole header string
Sheet.CenterHeader := 'ACME Industrial';
Sheet.RightHeader := 'Page &P of &N';
Logo := Sheet.HeaderFooterImages.AddFromFile(xlsxHFOddPage,
xlsxHFLeftHeader, 'C:\Brand\logo-small.png');
Logo.WidthPt := 96;
Logo.HeightPt := 32;
// Page one: a larger logo and its own complete header text
Sheet.DifferentFirst := True;
Sheet.FirstHeaderText := '&CACME Industrial&RQuote 2026-118';
Logo := Sheet.HeaderFooterImages.AddFromFile(xlsxHFFirstPage,
xlsxHFLeftHeader, 'C:\Brand\logo-cover.png');
Logo.WidthPt := 150;
Logo.HeightPt := 50;
// Leave room for the taller first-page logo
Sheet.MarginTopCM := 3.0;
Sheet.MarginHeaderCM := 0.8;
if Book.SaveAs(AFileName) <> 1 then
raise Exception.Create('Saving the quote workbook failed');
finally
Book.Free;
end;
end;
Expected: the worksheet part gains a headerFooter element with differentFirst="1", an oddHeader that ends in &L&G (the left section was empty, so AddFromFile appended it), and a firstHeader that holds its own text plus a second &G. Further down the same part, legacyDrawingHF names the VML drawing, and the VML carries one shape per picture. The shape id is the only thing that ties a picture to a slot, and its v:imagedata points through the VML part's own relationships to the image file under xl/media/:
<headerFooter differentFirst="1">
<oddHeader>&CACME Industrial&RPage &P of &N&L&G</oddHeader>
<firstHeader>&CACME Industrial&RQuote 2026-118&L&G</firstHeader>
</headerFooter>
...
<legacyDrawingHF r:id="rId1"/>
<!-- the VML part, abridged -->
<v:shape id="LH" o:spid="_x0000_s1025" type="#_x0000_t75"
style='position:absolute;margin-left:0pt;margin-top:0pt;width:96pt;height:32pt;z-index:1'>
<v:imagedata o:relid="rId1" o:title="logo-small"/>
</v:shape>
<v:shape id="LHFIRST" o:spid="_x0000_s1026" type="#_x0000_t75"
style='position:absolute;margin-left:0pt;margin-top:0pt;width:150pt;height:50pt;z-index:2'>
<v:imagedata o:relid="rId2" o:title="logo-cover"/>
</v:shape>
Why does Excel ignore a header picture that is in the file?
Excel joins the two halves by name and by flag, and a mismatch on either one drops the image without any error message. The VML shape id encodes the slot: two letters for the position, plus an EVEN or FIRST suffix for the page variant. A shape is drawn only when the header or footer string Excel actually uses for that page contains &G in the section the id names, and Excel decides which string it uses from the two switches on headerFooter. With differentFirst off, page one prints oddHeader, and a perfectly valid LHFIRST shape together with its firstHeader string is simply skipped. With differentOddEven off, the same happens to every EVEN shape. HotXLS maps the ids to its enum values as follows when it reads and writes the VML part
Slot (TXLSXHeaderFooterPicturePosition) | Odd pages | Even pages | First page | Section holding &G |
|---|---|---|---|---|
xlsxHFLeftHeader | LH | LHEVEN | LHFIRST | &L of the header string |
xlsxHFCenterHeader | CH | CHEVEN | CHFIRST | &C of the header string |
xlsxHFRightHeader | RH | RHEVEN | RHFIRST | &R of the header string |
xlsxHFLeftFooter | LF | LFEVEN | LFFIRST | &L of the footer string |
xlsxHFCenterFooter | CF | CFEVEN | CFFIRST | &C of the footer string |
xlsxHFRightFooter | RF | RFEVEN | RFFIRST | &R of the footer string |
The most common way to lose &G in your own code is ordering. HeaderText, FirstHeaderText and the other whole-string setters replace the string, and section properties such as LeftHeader rebuild it from its three parts, so assigning the section that holds the picture after adding the picture wipes its &G. The image is still written to the VML part. Excel never draws it. Writing CenterHeader after a left-header logo is safe, because the left section is carried over. Files from other tools fail the other way, with &G and no matching shape. Add on an occupied slot returns the same TXLSXHeaderFooterImage and re-inserts the code, but for an even-page or first-page slot it also sets DifferentOddEven or DifferentFirst to True, so a repair pass before SaveAs should touch those slots only while the matching switch is already on:
uses
Classes, SysUtils, lxHandleX;
// Restores &G for stored pictures without turning page variants on,
// then lists odd-page sections that ask for a picture nobody stored
procedure RepairHeaderPictureCodes(Sheet: TXLSXWorksheet; Problems: TStrings);
const
SlotNames: array[TXLSXHeaderFooterPicturePosition] of string = (
'left header', 'center header', 'right header',
'left footer', 'center footer', 'right footer');
var
I: Integer;
Img: TXLSXHeaderFooterImage;
Slot: TXLSXHeaderFooterPicturePosition;
Section: WideString;
begin
for I := 0 to Sheet.HeaderFooterImages.Count - 1 do
begin
Img := Sheet.HeaderFooterImages[I];
// Add returns the existing image and re-inserts &G, but for even and
// first-page slots it also sets DifferentOddEven / DifferentFirst to
// True: only call it when that switch is already on
if (Img.PageKind = xlsxHFOddPage) or
((Img.PageKind = xlsxHFEvenPage) and Sheet.DifferentOddEven) or
((Img.PageKind = xlsxHFFirstPage) and Sheet.DifferentFirst) then
Sheet.HeaderFooterImages.Add(Img.PageKind, Img.Position)
else
Problems.Add('Picture in the ' + SlotNames[Img.Position] +
' of a page variant that is switched off');
end;
for Slot := Low(TXLSXHeaderFooterPicturePosition) to
High(TXLSXHeaderFooterPicturePosition) do
begin
case Slot of
xlsxHFLeftHeader: Section := Sheet.LeftHeader;
xlsxHFCenterHeader: Section := Sheet.CenterHeader;
xlsxHFRightHeader: Section := Sheet.RightHeader;
xlsxHFLeftFooter: Section := Sheet.LeftFooter;
xlsxHFCenterFooter: Section := Sheet.CenterFooter;
else
Section := Sheet.RightFooter;
end;
// Simplified test: an escaped "&&G" would also match
if (Pos('&G', WideUpperCase(Section)) > 0) and
(Sheet.HeaderFooterImages.Find(xlsxHFOddPage, Slot) = nil) then
Problems.Add('Odd-page ' + SlotNames[Slot] + ' has &G but no picture');
end;
end;
Copying a worksheet together with its header pictures
A sheet copy has to carry the header and footer strings, the four headerFooter switches, the pictures and the relationship chain from the sheet through the VML part to the media files; generic copy code tends to move only the strings. TXLSXSheets.AddCopy(ASrcSheet, ANewName) carries all of it, giving the copy its own VML part while the image bytes stay in one shared media file. TXLSWorksheets.AddCopy does the same for XLS by cloning the preserved HeaderFooter and HFPicture records with the sheet. What else AddCopy brings across, formula rewriting included, is covered in duplicating an XLSX worksheet in Delphi
uses
SysUtils, lxHandle, lxHandleX;
procedure CopyInvoiceTemplates;
var
XBook: TXLSXWorkbook;
XCopy: TXLSXWorksheet;
Book: IXLSWorkbook;
begin
XBook := TXLSXWorkbook.Create;
try
if (XBook.Open('C:\Templates\invoice.xlsx') <> 1) or
(XBook.Sheets.Count = 0) then
raise Exception.Create('Cannot open the XLSX template');
// Sheets[1] is the first sheet; Sheets[0] returns nil
XCopy := XBook.Sheets.AddCopy(XBook.Sheets[1], 'Invoice 2026-11');
// Header text, the four switches and the pictures came along.
// CenterFooter rebuilds the footer but keeps its left and right parts
XCopy.CenterFooter := 'Customer copy';
if XBook.SaveAs('C:\Out\invoices.xlsx') <> 1 then
raise Exception.Create('Cannot save invoices.xlsx');
finally
XBook.Free;
end;
// Legacy .xls: the interface reference releases the workbook
Book := TXLSWorkbook.Create;
if (Book.Open('C:\Templates\invoice.xls') <> 1) or
(Book.Sheets.Count = 0) then
raise Exception.Create('Cannot open the XLS template');
Book.Sheets.AddCopy(Book.Sheets[1], 'Invoice 2026-11');
if Book.SaveAs('C:\Out\invoices.xls') <> 1 then
raise Exception.Create('Cannot save invoices.xls');
end;
Where an XLS file keeps the same header picture
BIFF8 spreads a header picture across two substreams, and simple XLS writers lose logos at that seam. The picture bytes live in the workbook globals, inside an HFPicture record (MS-XLS §2.4.138, record type 0x0866) holding an OfficeArt drawing group with the image store. Each worksheet carries its own HFPicture records with the shapes that sit in the header and footer slots and refer back into that store. Keep the worksheet side, rebuild the globals from scratch, and Excel opens the file with empty headers. Text is split too: the Header and Footer records hold only odd-page strings, while even and first-page text and all four switches exist only in the HeaderFooter record (MS-XLS §2.4.137, 0x089C). HotXLS keeps both kinds of records on open and writes them back on save, so a load-and-save pass leaves the logo, the first-page text and the switches in place
| What Excel needs | XLSX (OOXML) | XLS (BIFF8) |
|---|---|---|
| Image bytes | Media part, reached through the VML part's relationships | Globals HFPicture: OfficeArt drawing group with the image store |
| Slot, size and offset | VML shape id and its style attribute | Worksheet HFPicture: OfficeArt drawing with the header shapes |
| Link from the sheet | <legacyDrawingHF r:id> plus the sheet relationship | Shapes refer into the globals image store |
| Picture code | &G inside oddHeader, evenHeader, firstHeader and the footers | &G inside the Header, Footer and HeaderFooter strings |
| Odd-page text | oddHeader, oddFooter | Header and Footer records |
| Even and first-page text | evenHeader, evenFooter, firstHeader, firstFooter | HeaderFooter record only |
differentOddEven, differentFirst, scaleWithDoc, alignWithMargins | Attributes of headerFooter | Flag bits of the HeaderFooter record |
How is an oversized HFPicture record continued?
An oversized HFPicture does not use the ordinary CONTINUE record. A BIFF8 record body tops out at 8,224 bytes, and a logo embedded as an OfficeArt BLIP routinely runs past that. Most records spill into CONTINUE (0x003C) records that carry raw bytes and nothing else. A logical HFPicture is instead split into further 0x0866 records, and every fragment starts with the same 14-byte envelope:
- 12 bytes of FrtHeader: the record type repeated, a
grbitFrtword and 8 reserved bytes - 1 flags byte:
fIsDrawingfor a worksheet drawing orfIsDrawingGroupfor the globals drawing group, plusfContinue, set on every fragment after the first - 1 reserved byte
Read those fragments as CONTINUE records and the 14 bytes land in the middle of the OfficeArt stream, shifting every container header after them; at that offset they can resemble a shape id and flags, yet they belong to the record envelope. The opposite mistake does as much damage. A reader that strips 14 bytes from every continuation, including plain CONTINUE records from writers that never framed their overflow, deletes 14 bytes of image per record, about 3,570 bytes across the roughly 255 records of a 2 MiB logo, and the BLIP no longer decodes. HotXLS keeps continuation bytes as they are and removes the repeated envelope only when the header matches the owning record exactly and the OfficeArt container length (0xF000 for the drawing group, 0xF002 for a drawing) confirms the framed form. On save it writes the framed form MS-XLS describes, with fContinue set from the second piece on
First-page and even-page logos need their own header text
When page one needs a different logo, switch DifferentFirst on explicitly and give FirstHeaderText the complete text for that page. Excel does not blend the two headers. With differentFirst set, page one prints firstHeader and nothing else, so a first-page logo over an empty FirstHeaderText prints alone, without the company name and page number. Add(xlsxHFFirstPage, …) flips the switch for you and in doing so enables a header that holds only the picture; setting the property yourself, next to the text, keeps the intent visible to the next maintainer. Duplex layouts follow the same rule with DifferentOddEven and EvenHeaderText. A three-page sample in Excel's Page Layout view shows all three variants: page one carries the first-page header, page two the even one, page three the odd one
ScaleWithDoc, True by default, scales header and footer with the sheet's print scaling, so a report set to fit on one page shrinks the logo along with the cells. Turn it off when a brand guideline fixes the logo size; HotXLS then writes scaleWithDoc="0", and on the XLS side the same bit travels in the preserved HeaderFooter record. If the logo still comes out smaller than its WidthPt and HeightPt suggest, the fit-to-page and margin settings covered in sheet protection, page setup and printing with HotXLS are the next place to look
For reports that ship as both XLSX and legacy XLS, one template sheet copied with AddCopy keeps the logo, the first-page text and the header switches together in either format, and the HotXLS Delphi spreadsheet component handles both from the same Delphi code