diff --git a/src/ImageProcessorCore/Formats/Jpg/JpegEncoder.cs b/src/ImageProcessorCore/Formats/Jpg/JpegEncoder.cs
index 3236524bf..8c2509514 100644
--- a/src/ImageProcessorCore/Formats/Jpg/JpegEncoder.cs
+++ b/src/ImageProcessorCore/Formats/Jpg/JpegEncoder.cs
@@ -18,6 +18,12 @@ namespace ImageProcessorCore.Formats
///
private int quality = 75;
+ ///
+ /// The subsamples used to encode the image.
+ ///
+ private JpegSubsample subsample = JpegSubsample.Ratio420;
+ private bool subsampleSet = false;
+
///
/// Gets or sets the quality, that will be used to encode the image. Quality
/// index must be between 0 and 100 (compression from max to min).
@@ -29,6 +35,16 @@ namespace ImageProcessorCore.Formats
set { this.quality = value.Clamp(1, 100); }
}
+ ///
+ /// Gets or sets the subsample ration, that will be used to encode the image.
+ ///
+ /// The subsample ratio of the jpg image.
+ public JpegSubsample Subsample
+ {
+ get { return this.subsample; }
+ set { this.subsample = value; subsampleSet = true; }
+ }
+
///
public string MimeType => "image/jpeg";
@@ -56,8 +72,11 @@ namespace ImageProcessorCore.Formats
Guard.NotNull(image, nameof(image));
Guard.NotNull(stream, nameof(stream));
- JpegEncoderCore encode = new JpegEncoderCore();
- encode.Encode(stream, image, this.Quality);
+ JpegEncoderCore encode = new JpegEncoderCore();
+ if(subsampleSet)
+ encode.Encode(stream, image, this.Quality, this.Subsample);
+ else
+ encode.Encode(stream, image, this.Quality, this.Quality >= 80 ? JpegSubsample.Ratio444 : JpegSubsample.Ratio420);
}
}
}
diff --git a/src/ImageProcessorCore/Formats/Jpg/JpegEncoderCore.cs b/src/ImageProcessorCore/Formats/Jpg/JpegEncoderCore.cs
index fa2d52152..be5fe09ab 100644
--- a/src/ImageProcessorCore/Formats/Jpg/JpegEncoderCore.cs
+++ b/src/ImageProcessorCore/Formats/Jpg/JpegEncoderCore.cs
@@ -223,6 +223,7 @@
private byte[][] quant = new byte[nQuantIndex][];//[Block.blockSize];
// theHuffmanLUT are compiled representations of theHuffmanSpec.
private huffmanLUT[] theHuffmanLUT = new huffmanLUT[4];
+ private JpegSubsample subsample;
private void writeByte(byte b)
{
@@ -304,8 +305,19 @@
// writeSOF0 writes the Start Of Frame (Baseline) marker.
private void writeSOF0(int wid, int hei, int nComponent)
{
- byte[] chroma1 = new byte[] { 0x22, 0x11, 0x11 };
- byte[] chroma2 = new byte[] { 0x00, 0x01, 0x01 };
+ //"default" to 4:2:0
+ byte[] subsamples = new byte[] { 0x22, 0x11, 0x11 };
+ byte[] chroma = new byte[] { 0x00, 0x01, 0x01 };
+
+ switch (subsample)
+ {
+ case JpegSubsample.Ratio444:
+ subsamples = new byte[] { 0x11, 0x11, 0x11 };
+ break;
+ case JpegSubsample.Ratio420:
+ subsamples = new byte[] { 0x22, 0x11, 0x11 };
+ break;
+ }
int markerlen = 8 + 3 * nComponent;
writeMarkerHeader(sof0Marker, markerlen);
@@ -328,8 +340,8 @@
{
buf[3 * i + 6] = (byte)(i + 1);
// We use 4:2:0 chroma subsampling.
- buf[3 * i + 7] = chroma1[i];
- buf[3 * i + 8] = chroma2[i];
+ buf[3 * i + 7] = subsamples[i];
+ buf[3 * i + 8] = chroma[i];
}
}
outputStream.Write(buf, 0, 3 * (nComponent - 1) + 9);
@@ -425,7 +437,7 @@
// scale scales the 16x16 region represented by the 4 src blocks to the 8x8
// dst block.
- private void scale(Block dst, Block[] src)
+ private void scale_16x16_8x8(Block dst, Block[] src)
{
for (int i = 0; i < 4; i++)
{
@@ -436,7 +448,7 @@
{
int j = 16 * y + 2 * x;
int sum = src[i][j] + src[i][j + 1] + src[i][j + 8] + src[i][j + 9];
- dst[8 * y + x + dstOff] = (sum + 2) >> 2;
+ dst[8 * y + x + dstOff] = (sum + 2) / 4;
}
}
}
@@ -467,11 +479,47 @@
0x11, 0x03, 0x11, 0x00, 0x3f, 0x00,
};
+
// writeSOS writes the StartOfScan marker.
private void writeSOS(ImageBase m)
{
outputStream.Write(sosHeaderYCbCr, 0, sosHeaderYCbCr.Length);
+ switch (subsample)
+ {
+ case JpegSubsample.Ratio444:
+ encode444(m);
+ break;
+ case JpegSubsample.Ratio420:
+ encode420(m);
+ break;
+ }
+
+ // Pad the last byte with 1's.
+ emit(0x7f, 7);
+ }
+
+ private void encode444(ImageBase m)
+ {
+ Block b = new Block();
+ Block cb = new Block();
+ Block cr = new Block();
+ int prevDCY = 0, prevDCCb = 0, prevDCCr = 0;
+
+ for (int y = 0; y < m.Height; y += 8)
+ {
+ for (int x = 0; x < m.Width; x += 8)
+ {
+ toYCbCr(m, x, y, b, cb, cr);
+ prevDCY = writeBlock(b, (quantIndex)0, prevDCY);
+ prevDCCb = writeBlock(cb, (quantIndex)1, prevDCCb);
+ prevDCCr = writeBlock(cr, (quantIndex)1, prevDCCr);
+ }
+ }
+ }
+
+ private void encode420(ImageBase m)
+ {
Block b = new Block();
Block[] cb = new Block[4];
Block[] cr = new Block[4];
@@ -490,24 +538,22 @@
int yOff = (i & 2) * 4;
toYCbCr(m, x + xOff, y + yOff, b, cb[i], cr[i]);
- prevDCY = writeBlock(b, 0, prevDCY);
+ prevDCY = writeBlock(b, (quantIndex)0, prevDCY);
}
- scale(b, cb);
+ scale_16x16_8x8(b, cb);
prevDCCb = writeBlock(b, (quantIndex)1, prevDCCb);
- scale(b, cr);
+ scale_16x16_8x8(b, cr);
prevDCCr = writeBlock(b, (quantIndex)1, prevDCCr);
}
}
-
- // Pad the last byte with 1's.
- emit(0x7f, 7);
}
// Encode writes the Image m to w in JPEG 4:2:0 baseline format with the given
// options. Default parameters are used if a nil *Options is passed.
- public void Encode(Stream stream, ImageBase m, int quality)
+ public void Encode(Stream stream, ImageBase m, int quality, JpegSubsample subsample)
{
this.outputStream = stream;
+ this.subsample = subsample;
for (int i = 0; i < theHuffmanSpec.Length; i++)
{
diff --git a/src/ImageProcessorCore/Formats/Jpg/JpegSubsample.cs b/src/ImageProcessorCore/Formats/Jpg/JpegSubsample.cs
new file mode 100644
index 000000000..20b884b05
--- /dev/null
+++ b/src/ImageProcessorCore/Formats/Jpg/JpegSubsample.cs
@@ -0,0 +1,8 @@
+namespace ImageProcessorCore.Formats
+{
+ public enum JpegSubsample
+ {
+ Ratio444,
+ Ratio420,
+ }
+}