Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,9 @@ against an empty feed.
- **Baseline JPEG images are decoded.** `DCTDecode` now yields samples — `Gray8` for one component,
`Rgb24` for three (YCbCr converted) and `Cmyk32` for four with the Adobe YCCK transform — while `Data`
keeps the stored JPEG and `DecodedPixels` carries the samples, so a caller can have either. Baseline and
extended sequential (SOF0/SOF1) are decoded; progressive (SOF2) and the fancy chroma upsampling a
subsampled fixture needs are the next step, per ADR-005.
extended sequential (SOF0/SOF1) are decoded, and a subsampled component is upsampled with libjpeg's
3:1 fancy filter, so a 4:2:0 fixture matches a reference decoder within two levels. Progressive (SOF2)
is the next step, per ADR-005.

## [1.8.0] - 2026-11-14

Expand Down
9 changes: 4 additions & 5 deletions docs/adr/0005-image-filter-decoders.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,17 +132,16 @@ their own fixtures and the same ±2 tolerance.

The baseline decoder is implemented: `PdfJpegFilterTests` decodes both fixtures within two levels of
Pillow, and the contract above holds — `Data` is the stored JPEG, `DecodedPixels` the samples. The variants
the mutation survivors asked for are there too: `jpeg-420.jpg` (4:2:0, nearest upsampling and a documented
edge tolerance), `jpeg-cmyk.jpg` (four components, inverted as the `/Decode` array states) and
`jpeg-restart.jpg` (restart markers). The fancy upsampling that matches Pillow at every edge, and SOF2,
are the next tests.
the mutation survivors asked for are there too: `jpeg-420.jpg` (4:2:0, decoded with libjpeg's 3:1 fancy
filter and within two levels of Pillow), `jpeg-cmyk.jpg` (four components, inverted as the `/Decode` array
states) and `jpeg-restart.jpg` (restart markers). SOF2 is the next slice.

## Status of the slices

| Slice | State |
| --- | --- |
| `RunLengthDecode` | **Implemented** (2026-11-14) |
| `CCITTFaxDecode` | **Implemented** (2026-11-15) |
| `DCTDecode` | **Baseline (SOF0/SOF1) implemented** (2026-11-15); progressive (SOF2) next |
| `DCTDecode` | **Baseline (SOF0/SOF1) implemented, fancy upsampling** (2026-11-15); progressive (SOF2) next |
| `JPXDecode` | Planned |
| `JBIG2Decode` | Planned |
68 changes: 65 additions & 3 deletions src/SharpCorePDF.Infrastructure/Pdf/JpegDecoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
out int height) =>
new JpegDecoder(data).Run(maxBytes, out pixels, out format, out width, out height);

private bool Run(int maxBytes, out byte[]? pixels, out string? format, out int width, out int height)

Check warning on line 66 in src/SharpCorePDF.Infrastructure/Pdf/JpegDecoder.cs

View workflow job for this annotation

GitHub Actions / Build, test and analyse

Refactor this method to reduce its Cognitive Complexity from 41 to the 15 allowed.

Check warning on line 66 in src/SharpCorePDF.Infrastructure/Pdf/JpegDecoder.cs

View workflow job for this annotation

GitHub Actions / Build, test and analyse

Refactor this method to reduce its Cognitive Complexity from 41 to the 15 allowed.

Check warning on line 66 in src/SharpCorePDF.Infrastructure/Pdf/JpegDecoder.cs

View workflow job for this annotation

GitHub Actions / Build, test and analyse

Refactor this method to reduce its Cognitive Complexity from 41 to the 15 allowed.

Check warning on line 66 in src/SharpCorePDF.Infrastructure/Pdf/JpegDecoder.cs

View workflow job for this annotation

GitHub Actions / Build, test and analyse

Refactor this method to reduce its Cognitive Complexity from 41 to the 15 allowed.
{
pixels = null;
format = null;
Expand Down Expand Up @@ -402,7 +402,7 @@
public int Predictor { get; set; }
}

private bool DecodeScan(

Check warning on line 405 in src/SharpCorePDF.Infrastructure/Pdf/JpegDecoder.cs

View workflow job for this annotation

GitHub Actions / Build, test and analyse

Refactor this method to reduce its Cognitive Complexity from 40 to the 15 allowed.

Check warning on line 405 in src/SharpCorePDF.Infrastructure/Pdf/JpegDecoder.cs

View workflow job for this annotation

GitHub Actions / Build, test and analyse

Refactor this method to reduce its Cognitive Complexity from 40 to the 15 allowed.

Check warning on line 405 in src/SharpCorePDF.Infrastructure/Pdf/JpegDecoder.cs

View workflow job for this annotation

GitHub Actions / Build, test and analyse

Refactor this method to reduce its Cognitive Complexity from 40 to the 15 allowed.

Check warning on line 405 in src/SharpCorePDF.Infrastructure/Pdf/JpegDecoder.cs

View workflow job for this annotation

GitHub Actions / Build, test and analyse

Refactor this method to reduce its Cognitive Complexity from 40 to the 15 allowed.
int scanStart,
int maxBytes,
out byte[]? pixels,
Expand Down Expand Up @@ -481,7 +481,7 @@
return BuildOutput(maxBytes, out pixels, out format);
}

private bool DecodeBlock(ref JpegBits bits, JpegComponent component, int[] coefficients, byte[] samples)

Check warning on line 484 in src/SharpCorePDF.Infrastructure/Pdf/JpegDecoder.cs

View workflow job for this annotation

GitHub Actions / Build, test and analyse

Refactor this method to reduce its Cognitive Complexity from 18 to the 15 allowed.

Check warning on line 484 in src/SharpCorePDF.Infrastructure/Pdf/JpegDecoder.cs

View workflow job for this annotation

GitHub Actions / Build, test and analyse

Refactor this method to reduce its Cognitive Complexity from 18 to the 15 allowed.

Check warning on line 484 in src/SharpCorePDF.Infrastructure/Pdf/JpegDecoder.cs

View workflow job for this annotation

GitHub Actions / Build, test and analyse

Refactor this method to reduce its Cognitive Complexity from 18 to the 15 allowed.

Check warning on line 484 in src/SharpCorePDF.Infrastructure/Pdf/JpegDecoder.cs

View workflow job for this annotation

GitHub Actions / Build, test and analyse

Refactor this method to reduce its Cognitive Complexity from 18 to the 15 allowed.
{
var dcTable = dcTables[component.DcTable];
var acTable = acTables[component.AcTable];
Expand Down Expand Up @@ -576,7 +576,7 @@
}
}

private bool BuildOutput(int maxBytes, out byte[]? pixels, out string? format)

Check warning on line 579 in src/SharpCorePDF.Infrastructure/Pdf/JpegDecoder.cs

View workflow job for this annotation

GitHub Actions / Build, test and analyse

Refactor this method to reduce its Cognitive Complexity from 26 to the 15 allowed.

Check warning on line 579 in src/SharpCorePDF.Infrastructure/Pdf/JpegDecoder.cs

View workflow job for this annotation

GitHub Actions / Build, test and analyse

Refactor this method to reduce its Cognitive Complexity from 26 to the 15 allowed.

Check warning on line 579 in src/SharpCorePDF.Infrastructure/Pdf/JpegDecoder.cs

View workflow job for this annotation

GitHub Actions / Build, test and analyse

Refactor this method to reduce its Cognitive Complexity from 26 to the 15 allowed.

Check warning on line 579 in src/SharpCorePDF.Infrastructure/Pdf/JpegDecoder.cs

View workflow job for this annotation

GitHub Actions / Build, test and analyse

Refactor this method to reduce its Cognitive Complexity from 26 to the 15 allowed.
{
pixels = null;
format = null;
Expand Down Expand Up @@ -612,9 +612,16 @@
for (var component = 0; component < count; component++)
{
var source = components[component];
var sx = (x * source.H) / maxH;
var sy = (y * source.V) / maxV;
samples[component] = source.Plane[(sy * source.Stride) + sx];
samples[component] = Sample(
source.Plane,
source.Stride,
source.Stride == 0 ? 0 : source.Plane.Length / source.Stride,
x,
y,
source.H,
source.V,
maxH,
maxV);
}

if (count == 1)
Expand Down Expand Up @@ -663,6 +670,61 @@
return true;
}

/// <summary>
/// Reads one sample, upsampling a subsampled component with the 3:1 filter libjpeg calls fancy: an
/// even output position takes three parts of its sample and one of the neighbour before it, an odd one
/// three parts and one of the neighbour after it, rounded as libjpeg rounds.
/// </summary>
private static int Sample(

Check warning on line 678 in src/SharpCorePDF.Infrastructure/Pdf/JpegDecoder.cs

View workflow job for this annotation

GitHub Actions / Build, test and analyse

Method has 9 parameters, which is greater than the 7 authorized.

Check warning on line 678 in src/SharpCorePDF.Infrastructure/Pdf/JpegDecoder.cs

View workflow job for this annotation

GitHub Actions / Build, test and analyse

Method has 9 parameters, which is greater than the 7 authorized.

Check warning on line 678 in src/SharpCorePDF.Infrastructure/Pdf/JpegDecoder.cs

View workflow job for this annotation

GitHub Actions / Build, test and analyse

Method has 9 parameters, which is greater than the 7 authorized.

Check warning on line 678 in src/SharpCorePDF.Infrastructure/Pdf/JpegDecoder.cs

View workflow job for this annotation

GitHub Actions / Build, test and analyse

Method has 9 parameters, which is greater than the 7 authorized.
byte[] plane,
int stride,
int rows,
int x,
int y,
int horizontal,
int vertical,
int maxH,
int maxV)
{
if (horizontal == maxH && vertical == maxV)
{
return plane[(y * stride) + x];
}

var sx = (x * horizontal) / maxH;
var sy = (y * vertical) / maxV;
var fancyX = horizontal * 2 == maxH;
var fancyY = vertical * 2 == maxV;

if (!fancyX && !fancyY)
{
return plane[(sy * stride) + sx];
}

var left = Math.Max(sx - 1, 0);
var right = Math.Min(sx + 1, stride - 1);

var centre = Column(sx);
var before = Column(left);
var after = Column(right);

return x % 2 == 0
? ((3 * centre) + before + 1) >> 2
: ((3 * centre) + after + 2) >> 2;

int Column(int column)
{
if (!fancyY)
{
return plane[(sy * stride) + column];
}

return y % 2 == 0
? ((3 * plane[(sy * stride) + column]) + plane[(Math.Max(sy - 1, 0) * stride) + column] + 1) >> 2
: ((3 * plane[(sy * stride) + column]) + plane[(Math.Min(sy + 1, rows - 1) * stride) + column] + 2) >> 2;
}
}

private static byte Clamp(double value) =>
(byte)Math.Clamp((int)Math.Round(value), 0, 255);

Expand Down
8 changes: 4 additions & 4 deletions tests/SharpCorePDF.Tests/PdfJpegFilterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,18 +50,18 @@ public void Decodes_a_baseline_gray_jpeg_within_two_levels()
}

/// <summary>
/// A 4:2:0 JPEG halves the chroma in both directions. The decoder upsamples with the nearest sample,
/// where Pillow uses the fancy filter, so the tolerance is the edge difference rather than two levels.
/// A 4:2:0 JPEG halves the chroma in both directions; the decoder upsamples with the same 3:1 filter
/// libjpeg (and so Pillow) uses, which keeps the comparison inside the two-level tolerance.
/// </summary>
[Fact]
public void Decodes_a_subsampled_jpeg_within_the_upsampling_tolerance()
public void Decodes_a_subsampled_jpeg_within_two_levels()
{
Assert.True(JpegDecoder.TryDecode(Rgb420, 1 << 20, out var pixels, out var format, out var width, out var height));

Assert.Equal("Rgb24", format);
Assert.Equal(32, width);
Assert.Equal(32, height);
Assert.InRange(MaxDifference(pixels!, Rgb420Expected), 0, 6);
Assert.InRange(MaxDifference(pixels!, Rgb420Expected), 0, 2);
}

/// <summary>
Expand Down
Loading