[GH-ISSUE #523] drawing_id counts sheets that produce no drawing, breaking later sheets' drawing relationships #522

Open
opened 2026-10-02 23:11:48 -06:00 by gitea-mirror · 1 comment
Owner

Originally created by @billdenney on GitHub (Jul 28, 2026).
Original GitHub issue: https://github.com/jmcnamara/libxlsxwriter/issues/523

Originally assigned to: @jmcnamara on GitHub.

A worksheet whose only image is embedded, or is a header/footer image,
consumes a drawing_id without producing a drawing<N>.xml. Every floating
image on a later sheet then gets a relationship target one too high,
pointing at a part that does not exist. Excel opens the file, reports
"Removed Part: Drawing shape", and drops the image.

Cause

_prepare_drawings() (workbook.c:1106) increments the id for any sheet with
any of five things:

if (STAILQ_EMPTY(worksheet->image_props)
    && STAILQ_EMPTY(worksheet->embedded_image_props)
    && STAILQ_EMPTY(worksheet->chart_data)
    && !worksheet->has_header_vml && !worksheet->has_background_image) {
    continue;
}

drawing_id++;

but the packager numbers the drawing files counting only sheets that actually
have one — _get_drawing_count() (packager.c:619):

drawing = worksheet->drawing;
if (drawing)
    drawing_count++;

Embedded images and header/footer VML produce no worksheet->drawing, so the
two counters diverge from that sheet onward. has_background_image looks
like the same case, though I have not tested it.

Reproducing

Order is what matters — a drawing-less-but-counted sheet poisons every floating
image after it:

Workbook Result
float, float, float ok
plain sheet, then float ok
float, then embedded ok
embedded, then float sheet2.xml.rels -> drawing2.xml, which is absent
float, embedded, float sheet3.xml.rels -> drawing3.xml, absent
header image, then float same
float, then header image ok

Minimal C:

#include "xlsxwriter.h"

int main(void) {
    lxw_workbook  *wb = workbook_new("desync.xlsx");
    lxw_worksheet *s1 = workbook_add_worksheet(wb, NULL);
    lxw_worksheet *s2 = workbook_add_worksheet(wb, NULL);

    /* Sheet 1: an embedded image -- counted, but writes no drawing part. */
    worksheet_embed_image(s1, 0, 0, "logo.png");

    /* Sheet 2: an ordinary floating image. */
    worksheet_insert_image(s2, 0, 0, "logo.png");

    return workbook_close(wb);
}

xl/worksheets/_rels/sheet2.xml.rels targets ../drawings/drawing2.xml, but
only xl/drawings/drawing1.xml exists.

Substituting worksheet_set_header_opt() with an image_left for the embedded
image on sheet 1 reproduces it identically.

Comment VML is not affected: _prepare_vml_objects() increments
vml_drawing_id for comment VML and header VML alike, and both produce a file,
so that counter stays in step.

Suggested fix

Increment drawing_id only where a drawing part will be written — i.e. move
the increment to where worksheet->drawing is created — or track a separate
counter for the relationship target.

Found while adding image support to writexl (R). Version 1.2.4.

Originally created by @billdenney on GitHub (Jul 28, 2026). Original GitHub issue: https://github.com/jmcnamara/libxlsxwriter/issues/523 Originally assigned to: @jmcnamara on GitHub. A worksheet whose only image is *embedded*, or is a header/footer image, consumes a `drawing_id` without producing a `drawing<N>.xml`. Every floating image on a **later** sheet then gets a relationship target one too high, pointing at a part that does not exist. Excel opens the file, reports "Removed Part: Drawing shape", and drops the image. ## Cause `_prepare_drawings()` (`workbook.c:1106`) increments the id for any sheet with *any* of five things: ```c if (STAILQ_EMPTY(worksheet->image_props) && STAILQ_EMPTY(worksheet->embedded_image_props) && STAILQ_EMPTY(worksheet->chart_data) && !worksheet->has_header_vml && !worksheet->has_background_image) { continue; } drawing_id++; ``` but the packager numbers the drawing *files* counting only sheets that actually have one — `_get_drawing_count()` (`packager.c:619`): ```c drawing = worksheet->drawing; if (drawing) drawing_count++; ``` Embedded images and header/footer VML produce no `worksheet->drawing`, so the two counters diverge from that sheet onward. `has_background_image` looks like the same case, though I have not tested it. ## Reproducing Order is what matters — a drawing-less-but-counted sheet poisons every floating image after it: | Workbook | Result | |---|---| | float, float, float | ok | | plain sheet, then float | ok | | float, then embedded | ok | | **embedded, then float** | `sheet2.xml.rels` -> `drawing2.xml`, which is absent | | **float, embedded, float** | `sheet3.xml.rels` -> `drawing3.xml`, absent | | **header image, then float** | same | | float, then header image | ok | Minimal C: ```c #include "xlsxwriter.h" int main(void) { lxw_workbook *wb = workbook_new("desync.xlsx"); lxw_worksheet *s1 = workbook_add_worksheet(wb, NULL); lxw_worksheet *s2 = workbook_add_worksheet(wb, NULL); /* Sheet 1: an embedded image -- counted, but writes no drawing part. */ worksheet_embed_image(s1, 0, 0, "logo.png"); /* Sheet 2: an ordinary floating image. */ worksheet_insert_image(s2, 0, 0, "logo.png"); return workbook_close(wb); } ``` `xl/worksheets/_rels/sheet2.xml.rels` targets `../drawings/drawing2.xml`, but only `xl/drawings/drawing1.xml` exists. Substituting `worksheet_set_header_opt()` with an `image_left` for the embedded image on sheet 1 reproduces it identically. Comment VML is *not* affected: `_prepare_vml_objects()` increments `vml_drawing_id` for comment VML and header VML alike, and both produce a file, so that counter stays in step. ## Suggested fix Increment `drawing_id` only where a drawing part will be written — i.e. move the increment to where `worksheet->drawing` is created — or track a separate counter for the relationship target. Found while adding image support to writexl (R). Version 1.2.4.
Author
Owner

@jmcnamara commented on GitHub (Jul 29, 2026):

Thanks. I'll look into it.

<!-- gh-comment-id:5113807048 --> @jmcnamara commented on GitHub (Jul 29, 2026): Thanks. I'll look into it.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
github-starred/libxlsxwriter#522
No description provided.