From 8d327a51969bb698593d2531a9108e44483aa52c Mon Sep 17 00:00:00 2001 From: Julien Neuhart Date: Wed, 12 Aug 2026 21:34:10 +0200 Subject: [PATCH] fix(libreoffice): render scrolled workbooks in full for SinglePageSheets --- pkg/modules/libreoffice/api/libreoffice.go | 8 + .../libreoffice/api/singlepagesheets.go | 230 ++++++++++++++++++ .../libreoffice/api/singlepagesheets_test.go | 216 ++++++++++++++++ .../features/libreoffice_convert.feature | 14 ++ .../testdata/singlepagesheets-scrolled.xlsx | Bin 0 -> 24514 bytes 5 files changed, 468 insertions(+) create mode 100644 pkg/modules/libreoffice/api/singlepagesheets.go create mode 100644 pkg/modules/libreoffice/api/singlepagesheets_test.go create mode 100644 test/integration/testdata/singlepagesheets-scrolled.xlsx diff --git a/pkg/modules/libreoffice/api/libreoffice.go b/pkg/modules/libreoffice/api/libreoffice.go index d722b90f..b9462b29 100644 --- a/pkg/modules/libreoffice/api/libreoffice.go +++ b/pkg/modules/libreoffice/api/libreoffice.go @@ -293,6 +293,14 @@ func (p *libreOfficeProcess) pdf(ctx context.Context, logger *slog.Logger, input return errors.New("LibreOffice not started, cannot handle PDF conversion") } + // SinglePageSheets starts each sheet's single page at the workbook's saved + // scroll position, truncating everything above and to the left of it. + // Render a copy with that position reset to the top-left cell instead. + // See https://github.com/gotenberg/gotenberg/issues/1222. + if options.SinglePageSheets { + inputPath = resetCalcScrollPosition(ctx, logger, inputPath) + } + args := []string{ "--no-launch", "--format", diff --git a/pkg/modules/libreoffice/api/singlepagesheets.go b/pkg/modules/libreoffice/api/singlepagesheets.go new file mode 100644 index 00000000..ce2f5549 --- /dev/null +++ b/pkg/modules/libreoffice/api/singlepagesheets.go @@ -0,0 +1,230 @@ +package api + +import ( + "archive/zip" + "bytes" + "context" + "fmt" + "io" + "log/slog" + "os" + "path/filepath" + "regexp" + "strings" +) + +// topLeftCellAttr matches the topLeftCell attribute that an OOXML worksheet +// uses (on and, for frozen panes, ) to store the cell that +// was at the top-left of the window when the workbook was saved. +var topLeftCellAttr = regexp.MustCompile(` topLeftCell="[^"]*"`) + +// maxDecompressedWorksheet bounds how much a single worksheet may decompress to +// while rewriting it. It guards against a decompression bomb and keeps memory +// predictable. A worksheet larger than this is left untouched, so a pathological +// workbook falls back to the original file rather than being rewritten. +const maxDecompressedWorksheet = 128 << 20 // 128 MiB + +// resetCalcScrollPosition returns a path to a copy of inputPath whose worksheet +// scroll positions have been reset to the top-left cell, or inputPath unchanged +// when the reset does not apply or cannot be performed safely. +// +// LibreOffice's SinglePageSheets export starts each single page at the sheet's +// saved topLeftCell, dropping every row and column above and to the left of it. +// A workbook saved scrolled away from A1 therefore renders truncated. Removing +// the attribute before the conversion makes the whole used range render. +// See https://github.com/gotenberg/gotenberg/issues/1222. +// +// The function never fails the conversion. On a non-xlsx input, a workbook that +// carries no scroll position, or any read, rewrite or validation error, it +// returns the original path so a malformed rewrite can never reach LibreOffice. +func resetCalcScrollPosition(ctx context.Context, logger *slog.Logger, inputPath string) string { + // Resolve the extension to a literal so the sanitized filename is never + // derived from the (user-controlled) upload name. + var ext string + switch strings.ToLower(filepath.Ext(inputPath)) { + case ".xlsx": + ext = ".xlsx" + case ".xlsm": + ext = ".xlsm" + default: + return inputPath + } + + src, err := os.ReadFile(inputPath) + if err != nil { + logger.WarnContext(ctx, fmt.Sprintf("reset calc scroll position: read input: %s; using the original file", err)) + return inputPath + } + + out, changed, err := stripWorksheetScrollPosition(src) + if err != nil { + logger.WarnContext(ctx, fmt.Sprintf("reset calc scroll position: %s; using the original file", err)) + return inputPath + } + if !changed { + // The common case: nothing was saved scrolled, so nothing to do. + return inputPath + } + + // A rewrite that dropped, renamed or corrupted an entry must never reach + // LibreOffice; fall back to the original workbook if it does not round-trip. + if err = validateWorkbook(src, out); err != nil { + logger.WarnContext(ctx, fmt.Sprintf("reset calc scroll position: %s; using the original file", err)) + return inputPath + } + + // Write the sanitized copy alongside the input, inside the request working + // directory that LibreOffice already reads from. The pattern is constant, + // so the resulting name carries no user-controlled path component. + dst, err := os.CreateTemp(filepath.Dir(inputPath), "singlepagesheets-*"+ext) + if err != nil { + logger.WarnContext(ctx, fmt.Sprintf("reset calc scroll position: create sanitized file: %s; using the original file", err)) + return inputPath + } + defer dst.Close() + + _, err = dst.Write(out) + if err != nil { + _ = os.Remove(dst.Name()) + logger.WarnContext(ctx, fmt.Sprintf("reset calc scroll position: write sanitized file: %s; using the original file", err)) + return inputPath + } + + logger.DebugContext(ctx, "reset calc scroll position: cleared worksheet topLeftCell for SinglePageSheets export") + return dst.Name() +} + +// stripWorksheetScrollPosition rewrites the worksheet XML entries of an xlsx +// workbook, removing the topLeftCell attribute, and reports whether anything +// changed. Every non-worksheet entry, and every worksheet that does not carry +// the attribute, is copied byte-for-byte without recompression. +func stripWorksheetScrollPosition(src []byte) ([]byte, bool, error) { + reader, err := zip.NewReader(bytes.NewReader(src), int64(len(src))) + if err != nil { + return nil, false, fmt.Errorf("open workbook: %w", err) + } + + var buf bytes.Buffer + writer := zip.NewWriter(&buf) + changed := false + + for _, file := range reader.File { + rewritten, ok, err := rewriteWorksheet(file) + if err != nil { + return nil, false, err + } + + if ok { + // Recompress only the worksheets that actually changed. + header := file.FileHeader + header.Method = zip.Deflate + w, err := writer.CreateHeader(&header) + if err != nil { + return nil, false, fmt.Errorf("write worksheet %q: %w", file.Name, err) + } + _, err = w.Write(rewritten) + if err != nil { + return nil, false, fmt.Errorf("write worksheet %q: %w", file.Name, err) + } + changed = true + continue + } + + err = copyZipEntry(writer, file) + if err != nil { + return nil, false, err + } + } + + err = writer.Close() + if err != nil { + return nil, false, fmt.Errorf("finalize workbook: %w", err) + } + if !changed { + return nil, false, nil + } + return buf.Bytes(), true, nil +} + +// rewriteWorksheet returns file's contents with topLeftCell removed, and +// whether file is a worksheet that carried the attribute. A worksheet without +// the attribute, or any other entry, returns ok false so the caller copies it +// verbatim. +func rewriteWorksheet(file *zip.File) ([]byte, bool, error) { + if !strings.HasPrefix(file.Name, "xl/worksheets/") || !strings.HasSuffix(strings.ToLower(file.Name), ".xml") { + return nil, false, nil + } + + rc, err := file.Open() + if err != nil { + return nil, false, fmt.Errorf("open worksheet %q: %w", file.Name, err) + } + defer rc.Close() + + // Read at most maxDecompressedWorksheet+1 bytes so a decompression bomb + // cannot exhaust memory; a genuine overflow aborts the rewrite. + data, err := io.ReadAll(io.LimitReader(rc, maxDecompressedWorksheet+1)) + if err != nil { + return nil, false, fmt.Errorf("read worksheet %q: %w", file.Name, err) + } + if len(data) > maxDecompressedWorksheet { + return nil, false, fmt.Errorf("worksheet %q exceeds %d bytes", file.Name, maxDecompressedWorksheet) + } + + if !bytes.Contains(data, []byte("topLeftCell")) { + return nil, false, nil + } + return topLeftCellAttr.ReplaceAll(data, nil), true, nil +} + +// copyZipEntry writes file into writer without decompressing and recompressing +// it, preserving its exact bytes. +func copyZipEntry(writer *zip.Writer, file *zip.File) error { + w, err := writer.CreateRaw(&file.FileHeader) + if err != nil { + return fmt.Errorf("copy entry %q: %w", file.Name, err) + } + rc, err := file.OpenRaw() + if err != nil { + return fmt.Errorf("open entry %q: %w", file.Name, err) + } + _, err = io.Copy(w, rc) + if err != nil { + return fmt.Errorf("copy entry %q: %w", file.Name, err) + } + return nil +} + +// validateWorkbook checks that out reopens as a zip holding exactly the same +// entry names as src, rejecting a rewrite that lost, renamed or added an entry +// or produced a broken central directory. The entry payloads themselves are +// not re-read: unchanged entries are copied byte-for-byte from a workbook that +// already parsed, and rewritten worksheets are produced by the standard library +// writer, so re-decompressing everything would only add a decompression-bomb +// surface without catching a failure this transform can introduce. +func validateWorkbook(src, out []byte) error { + original, err := zip.NewReader(bytes.NewReader(src), int64(len(src))) + if err != nil { + return fmt.Errorf("reopen original workbook: %w", err) + } + rewritten, err := zip.NewReader(bytes.NewReader(out), int64(len(out))) + if err != nil { + return fmt.Errorf("reopen rewritten workbook: %w", err) + } + + if len(rewritten.File) != len(original.File) { + return fmt.Errorf("entry count changed from %d to %d", len(original.File), len(rewritten.File)) + } + + names := make(map[string]struct{}, len(original.File)) + for _, file := range original.File { + names[file.Name] = struct{}{} + } + for _, file := range rewritten.File { + _, ok := names[file.Name] + if !ok { + return fmt.Errorf("unexpected entry %q", file.Name) + } + } + return nil +} diff --git a/pkg/modules/libreoffice/api/singlepagesheets_test.go b/pkg/modules/libreoffice/api/singlepagesheets_test.go new file mode 100644 index 00000000..0b8fd217 --- /dev/null +++ b/pkg/modules/libreoffice/api/singlepagesheets_test.go @@ -0,0 +1,216 @@ +package api + +import ( + "archive/zip" + "bytes" + "context" + "io" + "log/slog" + "os" + "path/filepath" + "strings" + "testing" +) + +// buildWorkbook packs entries into an in-memory xlsx-like zip. +func buildWorkbook(t *testing.T, entries map[string]string) []byte { + t.Helper() + var buf bytes.Buffer + w := zip.NewWriter(&buf) + for name, content := range entries { + f, err := w.Create(name) + if err != nil { + t.Fatalf("create entry %q: %v", name, err) + } + _, err = f.Write([]byte(content)) + if err != nil { + t.Fatalf("write entry %q: %v", name, err) + } + } + err := w.Close() + if err != nil { + t.Fatalf("close workbook: %v", err) + } + return buf.Bytes() +} + +func readEntry(t *testing.T, workbook []byte, name string) string { + t.Helper() + r, err := zip.NewReader(bytes.NewReader(workbook), int64(len(workbook))) + if err != nil { + t.Fatalf("open workbook: %v", err) + } + for _, f := range r.File { + if f.Name != name { + continue + } + rc, err := f.Open() + if err != nil { + t.Fatalf("open entry %q: %v", name, err) + } + defer rc.Close() + data, err := io.ReadAll(rc) + if err != nil { + t.Fatalf("read entry %q: %v", name, err) + } + return string(data) + } + t.Fatalf("entry %q not found", name) + return "" +} + +const scrolledSheet = `` + + `` + + `` + + `1` + +const topSheet = `` + + `` + + `1` + +func TestStripWorksheetScrollPosition(t *testing.T) { + for _, tc := range []struct { + scenario string + workbook []byte + expectErr bool + expectChange bool + }{ + { + scenario: "removes topLeftCell from sheetView and pane", + workbook: buildWorkbook(t, map[string]string{ + "[Content_Types].xml": "", + "xl/worksheets/sheet1.xml": scrolledSheet, + "xl/sharedStrings.xml": "", + }), + expectChange: true, + }, + { + scenario: "leaves a workbook without a saved scroll position untouched", + workbook: buildWorkbook(t, map[string]string{ + "[Content_Types].xml": "", + "xl/worksheets/sheet1.xml": topSheet, + }), + expectChange: false, + }, + { + scenario: "only rewrites worksheet entries", + workbook: buildWorkbook(t, map[string]string{ + "xl/worksheets/sheet1.xml": scrolledSheet, + // A stray topLeftCell elsewhere must not be touched. + "xl/workbook.xml": ``, + }), + expectChange: true, + }, + { + scenario: "rejects a non-zip input", + workbook: []byte("not a zip file"), + expectErr: true, + }, + } { + t.Run(tc.scenario, func(t *testing.T) { + out, changed, err := stripWorksheetScrollPosition(tc.workbook) + + if tc.expectErr { + if err == nil { + t.Fatalf("expected error, got nil") + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if changed != tc.expectChange { + t.Fatalf("expected changed=%v, got %v", tc.expectChange, changed) + } + if !changed { + return + } + + // The rewrite must round-trip and hold the same entries. + if err = validateWorkbook(tc.workbook, out); err != nil { + t.Fatalf("rewritten workbook did not validate: %v", err) + } + if strings.Contains(readEntry(t, out, "xl/worksheets/sheet1.xml"), "topLeftCell") { + t.Fatalf("worksheet still contains topLeftCell") + } + // Non-worksheet entries are copied verbatim. + if _, ok := entryNames(t, out)["xl/workbook.xml"]; ok { + if got := readEntry(t, out, "xl/workbook.xml"); got != `` { + t.Fatalf("non-worksheet entry was modified: %q", got) + } + } + }) + } +} + +func entryNames(t *testing.T, workbook []byte) map[string]struct{} { + t.Helper() + r, err := zip.NewReader(bytes.NewReader(workbook), int64(len(workbook))) + if err != nil { + t.Fatalf("open workbook: %v", err) + } + names := make(map[string]struct{}, len(r.File)) + for _, f := range r.File { + names[f.Name] = struct{}{} + } + return names +} + +func TestResetCalcScrollPosition(t *testing.T) { + logger := slog.New(slog.NewTextHandler(io.Discard, nil)) + ctx := context.Background() + + writeFile := func(t *testing.T, name string, content []byte) string { + t.Helper() + path := filepath.Join(t.TempDir(), name) + err := os.WriteFile(path, content, 0o600) + if err != nil { + t.Fatalf("write %q: %v", name, err) + } + return path + } + + t.Run("non-xlsx input is returned unchanged", func(t *testing.T) { + path := writeFile(t, "input.docx", []byte("whatever")) + if got := resetCalcScrollPosition(ctx, logger, path); got != path { + t.Fatalf("expected %q, got %q", path, got) + } + }) + + t.Run("workbook without a scroll position is returned unchanged", func(t *testing.T) { + path := writeFile(t, "input.xlsx", buildWorkbook(t, map[string]string{ + "xl/worksheets/sheet1.xml": topSheet, + })) + if got := resetCalcScrollPosition(ctx, logger, path); got != path { + t.Fatalf("expected original path %q, got %q", path, got) + } + }) + + t.Run("corrupt xlsx falls back to the original path", func(t *testing.T) { + path := writeFile(t, "input.xlsx", []byte("PK\x03\x04 not really a zip")) + if got := resetCalcScrollPosition(ctx, logger, path); got != path { + t.Fatalf("expected fallback to %q, got %q", path, got) + } + }) + + t.Run("scrolled workbook yields a sanitized copy", func(t *testing.T) { + path := writeFile(t, "input.xlsx", buildWorkbook(t, map[string]string{ + "[Content_Types].xml": "", + "xl/worksheets/sheet1.xml": scrolledSheet, + })) + got := resetCalcScrollPosition(ctx, logger, path) + if got == path { + t.Fatalf("expected a sanitized copy, got the original path") + } + if filepath.Dir(got) != filepath.Dir(path) { + t.Fatalf("sanitized copy escaped the working directory: %q", got) + } + sanitized, err := os.ReadFile(got) + if err != nil { + t.Fatalf("read sanitized file: %v", err) + } + if strings.Contains(readEntry(t, sanitized, "xl/worksheets/sheet1.xml"), "topLeftCell") { + t.Fatalf("sanitized worksheet still contains topLeftCell") + } + }) +} diff --git a/test/integration/features/libreoffice_convert.feature b/test/integration/features/libreoffice_convert.feature index 4edce46b..7ae189d7 100644 --- a/test/integration/features/libreoffice_convert.feature +++ b/test/integration/features/libreoffice_convert.feature @@ -964,3 +964,17 @@ Feature: /forms/libreoffice/convert Then the response status code should be 200 Then there should be 1 PDF(s) in the response Then the "foo.pdf" PDF should have 0 image(s) + + # The workbook was saved scrolled down to row 37. Without the topLeftCell + # reset, SinglePageSheets would start the page there and drop the header rows, + # so the "Meteor" column header only appears when the whole sheet is rendered. + # See https://github.com/gotenberg/gotenberg/issues/1222. + Scenario: POST /forms/libreoffice/convert (SinglePageSheets renders a scrolled workbook in full) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/libreoffice/convert" endpoint with the following form data and header(s): + | files | testdata/singlepagesheets-scrolled.xlsx | file | + | singlePageSheets | true | field | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then there should be 1 PDF(s) in the response + Then the "foo.pdf" PDF should have content matching "Meteor" at page 1 diff --git a/test/integration/testdata/singlepagesheets-scrolled.xlsx b/test/integration/testdata/singlepagesheets-scrolled.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..c16eb3cc79113dd4b7015d752b93728d0a5ecad9 GIT binary patch literal 24514 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