Technical Article

Excel Header Logos That Disappear: legacyDrawingHF and HFPicture

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>&amp;CACME Industrial&amp;RPage &amp;P of &amp;N&amp;L&amp;G</oddHeader>
  <firstHeader>&amp;CACME Industrial&amp;RQuote 2026-118&amp;L&amp;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 pagesEven pagesFirst pageSection holding &G
xlsxHFLeftHeaderLHLHEVENLHFIRST&L of the header string
xlsxHFCenterHeaderCHCHEVENCHFIRST&C of the header string
xlsxHFRightHeaderRHRHEVENRHFIRST&R of the header string
xlsxHFLeftFooterLFLFEVENLFFIRST&L of the footer string
xlsxHFCenterFooterCFCFEVENCFFIRST&C of the footer string
xlsxHFRightFooterRFRFEVENRFFIRST&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 needsXLSX (OOXML)XLS (BIFF8)
Image bytesMedia part, reached through the VML part's relationshipsGlobals HFPicture: OfficeArt drawing group with the image store
Slot, size and offsetVML shape id and its style attributeWorksheet HFPicture: OfficeArt drawing with the header shapes
Link from the sheet<legacyDrawingHF r:id> plus the sheet relationshipShapes 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 textoddHeader, oddFooterHeader and Footer records
Even and first-page textevenHeader, evenFooter, firstHeader, firstFooterHeaderFooter record only
differentOddEven, differentFirst, scaleWithDoc, alignWithMarginsAttributes of headerFooterFlag 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 grbitFrt word and 8 reserved bytes
  • 1 flags byte: fIsDrawing for a worksheet drawing or fIsDrawingGroup for the globals drawing group, plus fContinue, 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