Skip to content

fix: normalize 16-bit PNG images to 8-bit for PDF embedding#598

Open
Krishnanand-G wants to merge 1 commit into
johnfercher:masterfrom
Krishnanand-G:fix/png-16bit-to-8bit-528
Open

fix: normalize 16-bit PNG images to 8-bit for PDF embedding#598
Krishnanand-G wants to merge 1 commit into
johnfercher:masterfrom
Krishnanand-G:fix/png-16bit-to-8bit-528

Conversation

@Krishnanand-G

@Krishnanand-G Krishnanand-G commented Jun 29, 2026

Copy link
Copy Markdown

Summary

Closes #528.

16-bit per channel PNGs fail when registering with gofpdf. This converts 16-bit PNG payloads to 8-bit NRGBA before embedding.

Changes

  • Normalize NRGBA64, RGBA64, and Gray16 PNG payloads in FromBytes
  • Table-driven tests for all three 16-bit image types

Test plan

  • go test ./internal/providers/gofpdf/... -run TestFromBytes -v

@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 340d4a8f-575c-47c2-a36b-ef649aaffdb4

📥 Commits

Reviewing files that changed from the base of the PR and between c4ae85e and 44af67f.

📒 Files selected for processing (2)
  • internal/providers/gofpdf/parseimage.go
  • internal/providers/gofpdf/parseimage_test.go

📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes
    • Improved PNG handling by normalizing PNG inputs before processing, improving compatibility across PNG variants (including 16-bit PNGs).
    • If PNG normalization fails, the operation now returns an error instead of proceeding with unverified data.
  • Tests
    • Expanded image conversion tests to ensure valid non-PNG inputs (such as JPG) return an image without errors.
    • Added coverage confirming 16-bit PNGs are normalized to 8-bit output, while 8-bit PNG inputs remain unchanged.

Walkthrough

FromBytes now normalizes PNG input before constructing entity.Image, converting 16-bit PNGs to 8-bit PNG bytes when needed. Tests cover JPG handling, 16-bit PNG normalization, and unchanged 8-bit PNG input.

Changes

16-bit PNG normalization

Layer / File(s) Summary
PNG normalization path
internal/providers/gofpdf/parseimage.go
Imports expand for PNG decoding and drawing; FromBytes conditionally normalizes PNG input before building entity.Image; pngBitDepth reads the PNG header bit depth; normalizePNGBytes decodes 16-bit PNGs, copies them into NRGBA, and re-encodes PNG output.
PNG normalization tests
internal/providers/gofpdf/parseimage_test.go
The test file adds require, extends TestFromBytes with a valid JPG case, adds coverage for normalizing multiple 16-bit PNG image types, and adds a check that 8-bit PNG bytes remain unchanged.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐰 Sixteen bits hopped in so bright,
and came out eight-bit, neat and light.
PNGs boing through, crisp and true,
then land in PDFs, all shiny-new.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately describes the main change: normalizing 16-bit PNGs for PDF embedding.
Description check ✅ Passed The description covers the summary, related issue reference, changes, and test plan, with only minor template mismatches.
Linked Issues check ✅ Passed The PR satisfies #528 by converting 16-bit PNG inputs to 8-bit before embedding and covers the expected image types in tests.
Out of Scope Changes check ✅ Passed The changes stay focused on PNG normalization and tests, with no obvious unrelated additions.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/providers/gofpdf/parseimage_test.go`:
- Around line 38-59: The PNG normalization test only exercises the
*image.NRGBA64 path in TestFromBytes_Normalizes16BitPNG, so it misses the other
16-bit decode branches handled by is16BitImage. Extend this test to cover
*image.RGBA64 and *image.Gray16 as additional inputs through gofpdf.FromBytes,
and verify each is normalized to an 8-bit PNG after decoding img.Bytes. Keep the
checks anchored around TestFromBytes_Normalizes16BitPNG, mustDecodePNG, and
gofpdf.FromBytes so the regression coverage stays tied to the existing
normalization logic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 013700f1-0af7-49e5-8b35-7193440eefd3

📥 Commits

Reviewing files that changed from the base of the PR and between aa2645f and 3d4a7c7.

📒 Files selected for processing (3)
  • internal/providers/gofpdf/parseimage.go
  • internal/providers/gofpdf/parseimage_test.go
  • internal/providers/gofpdf/pngutil.go

Comment thread internal/providers/gofpdf/parseimage_test.go
@Krishnanand-G

Copy link
Copy Markdown
Author

Addressed review feedback in 3daf134:

  • Merged \pngutil.go\ into \parseimage.go\ and inlined the type switch (removed separate \is16BitImage\ helper)
  • Added table-driven tests for NRGBA64, RGBA64, and Gray16
  • Assertion now checks the output is no longer a 16-bit image type (Gray16 encodes back as *image.Gray, not *image.NRGBA)

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/providers/gofpdf/parseimage_test.go`:
- Around line 82-87: The test in parseimage_test.go only checks the decoded PNG
type, so it can pass even when normalization produces the wrong 8-bit image.
Update the assertions around png.Decode and the switch cases in the
parseImage-related test to verify at least one known pixel value (and optionally
image bounds) for each expected conversion path, using the existing
parseImage/normalize output and decoded image symbols to validate the actual
pixel data, not just the concrete type.

In `@internal/providers/gofpdf/parseimage.go`:
- Around line 22-25: The PNG handling in FromBytes currently always routes
through normalizePNGBytes, which forces an unnecessary full png.Decode on every
PNG input. Update the PNG path in parseimage.go to inspect the PNG IHDR bit
depth first and only call normalizePNGBytes when the image is actually 16-bit;
keep the existing fast path unchanged for already 8-bit PNGs. Use the FromBytes
and normalizePNGBytes symbols to locate the conditional so the common case
avoids extra decoding and allocation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9a8cc577-4f58-48b2-99a4-282917508fa3

📥 Commits

Reviewing files that changed from the base of the PR and between 3d4a7c7 and 3daf134.

📒 Files selected for processing (2)
  • internal/providers/gofpdf/parseimage.go
  • internal/providers/gofpdf/parseimage_test.go

Comment thread internal/providers/gofpdf/parseimage_test.go
Comment thread internal/providers/gofpdf/parseimage.go
@Krishnanand-G Krishnanand-G force-pushed the fix/png-16bit-to-8bit-528 branch 3 times, most recently from a0994f5 to c4ae85e Compare June 29, 2026 06:24

@coderabbitai coderabbitai 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.

♻️ Duplicate comments (2)
internal/providers/gofpdf/parseimage.go (1)

21-24: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Keep the 8-bit PNG path on the fast path.

This still decodes every PNG before checking whether the payload is actually 16-bit. Already-8-bit PNGs now pay an extra full decode/allocation even though the helper returns the original bytes unchanged. Sniff the IHDR bit depth first and skip normalization unless it is 16.

Suggested change
 func normalizePNGBytes(data []byte) ([]byte, error) {
+	if len(data) < 26 ||
+		!bytes.Equal(data[:8], []byte{137, 80, 78, 71, 13, 10, 26, 10}) ||
+		!bytes.Equal(data[12:16], []byte("IHDR")) ||
+		data[24] != 16 {
+		return data, nil
+	}
+
 	src, err := png.Decode(bytes.NewReader(data))
 	if err != nil {
 		return data, nil
 	}

Also applies to: 36-45

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/providers/gofpdf/parseimage.go` around lines 21 - 24, The PNG
handling in parseImage is doing unnecessary work for already-8-bit images by
always calling normalizePNGBytes before knowing the bit depth. Update parseImage
to inspect the PNG IHDR bit depth first and only invoke normalizePNGBytes for
16-bit payloads, keeping the 8-bit PNG path on the fast path. Use the existing
parseImage and normalizePNGBytes flow to preserve behavior while avoiding the
extra decode/allocation for non-16-bit PNGs.
internal/providers/gofpdf/parseimage_test.go (1)

82-87: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert a known pixel after normalization.

This only proves the decoded image is no longer one of the 16-bit concrete types. A broken conversion that returns the wrong 8-bit pixels would still pass. Compare at least one pixel (and ideally bounds) against the expected 8-bit value for each case.

Suggested change
 	cases := []struct {
 		name  string
 		build func() image.Image
+		want  color.Color
 	}{
 		{
 			name: "NRGBA64",
 			build: func() image.Image {
 				m := image.NewNRGBA64(image.Rect(0, 0, 2, 2))
 				m.SetNRGBA64(0, 0, color.NRGBA64{R: 65535, G: 32768, B: 16384, A: 65535})
 				return m
 			},
+			want: color.NRGBA{R: 0xFF, G: 0x80, B: 0x40, A: 0xFF},
 		},
 		{
 			name: "RGBA64",
 			build: func() image.Image {
 				m := image.NewRGBA64(image.Rect(0, 0, 2, 2))
 				m.SetRGBA64(0, 0, color.RGBA64{R: 65535, G: 32768, B: 16384, A: 65535})
 				return m
 			},
+			want: color.NRGBA{R: 0xFF, G: 0x80, B: 0x40, A: 0xFF},
 		},
 		{
 			name: "Gray16",
 			build: func() image.Image {
 				m := image.NewGray16(image.Rect(0, 0, 2, 2))
 				m.SetGray16(0, 0, color.Gray16{Y: 32768})
 				return m
 			},
+			want: color.Gray{Y: 0x80},
 		},
 	}
@@
-			var input bytes.Buffer
-			require.NoError(t, png.Encode(&input, tc.build()))
+			src := tc.build()
+			var input bytes.Buffer
+			require.NoError(t, png.Encode(&input, src))
@@
 			decoded, err := png.Decode(bytes.NewReader(img.Bytes))
 			require.NoError(t, err)
+			assert.Equal(t, src.Bounds(), decoded.Bounds())
 			switch decoded.(type) {
 			case *image.NRGBA64, *image.RGBA64, *image.Gray16:
 				t.Fatalf("expected 8-bit PNG, got %T", decoded)
 			}
+			wr, wg, wb, wa := tc.want.RGBA()
+			r, g, b, a := decoded.At(0, 0).RGBA()
+			assert.Equal(t, wr, r)
+			assert.Equal(t, wg, g)
+			assert.Equal(t, wb, b)
+			assert.Equal(t, wa, a)
 		})
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/providers/gofpdf/parseimage_test.go` around lines 82 - 87, The PNG
normalization test in parseimage_test.go only checks that decode does not return
16-bit concrete types, so it can miss incorrect pixel conversion. Update the
test around png.Decode and the type switch to also assert the normalized image
bounds and compare at least one known pixel value against the expected 8-bit
result for each case, ensuring ParseImage returns correctly converted 8-bit
pixels rather than just the right image type.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@internal/providers/gofpdf/parseimage_test.go`:
- Around line 82-87: The PNG normalization test in parseimage_test.go only
checks that decode does not return 16-bit concrete types, so it can miss
incorrect pixel conversion. Update the test around png.Decode and the type
switch to also assert the normalized image bounds and compare at least one known
pixel value against the expected 8-bit result for each case, ensuring ParseImage
returns correctly converted 8-bit pixels rather than just the right image type.

In `@internal/providers/gofpdf/parseimage.go`:
- Around line 21-24: The PNG handling in parseImage is doing unnecessary work
for already-8-bit images by always calling normalizePNGBytes before knowing the
bit depth. Update parseImage to inspect the PNG IHDR bit depth first and only
invoke normalizePNGBytes for 16-bit payloads, keeping the 8-bit PNG path on the
fast path. Use the existing parseImage and normalizePNGBytes flow to preserve
behavior while avoiding the extra decode/allocation for non-16-bit PNGs.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: b371b96c-136b-45ec-901a-92c031c9110a

📥 Commits

Reviewing files that changed from the base of the PR and between 3daf134 and 55328b3.

📒 Files selected for processing (2)
  • internal/providers/gofpdf/parseimage.go
  • internal/providers/gofpdf/parseimage_test.go

@Krishnanand-G Krishnanand-G force-pushed the fix/png-16bit-to-8bit-528 branch from c4ae85e to 44af67f Compare June 29, 2026 08:30
@Krishnanand-G

Copy link
Copy Markdown
Author

Addressed CodeRabbit feedback in 44af67f:

  • Performance: Sniff PNG IHDR bit depth first; only decode/re-encode when bit depth is 16
  • Tests: Assert bounds and converted pixel values per 16-bit case (NRGBA64, RGBA64, Gray16)
  • Tests: Added passthrough test confirming 8-bit PNG bytes are returned unchanged

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.

PDF generation fails with 16-bit per channel PNG images

1 participant