Skip to content

release: v2.4.0 — in-memory image API + merged-cell round-trip fix#33

Merged
mmonterroca merged 10 commits into
masterfrom
release/v2.4.0
May 1, 2026
Merged

release: v2.4.0 — in-memory image API + merged-cell round-trip fix#33
mmonterroca merged 10 commits into
masterfrom
release/v2.4.0

Conversation

@mmonterroca

Copy link
Copy Markdown
Owner

Summary

Release v2.4.0 consolidating two merged feature/fix branches:

What's included

New Features

  • In-memory image API on ParagraphBuilder and domain.Paragraph:
    • AddImageFromBytes(data, format)
    • AddImageFromBytesWithSize(data, format, size)
    • AddImageFromBytesWithPosition(data, format, size, pos)
  • Format normalization (`JPG` → `jpeg`, leading `.` trimmed) + validation against supported set
  • Defensive copy of input bytes for safe buffer reuse
  • CLI handler now embeds base64 images without temp files

Bug Fixes

  • `hydrateTableCell` now parses `<w:tcPr>` and restores `w:gridSpan` (via `cell.Merge(span, 1)`) and `w:vMerge` on document reopen
  • `hydrateTable` tracks `colOffset` and recomputes `maxCols` from gridSpan sums
  • Numeric parse errors wrapped with `errors.WrapWithContext` for clearer diagnostics

Tests

  • `TestGridSpanPreservedAfterRoundTrip` (asserts `IsHorizontallyMergedContinuation()` on spanned cells)
  • `TestVMergePreservedAfterRoundTrip` (asserts `VMergeRestart` / `VMergeContinue` round-trip)
  • Unit + builder tests for all 3 `AddImageFromBytes*` paths

Documentation

  • `CHANGELOG.md` — new v2.4.0 section
  • `RELEASE_NOTES_v2.4.0.md` — full release notes
  • `README.md` — version, features list, refreshed Roadmap (release history + planned)
  • `doc.go`, `docs/V2_DESIGN.md`, `docs/V2_API_GUIDE.md` — version bumped to 2.4.0

Verification

  • ✅ `go build ./...`
  • ✅ `go test ./...` (all packages green)

Post-merge

After merging, tag and create the GitHub Release:

```bash
git checkout master && git pull
git tag -a v2.4.0 -m "v2.4.0 — in-memory image API + merged-cell round-trip fix"
git push origin v2.4.0
```

Closes #25
Closes #29

- parse w:tcPr in hydrateTableCell to restore horizontal/vertical merges
- add TestGridSpanPreservedAfterRoundTrip reproduction test

Closes #25

🐛 - Generated by Copilot
- use cell.Merge(span, 1) instead of SetGridSpan to mark continuation cells
- compute grid column count from gridSpan sums, not tc element count
- track column offset in hydration loop for correct cell-to-grid mapping
- use WrapWithContext for gridSpan Atoi error with attr name and value
- add TestVMergePreservedAfterRoundTrip for vertical merge coverage
- assert IsHorizontallyMergedContinuation on spanned-over cells

🔧 - Generated by Copilot
Add support for adding images from raw byte data without requiring
file system access. This addresses the use case where users have
limited access to a file system (issue #29).

New public API methods on ParagraphBuilder:
- AddImageFromBytes(data, format)
- AddImageFromBytesWithSize(data, format, size)
- AddImageFromBytesWithPosition(data, format, size, pos)

New domain.Paragraph interface methods:
- AddImageFromBytes(data, format)
- AddImageFromBytesWithSize(data, format, size)
- AddImageFromBytesWithPosition(data, format, size, pos)

New internal constructors:
- core.NewImageFromBytes(id, data, format)
- core.NewImageFromBytesWithSize(id, data, format, size)
- core.NewImageFromBytesWithPosition(id, data, format, size, pos)

Includes unit tests and updated example 08_images.

Closes #29
…around

The CLI handler previously wrote base64 image data to a temporary file
and then called AddImage(path). Now it uses AddImageFromBytes directly,
eliminating unnecessary disk I/O and temp file cleanup.
- Validate format against known image formats, reject unsupported values
- Normalize format (lowercase, trim dots, map jpg->jpeg, tiff->tif)
- Extract shared normalizeImageFormat() used by both detectImageFormat
  and NewImageFromBytes
- Fix getImageDimensions to use bytes.NewReader instead of
  strings.NewReader(string(data)) to avoid unnecessary allocation
- Remove double copy in NewImageFromBytes (Data() already returns copy)
- Use img.Format() for sourceName in paragraph methods to stay
  consistent after normalization
- Fix error arg: pass data instead of nil in InvalidArgument
- Add tests for invalid format rejection and format normalization
- Restore defensive copy of data in NewImageFromBytes to prevent
  caller mutation (consistent with NewImageFromPackage)
- Fix AddImageFromBytesWithPosition to use img.Format() instead of
  raw format parameter for sourceName consistency
- Fix jpg normalization test to use actual JPEG-encoded bytes instead
  of PNG bytes with JPG format declaration
- Bump version to 2.4.0 in doc.go, README, V2_DESIGN, V2_API_GUIDE
- Add CHANGELOG entry for v2.4.0 (PR #26 + PR #30)
- Add RELEASE_NOTES_v2.4.0.md with feature, fix, and migration notes
- Refresh README features list and Roadmap (release history + planned items)

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request updates the library to version 2.4.0, introducing a new in-memory image API that allows inserting images from byte slices without temporary files. It also fixes a critical issue where table cell merge metadata (gridSpan and vMerge) was lost during document round-trips. The CLI handler was updated to utilize the new image API for base64 data, and comprehensive tests were added to verify the fixes and new functionality. Feedback suggests improving type safety for error context maps.

if val, ok := getAttr(gs, "val"); ok && val != "" {
span, err := strconv.Atoi(val)
if err != nil {
return errors.WrapWithContext(err, opHydrateTableCell, map[string]interface{}{"attr": "gridSpan", "value": val})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The error context map uses map[string]interface{} which is flexible but lacks type safety. Consider defining a structured error context or using a more specific type to avoid potential runtime issues with map key access.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Release v2.4.0 combining two main efforts: (1) adding an in-memory image insertion API (no filesystem required), and (2) fixing DOCX read/write round-tripping so merged table cells preserve w:gridSpan and w:vMerge when reopening documents.

Changes:

  • Added AddImageFromBytes* APIs across ParagraphBuilder, domain.Paragraph, and internal/core constructors; updated CLI base64 image handling to use the in-memory path.
  • Fixed table hydration to restore merged-cell metadata (gridSpan, vMerge) and correctly map XML cells to grid columns.
  • Added regression/unit tests plus release/docs updates for v2.4.0.

Reviewed changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
internal/reader/reconstruct.go Hydrates gridSpan/vMerge from <w:tcPr> and remaps columns using gridSpan sums.
internal/core/paragraph.go Implements Paragraph.AddImageFromBytes* methods and attaches images via media/relationship managers.
internal/core/image.go Adds byte-based image constructors, format normalization, and fixes dimension detection to use bytes.NewReader.
internal/core/image_test.go Adds unit tests for NewImageFromBytes* including normalization and error cases.
builder.go Exposes builder fluent APIs AddImageFromBytes*.
domain/paragraph.go Extends the public Paragraph interface with AddImageFromBytes*.
cmd/docxgo/handlers.go CLI embeds base64 images via AddImageFromBytes* (no temp files).
builder_test.go Adds builder error-path tests for nil byte payloads.
examples/08_images/main.go Demonstrates in-memory image insertion in the examples suite.
docx_read_test.go Adds round-trip tests for gridSpan and vMerge preservation.
docs/V2_DESIGN.md Bumps version metadata to v2.4.0.
docs/V2_API_GUIDE.md Bumps version metadata to 2.4.0.
doc.go Bumps “Current version” to 2.4.0.
README.md Updates version, features list, and release history for v2.4.0.
CHANGELOG.md Adds v2.4.0 changelog section.
RELEASE_NOTES_v2.4.0.md Adds full release notes for v2.4.0.
Comments suppressed due to low confidence (1)

internal/core/image.go:299

  • normalizeImageFormat treats bmp/tiff/svg/webp as supported, but getImageDimensions relies on Go's image.DecodeConfig with only gif/jpeg/png decoders registered in this package, and MediaManager only maps MIME types for png/jpg/gif/bmp/tiff. As a result, some formats will either fail to decode (e.g., real BMP/TIFF/WebP/SVG) or be packaged with application/octet-stream (webp/svg). Consider either registering the required decoders + adding proper content types, or restricting the accepted formats here to what the library can actually decode/package reliably (or requiring explicit size for non-decodable formats).
// normalizeImageFormat validates and normalizes an ImageFormat value.
// Returns empty string for unsupported formats.
func normalizeImageFormat(format domain.ImageFormat) domain.ImageFormat {
	normalized := strings.ToLower(strings.TrimPrefix(string(format), "."))

	switch normalized {
	case "png":
		return domain.ImageFormatPNG
	case "jpg", "jpeg":
		return domain.ImageFormatJPEG
	case "gif":
		return domain.ImageFormatGIF
	case "bmp":
		return domain.ImageFormatBMP
	case "tif", "tiff":
		return domain.ImageFormatTIFF
	case "svg":
		return domain.ImageFormatSVG
	case "webp":
		return domain.ImageFormatWEBP
	default:
		return ""
	}

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread RELEASE_NOTES_v2.4.0.md Outdated

## Compatibility

- Backward compatible with v2.3.x — all additions are new methods on existing interfaces. Custom `domain.Paragraph` implementations outside this module will need to add the three `AddImageFromBytes*` methods.

Copilot AI May 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The compatibility note is internally inconsistent: adding methods to the exported domain.Paragraph interface is a breaking change for any downstream code that implements that interface (even if most consumers only use it). Consider rewording this to clarify that it’s source-compatible for typical callers, but breaking for custom domain.Paragraph implementations (or call it out explicitly as an API-breaking change).

Suggested change
- Backward compatible with v2.3.x — all additions are new methods on existing interfaces. Custom `domain.Paragraph` implementations outside this module will need to add the three `AddImageFromBytes*` methods.
- For typical callers using the built-in implementations, the new image APIs are additive and remain source-compatible with v2.3.x. However, adding methods to the exported `domain.Paragraph` interface is a source-breaking API change for any custom `domain.Paragraph` implementations outside this module; those implementations must add the three `AddImageFromBytes*` methods.

Copilot uses AI. Check for mistakes.
Comment thread docs/V2_DESIGN.md Outdated
@@ -1340,7 +1340,7 @@ See [CREDITS.md](../CREDITS.md) for complete project history.
---

**Last Updated**: February 2026

Copilot AI May 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This section updates the release/version to April 2026 / v2.4.0, but the footer still says Last Updated: February 2026. Consider updating the "Last Updated" date as well (or removing it) to avoid conflicting metadata.

Suggested change
**Last Updated**: February 2026
**Last Updated**: April 2026

Copilot uses AI. Check for mistakes.
Comment thread docs/V2_API_GUIDE.md Outdated

**Version**: 2.3.0
**Version**: 2.4.0
**Last Updated**: February 2026

Copilot AI May 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The guide version is bumped to 2.4.0, but the header still says Last Updated: February 2026. If this document is meant to track the current release, consider updating the last-updated date for consistency.

Suggested change
**Last Updated**: February 2026
**Last Updated**: May 2026

Copilot uses AI. Check for mistakes.
- Restrict in-memory image insertion to PNG/JPEG/GIF (formats with
  registered Go decoders + mapped MIME types). BMP/TIFF/SVG/WEBP now
  return a clear InvalidArgument from NewImageFromBytes* directing
  callers to AddImage(path). File-path API remains unchanged.
- Clarify v2.4.0 compatibility note: source-compatible for typical
  callers, breaking for custom domain.Paragraph implementations.
- Update Last Updated dates in V2_DESIGN and V2_API_GUIDE.
@mmonterroca

Copy link
Copy Markdown
Owner Author

Review feedback addressed in 93cf8a6

✅ [Copilot] Format support inconsistency in normalizeImageFormat (image.go)

Valid catch — image.DecodeConfig only has png/jpeg/gif decoders registered, so BMP/TIFF/SVG/WEBP would fail at dimension detection or be packaged as application/octet-stream. Added isDecodableInMemoryFormat() and now NewImageFromBytes* rejects non-decodable formats with a clear InvalidArgument error pointing callers to AddImage(path). The file-path API still supports the full format set unchanged.

✅ [Copilot] Compatibility note ambiguity (RELEASE_NOTES_v2.4.0.md)

Reworded to call out explicitly that the change is source-compatible for typical callers using the built-in implementations, but breaking for custom domain.Paragraph implementations outside this module. Also documented the supported in-memory formats (PNG/JPEG/GIF).

✅ [Copilot] Stale 'Last Updated' dates (V2_DESIGN.md, V2_API_GUIDE.md)

Both updated to April 2026.

🔵 [Gemini] map[string]interface{} in error context (reconstruct.go:2011)

Won't change. The signature comes from the existing errors.WrapWithContext API, which is the established pattern across this file (and matches the same usage in applyParagraphSpacing, applyParagraphIndent, etc.). Refactoring to a typed context is a project-wide change, out of scope for this release.

CI green, all tests pass.

@mmonterroca mmonterroca merged commit 44a7604 into master May 1, 2026
5 checks passed
@mmonterroca mmonterroca deleted the release/v2.4.0 branch May 1, 2026 02:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

AddImageFromBytes OpenDocument drops horizontal cell merges

2 participants