#include "jpeg_encode.h"
#include "huffman.h"
#include "jpeg_common.h"
#include <algorithm>
#include <array>
#include <bit>
#include <cmath>
#include <cstring>
namespace whiteout::textures::jpeg {
namespace {
constexpr i32 QUALITY_LOW_NUMERATOR = 5000;
constexpr i32 QUALITY_HIGH_BASE = 200;
constexpr u16 DQT_8BIT_SEGMENT_LENGTH = 2 + 1 + BLOCK_PIXELS;
constexpr u8 SAMPLING_FACTOR_1x1 = 0x11;
struct SpectralBand {
u8 ss;
u8 se;
};
constexpr std::array<SpectralBand, 2> DEFAULT_AC_BANDS = {{
{1, 5},
{6, 63},
}};
constexpr std::array<u8, BLOCK_PIXELS> STD_LUMINANCE_QUANT_NATURAL = {{
16, 11, 10, 16, 24, 40, 51, 61, 12, 12, 14, 19, 26, 58, 60, 55,
14, 13, 16, 24, 40, 57, 69, 56, 14, 17, 22, 29, 51, 87, 80, 62,
18, 22, 37, 56, 68, 109, 103, 77, 24, 35, 55, 64, 81, 104, 113, 92,
49, 64, 78, 87, 103, 121, 120, 101, 72, 92, 95, 98, 112, 100, 103, 99,
}};
constexpr std::array<f32, BLOCK_SIZE> AAN_SCALE_FACTORS = {{
1.0f,
SQRT2_F* dct_cos(1),
SQRT2_F* dct_cos(2),
SQRT2_F* dct_cos(3),
SQRT2_F* dct_cos(4),
SQRT2_F* dct_cos(5),
SQRT2_F* dct_cos(6),
SQRT2_F* dct_cos(7),
}};
constexpr auto NATURAL_TO_ZIGZAG = []() {
std::array<u8, BLOCK_PIXELS> table{};
for (i32 zz = 0; zz < BLOCK_PIXELS; ++zz) {
table[ZIGZAG_ORDER[zz]] = static_cast<u8>(zz);
}
return table;
}();
std::array<f32, BLOCK_PIXELS> build_quant_reciprocal_natural(
const std::array<u16, BLOCK_PIXELS>& quantTableZigzag) {
std::array<f32, BLOCK_PIXELS> reciprocal{};
for (i32 nat = 0; nat < BLOCK_PIXELS; ++nat) {
const i32 zz = NATURAL_TO_ZIGZAG[nat];
const i32 row = nat / BLOCK_SIZE;
const i32 col = nat % BLOCK_SIZE;
const f32 divisor = static_cast<f32>(quantTableZigzag[zz]) * AAN_SCALE_FACTORS[row] *
AAN_SCALE_FACTORS[col];
reciprocal[nat] = DCT_2D_NORMALISATION / divisor;
}
return reciprocal;
}
inline void fdct_1d_inplace(f32* data) {
const f32 d0 = data[0], d1 = data[1], d2 = data[2], d3 = data[3];
const f32 d4 = data[4], d5 = data[5], d6 = data[6], d7 = data[7];
const f32 tmp0 = d0 + d7, tmp7 = d0 - d7;
const f32 tmp1 = d1 + d6, tmp6 = d1 - d6;
const f32 tmp2 = d2 + d5, tmp5 = d2 - d5;
const f32 tmp3 = d3 + d4, tmp4 = d3 - d4;
const f32 es03 = tmp0 + tmp3, ed03 = tmp0 - tmp3;
const f32 es12 = tmp1 + tmp2, ed12 = tmp1 - tmp2;
const f32 z1 = (ed12 + ed03) * COS_PI_OVER_4;
const f32 os45 = tmp4 + tmp5;
const f32 os56 = tmp5 + tmp6;
const f32 os67 = tmp6 + tmp7;
const f32 z5 = (os45 - os67) * SIN_PI_OVER_8;
const f32 z2 = EVEN_ROTATION_K * os45 + z5;
const f32 z4 = SQRT2_COS_PI_OVER_8 * os67 + z5;
const f32 z3 = os56 * COS_PI_OVER_4;
const f32 z11 = tmp7 + z3;
const f32 z13 = tmp7 - z3;
data[0] = es03 + es12;
data[4] = es03 - es12;
data[2] = ed03 + z1;
data[6] = ed03 - z1;
data[5] = z13 + z2;
data[3] = z13 - z2;
data[1] = z11 + z4;
data[7] = z11 - z4;
}
inline void transpose_8x8_inplace(f32* block) {
for (i32 i = 0; i < BLOCK_SIZE; ++i) {
for (i32 j = i + 1; j < BLOCK_SIZE; ++j) {
const i32 a = i * BLOCK_SIZE + j;
const i32 b = j * BLOCK_SIZE + i;
const f32 tmp = block[a];
block[a] = block[b];
block[b] = tmp;
}
}
}
void forward_dct_and_quantise(const u8* inputPixels, u32 inputRowStride,
std::array<i32, BLOCK_PIXELS>& quantisedCoeffs,
const std::array<f32, BLOCK_PIXELS>& quantReciprocalNatural) {
alignas(32) f32 block[BLOCK_PIXELS];
for (i32 row = 0; row < BLOCK_SIZE; ++row) {
const u8* srcRow = inputPixels + row * inputRowStride;
f32* dst = block + row * BLOCK_SIZE;
for (i32 col = 0; col < BLOCK_SIZE; ++col) {
dst[col] = static_cast<f32>(srcRow[col]) - DC_LEVEL_SHIFT;
}
}
for (i32 row = 0; row < BLOCK_SIZE; ++row) {
fdct_1d_inplace(block + row * BLOCK_SIZE);
}
transpose_8x8_inplace(block);
for (i32 col = 0; col < BLOCK_SIZE; ++col) {
fdct_1d_inplace(block + col * BLOCK_SIZE);
}
transpose_8x8_inplace(block);
for (i32 nat = 0; nat < BLOCK_PIXELS; ++nat) {
const f32 val = block[nat] * quantReciprocalNatural[nat];
quantisedCoeffs[NATURAL_TO_ZIGZAG[nat]] = static_cast<i32>(val + std::copysignf(0.5f, val));
}
}
struct CategoryMagnitude {
i32 category;
u32 magnitude;
};
CategoryMagnitude compute_category_magnitude(i32 value) {
const i32 mask = value >> 31; const u32 abs = static_cast<u32>((value ^ mask) - mask); const i32 cat = static_cast<i32>(std::bit_width(abs));
const u32 mag = static_cast<u32>(value + (mask & ((1 << cat) - 1)));
return {cat, mag};
}
std::array<u16, BLOCK_PIXELS> build_quant_table(i32 quality) {
quality = std::clamp(quality, 1, 100);
i32 const scaleFactor =
(quality < 50) ? (QUALITY_LOW_NUMERATOR / quality) : (QUALITY_HIGH_BASE - quality * 2);
std::array<u16, BLOCK_PIXELS> quantTableZigzag{};
for (i32 zigzagIndex = 0; zigzagIndex < BLOCK_PIXELS; zigzagIndex++) {
i32 const naturalPosition = ZIGZAG_ORDER[zigzagIndex];
i32 const baseValue = static_cast<i32>(STD_LUMINANCE_QUANT_NATURAL[naturalPosition]);
i32 scaledValue = (baseValue * scaleFactor + 50) / 100;
scaledValue = std::clamp(scaledValue, 1, 255);
quantTableZigzag[zigzagIndex] = static_cast<u16>(scaledValue);
}
return quantTableZigzag;
}
void write_u8(std::vector<u8>& out, u8 value) {
out.push_back(value);
}
void write_u16_be(std::vector<u8>& out, u16 value) {
out.push_back(static_cast<u8>(value >> 8));
out.push_back(static_cast<u8>(value & 0xFF));
}
void write_marker(std::vector<u8>& out, u8 markerCode) {
out.push_back(0xFF);
out.push_back(markerCode);
}
void write_soi(std::vector<u8>& out) {
write_marker(out, MARKER_SOI);
}
void write_dqt(std::vector<u8>& out, u8 tableIndex,
const std::array<u16, BLOCK_PIXELS>& quantTable) {
write_marker(out, MARKER_DQT);
write_u16_be(out, DQT_8BIT_SEGMENT_LENGTH);
write_u8(out, tableIndex); for (i32 coefficient = 0; coefficient < BLOCK_PIXELS; coefficient++) {
write_u8(out, static_cast<u8>(quantTable[coefficient]));
}
}
void write_sof(std::vector<u8>& out, u8 sofMarker, u32 width, u32 height, u32 componentCount) {
write_marker(out, sofMarker);
u16 const segmentLength = static_cast<u16>(8 + componentCount * 3);
write_u16_be(out, segmentLength);
write_u8(out, 8); write_u16_be(out, static_cast<u16>(height));
write_u16_be(out, static_cast<u16>(width));
write_u8(out, static_cast<u8>(componentCount));
for (u32 componentIndex = 0; componentIndex < componentCount; componentIndex++) {
write_u8(out, static_cast<u8>(componentIndex + 1)); write_u8(out, SAMPLING_FACTOR_1x1); write_u8(out, 0); }
}
void write_dht(std::vector<u8>& out, u8 tableClass, u8 tableIndex, const u8* lengthCounts,
const u8* symbols, i32 symbolCount) {
write_marker(out, MARKER_DHT);
u16 const segmentLength = static_cast<u16>(2 + 1 + 16 + symbolCount);
write_u16_be(out, segmentLength);
write_u8(out, static_cast<u8>((tableClass << 4) | tableIndex));
for (i32 lengthIndex = 0; lengthIndex < 16; lengthIndex++) {
write_u8(out, lengthCounts[lengthIndex]);
}
for (i32 symbolIndex = 0; symbolIndex < symbolCount; symbolIndex++) {
write_u8(out, symbols[symbolIndex]);
}
}
void write_sos(std::vector<u8>& out, const u8* compIds, const u8* dcIds, const u8* acIds, u32 count,
u8 ss, u8 se, u8 ah, u8 al) {
write_marker(out, MARKER_SOS);
write_u16_be(out, static_cast<u16>(6 + count * 2));
write_u8(out, static_cast<u8>(count));
for (u32 i = 0; i < count; ++i) {
write_u8(out, compIds[i]);
write_u8(out, static_cast<u8>((dcIds[i] << 4) | acIds[i]));
}
write_u8(out, ss);
write_u8(out, se);
write_u8(out, static_cast<u8>((ah << 4) | al));
}
void write_dri(std::vector<u8>& out, u16 restartInterval) {
write_marker(out, MARKER_DRI);
write_u16_be(out, 4); write_u16_be(out, restartInterval);
}
void write_rst(std::vector<u8>& out, u32 index) {
write_marker(out, static_cast<u8>(MARKER_RST0 + (index & 7)));
}
void write_eoi(std::vector<u8>& out) {
write_marker(out, MARKER_EOI);
}
void encode_dc_coefficient(BitstreamWriter& writer, const HuffmanEncodeTable& dcTable,
i32 dcDifference) {
const auto [cat, mag] = compute_category_magnitude(dcDifference);
const auto& hcode = dcTable.codes[cat];
writer.writeBits(hcode.code, hcode.length);
if (cat > 0) {
writer.writeBits(mag, cat);
}
}
void encode_ac_coefficients(BitstreamWriter& writer, const HuffmanEncodeTable& acTable,
const std::array<i32, BLOCK_PIXELS>& coeffs) {
i32 lastNonzero = BLOCK_PIXELS - 1;
while (lastNonzero > 0 && coeffs[lastNonzero] == 0)
--lastNonzero;
if (lastNonzero == 0) {
const auto& eob = acTable.codes[0x00];
writer.writeBits(eob.code, eob.length);
return;
}
i32 consecutiveZeros = 0;
for (i32 k = 1; k <= lastNonzero; ++k) {
if (coeffs[k] == 0) {
consecutiveZeros++;
continue;
}
while (consecutiveZeros > 15) {
const auto& zrl = acTable.codes[0xF0];
writer.writeBits(zrl.code, zrl.length);
consecutiveZeros -= 16;
}
const auto [cat, mag] = compute_category_magnitude(coeffs[k]);
const u8 rlSymbol = static_cast<u8>((consecutiveZeros << 4) | cat);
const auto& hc = acTable.codes[rlSymbol];
writer.writeBits(hc.code, hc.length);
writer.writeBits(mag, cat);
consecutiveZeros = 0;
}
if (lastNonzero < BLOCK_PIXELS - 1) {
const auto& eob = acTable.codes[0x00];
writer.writeBits(eob.code, eob.length);
}
}
u8 huffTableIndex(u32 compIndex, u32 componentCount) {
return (componentCount > 1 && compIndex > 0) ? 1 : 0;
}
struct JpegEncoder {
std::vector<u8> outputBuffer;
std::array<u16, BLOCK_PIXELS> quantTable{};
std::array<f32, BLOCK_PIXELS> quantReciprocal{};
std::array<HuffmanEncodeTable, 2> dcHuffTables;
std::array<HuffmanEncodeTable, 2> acHuffTables;
u32 imageWidth = 0;
u32 imageHeight = 0;
u32 componentCount = 0;
u32 mcuColumnsCount = 0;
u32 mcuRowsCount = 0;
std::string* errorOutput = nullptr;
JpegContext* ctx = nullptr;
bool reportError(const std::string& message) const {
if (errorOutput) {
*errorOutput = message;
}
return false;
}
bool initFromImage(const Image& image, i32 quality);
void writeAllDhtMarkers();
void buildComponentBuffers(const Image& image,
std::array<std::vector<u8>, MAX_COMPONENTS>& buffers,
std::array<u32, MAX_COMPONENTS>& strides) const;
bool encodeScanDataFromCoeffs(
const std::array<std::vector<std::array<i32, BLOCK_PIXELS>>, MAX_COMPONENTS>& coeffBlocks);
void buildCoefficientBlocks(
const std::array<std::vector<u8>, MAX_COMPONENTS>& buffers,
const std::array<u32, MAX_COMPONENTS>& strides,
std::array<std::vector<std::array<i32, BLOCK_PIXELS>>, MAX_COMPONENTS>& coeffBlocks) const;
bool encodeProgressiveDcScan(
const std::array<std::vector<std::array<i32, BLOCK_PIXELS>>, MAX_COMPONENTS>& coeffBlocks,
i32 al = 0);
bool encodeProgressiveDcRefineScan(
const std::array<std::vector<std::array<i32, BLOCK_PIXELS>>, MAX_COMPONENTS>& coeffBlocks,
i32 al);
bool encodeProgressiveAcScan(const std::vector<std::array<i32, BLOCK_PIXELS>>& coeffBlocks,
u32 compIndex, i32 ss, i32 se, i32 al, std::vector<u8>& output);
bool encodeProgressiveAcRefineScan(
const std::vector<std::array<i32, BLOCK_PIXELS>>& coeffBlocks, u32 compIndex, i32 ss,
i32 se, i32 al, std::vector<u8>& output);
void writeBaselineStream(
const std::array<std::vector<std::array<i32, BLOCK_PIXELS>>, MAX_COMPONENTS>& coeffBlocks);
void writeProgressiveStream(
const std::array<std::vector<std::array<i32, BLOCK_PIXELS>>, MAX_COMPONENTS>& coeffBlocks);
};
bool JpegEncoder::initFromImage(const Image& image, i32 quality) {
imageWidth = image.width;
imageHeight = image.height;
componentCount = image.components;
if (imageWidth == 0 || imageHeight == 0) {
return reportError("Cannot encode an image with zero dimensions");
}
if (componentCount == 0 || componentCount > MAX_COMPONENTS) {
return reportError("Unsupported component count " + std::to_string(componentCount));
}
if (imageWidth > 65535 || imageHeight > 65535) {
return reportError("Image dimensions exceed JPEG maximum (65535)");
}
if (image.pixels.size() < static_cast<size_t>(imageWidth) * imageHeight * componentCount) {
return reportError("Pixel buffer too small for the specified dimensions");
}
quantTable = build_quant_table(quality);
quantReciprocal = build_quant_reciprocal_natural(quantTable);
dcHuffTables[0].build(DC_LUMA_COUNTS.data(), DC_LUMA_SYMBOLS.data(),
static_cast<i32>(DC_LUMA_SYMBOLS.size()));
acHuffTables[0].build(AC_LUMA_COUNTS.data(), AC_LUMA_SYMBOLS.data(),
static_cast<i32>(AC_LUMA_SYMBOLS.size()));
if (componentCount > 1) {
dcHuffTables[1].build(DC_CHROMA_COUNTS.data(), DC_CHROMA_SYMBOLS.data(),
static_cast<i32>(DC_CHROMA_SYMBOLS.size()));
acHuffTables[1].build(AC_CHROMA_COUNTS.data(), AC_CHROMA_SYMBOLS.data(),
static_cast<i32>(AC_CHROMA_SYMBOLS.size()));
}
outputBuffer.clear();
outputBuffer.reserve(static_cast<size_t>(imageWidth) * imageHeight * componentCount);
mcuColumnsCount = (imageWidth + BLOCK_SIZE - 1) / BLOCK_SIZE;
mcuRowsCount = (imageHeight + BLOCK_SIZE - 1) / BLOCK_SIZE;
return true;
}
void JpegEncoder::writeAllDhtMarkers() {
write_dht(outputBuffer, 0, 0, DC_LUMA_COUNTS.data(), DC_LUMA_SYMBOLS.data(),
static_cast<i32>(DC_LUMA_SYMBOLS.size()));
write_dht(outputBuffer, 1, 0, AC_LUMA_COUNTS.data(), AC_LUMA_SYMBOLS.data(),
static_cast<i32>(AC_LUMA_SYMBOLS.size()));
if (componentCount > 1) {
write_dht(outputBuffer, 0, 1, DC_CHROMA_COUNTS.data(), DC_CHROMA_SYMBOLS.data(),
static_cast<i32>(DC_CHROMA_SYMBOLS.size()));
write_dht(outputBuffer, 1, 1, AC_CHROMA_COUNTS.data(), AC_CHROMA_SYMBOLS.data(),
static_cast<i32>(AC_CHROMA_SYMBOLS.size()));
}
}
void JpegEncoder::buildComponentBuffers(const Image& image,
std::array<std::vector<u8>, MAX_COMPONENTS>& buffers,
std::array<u32, MAX_COMPONENTS>& strides) const {
u32 const paddedWidth = ((imageWidth + BLOCK_SIZE - 1) / BLOCK_SIZE) * BLOCK_SIZE;
u32 const paddedHeight = ((imageHeight + BLOCK_SIZE - 1) / BLOCK_SIZE) * BLOCK_SIZE;
for (u32 componentIndex = 0; componentIndex < componentCount; componentIndex++) {
strides[componentIndex] = paddedWidth;
buffers[componentIndex].resize(static_cast<size_t>(paddedWidth) * paddedHeight, 0);
}
const u32 compCnt = componentCount;
const u32 imgW = imageWidth;
const u32 imgH = imageHeight;
const u8* srcPixels = image.pixels.data();
parallel_for_blocks(
paddedHeight, ctx,
[&, paddedWidth, compCnt, imgW, imgH, srcPixels](u32 rowBegin, u32 rowEnd) {
for (u32 destY = rowBegin; destY < rowEnd; destY++) {
const u32 sourceY = std::min(destY, imgH - 1);
const u8* srcRow = srcPixels + sourceY * imgW * compCnt;
if (compCnt == 1) {
u8* destRow = buffers[0].data() + destY * paddedWidth;
std::memcpy(destRow, srcRow, imgW);
if (paddedWidth > imgW) {
std::memset(destRow + imgW, destRow[imgW - 1], paddedWidth - imgW);
}
} else if (compCnt == 4) {
u8* d0 = buffers[0].data() + destY * paddedWidth;
u8* d1 = buffers[1].data() + destY * paddedWidth;
u8* d2 = buffers[2].data() + destY * paddedWidth;
u8* d3 = buffers[3].data() + destY * paddedWidth;
for (u32 x = 0; x < imgW; ++x) {
const u32 srcOff = x * 4;
d0[x] = srcRow[srcOff];
d1[x] = srcRow[srcOff + 1];
d2[x] = srcRow[srcOff + 2];
d3[x] = srcRow[srcOff + 3];
}
if (paddedWidth > imgW) {
std::memset(d0 + imgW, d0[imgW - 1], paddedWidth - imgW);
std::memset(d1 + imgW, d1[imgW - 1], paddedWidth - imgW);
std::memset(d2 + imgW, d2[imgW - 1], paddedWidth - imgW);
std::memset(d3 + imgW, d3[imgW - 1], paddedWidth - imgW);
}
} else {
for (u32 ci = 0; ci < compCnt; ++ci) {
u8* destRow = buffers[ci].data() + destY * paddedWidth;
for (u32 x = 0; x < imgW; ++x) {
destRow[x] = srcRow[x * compCnt + ci];
}
if (paddedWidth > imgW) {
std::memset(destRow + imgW, destRow[imgW - 1], paddedWidth - imgW);
}
}
}
}
});
}
bool JpegEncoder::encodeScanDataFromCoeffs(
const std::array<std::vector<std::array<i32, BLOCK_PIXELS>>, MAX_COMPONENTS>& coeffBlocks) {
BitstreamWriter writer;
writer.init(&outputBuffer);
std::array<i32, MAX_COMPONENTS> dcPredictions{};
u32 const totalBlocks = mcuColumnsCount * mcuRowsCount;
for (u32 blockIdx = 0; blockIdx < totalBlocks; blockIdx++) {
for (u32 componentIndex = 0; componentIndex < componentCount; componentIndex++) {
const auto& quantisedCoefficients = coeffBlocks[componentIndex][blockIdx];
u8 const tblIdx = huffTableIndex(componentIndex, componentCount);
i32 const dcDifference = quantisedCoefficients[0] - dcPredictions[componentIndex];
dcPredictions[componentIndex] = quantisedCoefficients[0];
encode_dc_coefficient(writer, dcHuffTables[tblIdx], dcDifference);
encode_ac_coefficients(writer, acHuffTables[tblIdx], quantisedCoefficients);
}
}
writer.flushWithPadding();
return true;
}
void JpegEncoder::buildCoefficientBlocks(
const std::array<std::vector<u8>, MAX_COMPONENTS>& buffers,
const std::array<u32, MAX_COMPONENTS>& strides,
std::array<std::vector<std::array<i32, BLOCK_PIXELS>>, MAX_COMPONENTS>& coeffBlocks) const {
u32 const totalBlocks = mcuColumnsCount * mcuRowsCount;
for (u32 c = 0; c < componentCount; c++) {
coeffBlocks[c].resize(totalBlocks);
}
const u32 mcuCols = mcuColumnsCount;
const u32 compCnt = componentCount;
const auto& qr = quantReciprocal;
parallel_for_blocks(totalBlocks, ctx, [&, mcuCols, compCnt](u32 blockBegin, u32 blockEnd) {
for (u32 blockIndex = blockBegin; blockIndex < blockEnd; blockIndex++) {
const u32 mcuRow = blockIndex / mcuCols;
const u32 mcuCol = blockIndex % mcuCols;
const u32 blockPixelX = mcuCol * BLOCK_SIZE;
const u32 blockPixelY = mcuRow * BLOCK_SIZE;
for (u32 c = 0; c < compCnt; c++) {
const u8* blockDataPointer =
buffers[c].data() + blockPixelY * strides[c] + blockPixelX;
forward_dct_and_quantise(blockDataPointer, strides[c], coeffBlocks[c][blockIndex],
qr);
}
}
});
}
bool JpegEncoder::encodeProgressiveDcScan(
const std::array<std::vector<std::array<i32, BLOCK_PIXELS>>, MAX_COMPONENTS>& coeffBlocks,
i32 al) {
BitstreamWriter writer;
writer.init(&outputBuffer);
std::array<i32, MAX_COMPONENTS> dcPredictions{};
u32 const totalBlocks = mcuColumnsCount * mcuRowsCount;
for (u32 blockIdx = 0; blockIdx < totalBlocks; blockIdx++) {
for (u32 c = 0; c < componentCount; c++) {
i32 const dc = coeffBlocks[c][blockIdx][0];
i32 const dcShifted = dc >> al;
i32 const diff = dcShifted - dcPredictions[c];
dcPredictions[c] = dcShifted;
u8 const tblIdx = huffTableIndex(c, componentCount);
encode_dc_coefficient(writer, dcHuffTables[tblIdx], diff);
}
}
writer.flushWithPadding();
return true;
}
bool JpegEncoder::encodeProgressiveDcRefineScan(
const std::array<std::vector<std::array<i32, BLOCK_PIXELS>>, MAX_COMPONENTS>& coeffBlocks,
i32 al) {
BitstreamWriter writer;
writer.init(&outputBuffer);
u32 const totalBlocks = mcuColumnsCount * mcuRowsCount;
for (u32 blockIdx = 0; blockIdx < totalBlocks; blockIdx++) {
for (u32 c = 0; c < componentCount; c++) {
i32 const dc = coeffBlocks[c][blockIdx][0];
u32 const bit = (static_cast<u32>(dc) >> al) & 1u;
writer.writeBits(bit, 1);
}
}
writer.flushWithPadding();
return true;
}
bool JpegEncoder::encodeProgressiveAcScan(
const std::vector<std::array<i32, BLOCK_PIXELS>>& coeffBlocks, u32 compIndex, i32 ss, i32 se,
i32 al, std::vector<u8>& output) {
u8 const tblIdx = huffTableIndex(compIndex, componentCount);
const auto& acTable = acHuffTables[tblIdx];
BitstreamWriter writer;
writer.init(&output);
auto writeEob = [&]() {
const auto& hcode = acTable.codes[0x00];
writer.writeBits(hcode.code, hcode.length);
};
for (size_t blockIdx = 0; blockIdx < coeffBlocks.size(); blockIdx++) {
const auto& coeffs = coeffBlocks[blockIdx];
i32 lastNz = ss - 1;
for (i32 k = se; k >= ss; --k) {
const i32 absVal = coeffs[k] < 0 ? -coeffs[k] : coeffs[k];
if ((absVal >> al) != 0) {
lastNz = k;
break;
}
}
if (lastNz < ss) {
writeEob();
continue;
}
i32 consecutiveZeros = 0;
for (i32 k = ss; k <= lastNz; ++k) {
const i32 absVal = coeffs[k] < 0 ? -coeffs[k] : coeffs[k];
const i32 coeff = coeffs[k] < 0 ? -(absVal >> al) : (absVal >> al);
if (coeff == 0) {
consecutiveZeros++;
continue;
}
while (consecutiveZeros > 15) {
const auto& zrl = acTable.codes[0xF0];
writer.writeBits(zrl.code, zrl.length);
consecutiveZeros -= 16;
}
const auto [cat, mag] = compute_category_magnitude(coeff);
const u8 rlSymbol = static_cast<u8>((consecutiveZeros << 4) | cat);
const auto& hc = acTable.codes[rlSymbol];
writer.writeBits(hc.code, hc.length);
writer.writeBits(mag, cat);
consecutiveZeros = 0;
}
if (lastNz < se) {
writeEob();
}
}
writer.flushWithPadding();
return true;
}
bool JpegEncoder::encodeProgressiveAcRefineScan(
const std::vector<std::array<i32, BLOCK_PIXELS>>& coeffBlocks, u32 compIndex, i32 ss, i32 se,
i32 al, std::vector<u8>& output) {
u8 const tblIdx = huffTableIndex(compIndex, componentCount);
const auto& acTable = acHuffTables[tblIdx];
BitstreamWriter writer;
writer.init(&output);
u32 corrBits[BLOCK_PIXELS];
u32 corrCount = 0;
auto flushCorrections = [&](BitstreamWriter& w) {
for (u32 i = 0; i < corrCount; ++i) {
w.writeBits(corrBits[i], 1);
}
corrCount = 0;
};
auto writeEobWithCorrections = [&]() {
const auto& hcode = acTable.codes[0x00];
writer.writeBits(hcode.code, hcode.length);
flushCorrections(writer);
};
for (size_t blockIdx = 0; blockIdx < coeffBlocks.size(); blockIdx++) {
const auto& coeffs = coeffBlocks[blockIdx];
i32 lastNewNzPos = -1;
for (i32 k = se; k >= ss; k--) {
const i32 absCoeff = coeffs[k] < 0 ? -coeffs[k] : coeffs[k];
const bool prevNonzero = (absCoeff >> (al + 1)) != 0;
const bool newNonzero = !prevNonzero && ((absCoeff >> al) & 1) != 0;
if (newNonzero) {
lastNewNzPos = k;
break;
}
}
corrCount = 0;
if (lastNewNzPos < 0) {
for (i32 k = ss; k <= se; k++) {
const i32 absCoeff = coeffs[k] < 0 ? -coeffs[k] : coeffs[k];
if ((absCoeff >> (al + 1)) != 0) {
corrBits[corrCount++] = (static_cast<u32>(absCoeff) >> al) & 1u;
}
}
writeEobWithCorrections();
continue;
}
i32 zerosToSkip = 0;
for (i32 k = ss; k <= se; k++) {
const i32 absCoeff = coeffs[k] < 0 ? -coeffs[k] : coeffs[k];
const i32 shifted = absCoeff >> al;
if (shifted == 0) {
zerosToSkip++;
continue;
}
while (zerosToSkip > 15 && k <= lastNewNzPos) {
const auto& zrlCode = acTable.codes[0xF0];
writer.writeBits(zrlCode.code, zrlCode.length);
flushCorrections(writer);
zerosToSkip -= 16;
}
const bool prevNonzero = (shifted >> 1) != 0;
if (prevNonzero) {
corrBits[corrCount++] = static_cast<u32>(shifted) & 1u;
continue;
}
const u8 rlSymbol = static_cast<u8>((zerosToSkip << 4) | 1);
const auto& hc = acTable.codes[rlSymbol];
writer.writeBits(hc.code, hc.length);
const u32 signBit = (coeffs[k] >= 0) ? 1u : 0u;
writer.writeBits(signBit, 1);
flushCorrections(writer);
zerosToSkip = 0;
}
if (zerosToSkip > 0 || corrCount > 0) {
writeEobWithCorrections();
}
}
writer.flushWithPadding();
return true;
}
void JpegEncoder::writeProgressiveStream(
const std::array<std::vector<std::array<i32, BLOCK_PIXELS>>, MAX_COMPONENTS>& coeffBlocks) {
constexpr i32 SA_AL = 1;
write_soi(outputBuffer);
write_dqt(outputBuffer, 0, quantTable);
write_sof(outputBuffer, MARKER_SOF2, imageWidth, imageHeight, componentCount);
writeAllDhtMarkers();
{
u8 compIds[MAX_COMPONENTS], dcIds[MAX_COMPONENTS], acIds[MAX_COMPONENTS];
for (u32 c = 0; c < componentCount; c++) {
compIds[c] = static_cast<u8>(c + 1);
dcIds[c] = huffTableIndex(c, componentCount);
acIds[c] = 0;
}
write_sos(outputBuffer, compIds, dcIds, acIds, componentCount, 0, 0, 0, SA_AL);
encodeProgressiveDcScan(coeffBlocks, SA_AL);
}
for (u32 c = 0; c < componentCount; c++) {
const u8 tblIdx = huffTableIndex(c, componentCount);
const u8 compId = static_cast<u8>(c + 1);
const u8 dcId = 0;
for (const auto& band : DEFAULT_AC_BANDS) {
write_sos(outputBuffer, &compId, &dcId, &tblIdx, 1, band.ss, band.se, 0, SA_AL);
encodeProgressiveAcScan(coeffBlocks[c], c, band.ss, band.se, SA_AL, outputBuffer);
}
}
if constexpr (SA_AL > 0) {
{
u8 compIds[MAX_COMPONENTS], dcIds[MAX_COMPONENTS], acIds[MAX_COMPONENTS];
for (u32 c = 0; c < componentCount; c++) {
compIds[c] = static_cast<u8>(c + 1);
dcIds[c] = huffTableIndex(c, componentCount);
acIds[c] = 0;
}
write_sos(outputBuffer, compIds, dcIds, acIds, componentCount, 0, 0, SA_AL, 0);
encodeProgressiveDcRefineScan(coeffBlocks, 0);
}
for (u32 c = 0; c < componentCount; c++) {
const u8 tblIdx = huffTableIndex(c, componentCount);
const u8 compId = static_cast<u8>(c + 1);
const u8 dcId = 0;
for (const auto& band : DEFAULT_AC_BANDS) {
write_sos(outputBuffer, &compId, &dcId, &tblIdx, 1, band.ss, band.se, SA_AL, 0);
encodeProgressiveAcRefineScan(coeffBlocks[c], c, band.ss, band.se, 0, outputBuffer);
}
}
}
write_eoi(outputBuffer);
}
void JpegEncoder::writeBaselineStream(
const std::array<std::vector<std::array<i32, BLOCK_PIXELS>>, MAX_COMPONENTS>& coeffBlocks) {
write_soi(outputBuffer);
write_dqt(outputBuffer, 0, quantTable);
write_sof(outputBuffer, MARKER_SOF0, imageWidth, imageHeight, componentCount);
writeAllDhtMarkers();
{
u8 compIds[MAX_COMPONENTS], dcIds[MAX_COMPONENTS], acIds[MAX_COMPONENTS];
for (u32 c = 0; c < componentCount; c++) {
compIds[c] = static_cast<u8>(c + 1);
const u8 tblIdx = huffTableIndex(c, componentCount);
dcIds[c] = tblIdx;
acIds[c] = tblIdx;
}
write_sos(outputBuffer, compIds, dcIds, acIds, componentCount, 0, 63, 0, 0);
}
encodeScanDataFromCoeffs(coeffBlocks);
write_eoi(outputBuffer);
}
}
using CoeffBlocks = std::array<std::vector<std::array<i32, BLOCK_PIXELS>>, MAX_COMPONENTS>;
std::vector<u8> encode_raw(const Image& image, i32 quality, std::string* out_error,
bool progressive, JpegContext* ctx, std::vector<u8>* asyncOutput) {
auto enc = std::make_shared<JpegEncoder>();
enc->errorOutput = out_error;
enc->ctx = ctx;
if (!enc->initFromImage(image, quality)) {
return {};
}
auto compBufs = std::make_shared<std::array<std::vector<u8>, MAX_COMPONENTS>>();
auto compStrides = std::make_shared<std::array<u32, MAX_COMPONENTS>>();
enc->buildComponentBuffers(image, *compBufs, *compStrides);
auto coeffs = std::make_shared<CoeffBlocks>();
enc->buildCoefficientBlocks(*compBufs, *compStrides, *coeffs);
bool const canParallel = ctx && ctx->pool && ctx->pool->threadCount() > 1;
bool const useParallelBaseline = canParallel && !progressive && enc->mcuRowsCount >= 2;
bool const useParallelProgressive = canParallel && progressive && enc->componentCount >= 2;
if (useParallelBaseline) {
const u32 nIntervals = enc->mcuRowsCount;
submitSingleTask(ctx, [enc, compBufs]() {
for (u32 c = 0; c < enc->componentCount; c++)
(*compBufs)[c] = {};
write_soi(enc->outputBuffer);
write_dqt(enc->outputBuffer, 0, enc->quantTable);
write_sof(enc->outputBuffer, MARKER_SOF0, enc->imageWidth, enc->imageHeight,
enc->componentCount);
enc->writeAllDhtMarkers();
write_dri(enc->outputBuffer, static_cast<u16>(enc->mcuColumnsCount));
u8 compIds[MAX_COMPONENTS], dcIds[MAX_COMPONENTS], acIds[MAX_COMPONENTS];
for (u32 c = 0; c < enc->componentCount; c++) {
compIds[c] = static_cast<u8>(c + 1);
const u8 tblIdx = huffTableIndex(c, enc->componentCount);
dcIds[c] = tblIdx;
acIds[c] = tblIdx;
}
write_sos(enc->outputBuffer, compIds, dcIds, acIds, enc->componentCount, 0, 63, 0, 0);
});
auto intervalBufs = std::make_shared<std::vector<std::vector<u8>>>(nIntervals);
parallel_for_blocks(nIntervals, ctx, [enc, coeffs, intervalBufs](u32 begin, u32 end) {
for (u32 interval = begin; interval < end; ++interval) {
BitstreamWriter writer;
writer.init(&(*intervalBufs)[interval]);
std::array<i32, MAX_COMPONENTS> dcPreds{};
for (u32 mcuCol = 0; mcuCol < enc->mcuColumnsCount; ++mcuCol) {
u32 const blockIdx = interval * enc->mcuColumnsCount + mcuCol;
for (u32 ci = 0; ci < enc->componentCount; ++ci) {
const auto& qc = (*coeffs)[ci][blockIdx];
u8 const tblIdx = huffTableIndex(ci, enc->componentCount);
i32 const dcDiff = qc[0] - dcPreds[ci];
dcPreds[ci] = qc[0];
encode_dc_coefficient(writer, enc->dcHuffTables[tblIdx], dcDiff);
encode_ac_coefficients(writer, enc->acHuffTables[tblIdx], qc);
}
}
writer.flushWithPadding();
}
});
submitSingleTask(ctx, [enc, intervalBufs, nIntervals]() {
enc->outputBuffer.insert(enc->outputBuffer.end(), (*intervalBufs)[0].begin(),
(*intervalBufs)[0].end());
for (u32 i = 1; i < nIntervals; ++i) {
write_rst(enc->outputBuffer, i - 1);
enc->outputBuffer.insert(enc->outputBuffer.end(), (*intervalBufs)[i].begin(),
(*intervalBufs)[i].end());
}
write_eoi(enc->outputBuffer);
});
} else if (useParallelProgressive) {
constexpr i32 SA_AL = 1;
const u32 numBands = static_cast<u32>(DEFAULT_AC_BANDS.size());
const u32 totalAcScans = enc->componentCount * numBands;
submitSingleTask(ctx, [enc, compBufs, coeffs]() {
for (u32 c = 0; c < enc->componentCount; c++)
(*compBufs)[c] = {};
write_soi(enc->outputBuffer);
write_dqt(enc->outputBuffer, 0, enc->quantTable);
write_sof(enc->outputBuffer, MARKER_SOF2, enc->imageWidth, enc->imageHeight,
enc->componentCount);
enc->writeAllDhtMarkers();
{
u8 compIds[MAX_COMPONENTS], dcIds[MAX_COMPONENTS], acIds[MAX_COMPONENTS];
for (u32 c = 0; c < enc->componentCount; c++) {
compIds[c] = static_cast<u8>(c + 1);
dcIds[c] = huffTableIndex(c, enc->componentCount);
acIds[c] = 0;
}
write_sos(enc->outputBuffer, compIds, dcIds, acIds, enc->componentCount, 0, 0, 0,
SA_AL);
enc->encodeProgressiveDcScan(*coeffs, SA_AL);
}
});
auto acFirstBufs = std::make_shared<std::vector<std::vector<u8>>>(totalAcScans);
parallel_for_tasks(totalAcScans, ctx, [=](u32 scanIdx) {
constexpr i32 al = 1; u32 const c = scanIdx / numBands;
u32 const b = scanIdx % numBands;
const auto& band = DEFAULT_AC_BANDS[b];
enc->encodeProgressiveAcScan((*coeffs)[c], c, band.ss, band.se, al,
(*acFirstBufs)[scanIdx]);
});
submitSingleTask(ctx, [=]() {
constexpr i32 SA_AL = 1;
for (u32 c = 0; c < enc->componentCount; ++c) {
u8 const tblIdx = huffTableIndex(c, enc->componentCount);
for (u32 b = 0; b < numBands; ++b) {
const auto& band = DEFAULT_AC_BANDS[b];
const u8 compId = static_cast<u8>(c + 1);
const u8 dcId = 0;
write_sos(enc->outputBuffer, &compId, &dcId, &tblIdx, 1, band.ss, band.se, 0,
SA_AL);
enc->outputBuffer.insert(enc->outputBuffer.end(),
(*acFirstBufs)[c * numBands + b].begin(),
(*acFirstBufs)[c * numBands + b].end());
}
}
{
u8 compIds[MAX_COMPONENTS], dcIds[MAX_COMPONENTS], acIds[MAX_COMPONENTS];
for (u32 c = 0; c < enc->componentCount; c++) {
compIds[c] = static_cast<u8>(c + 1);
dcIds[c] = huffTableIndex(c, enc->componentCount);
acIds[c] = 0;
}
write_sos(enc->outputBuffer, compIds, dcIds, acIds, enc->componentCount, 0, 0,
SA_AL, 0);
enc->encodeProgressiveDcRefineScan(*coeffs, 0);
}
});
auto acRefineBufs = std::make_shared<std::vector<std::vector<u8>>>(totalAcScans);
parallel_for_tasks(totalAcScans, ctx, [=](u32 scanIdx) {
u32 const c = scanIdx / numBands;
u32 const b = scanIdx % numBands;
const auto& band = DEFAULT_AC_BANDS[b];
enc->encodeProgressiveAcRefineScan((*coeffs)[c], c, band.ss, band.se, 0,
(*acRefineBufs)[scanIdx]);
});
submitSingleTask(ctx, [=]() {
constexpr i32 SA_AL = 1;
for (u32 c = 0; c < enc->componentCount; ++c) {
u8 const tblIdx = huffTableIndex(c, enc->componentCount);
for (u32 b = 0; b < numBands; ++b) {
const auto& band = DEFAULT_AC_BANDS[b];
const u8 compId = static_cast<u8>(c + 1);
const u8 dcId = 0;
write_sos(enc->outputBuffer, &compId, &dcId, &tblIdx, 1, band.ss, band.se,
SA_AL, 0);
enc->outputBuffer.insert(enc->outputBuffer.end(),
(*acRefineBufs)[c * numBands + b].begin(),
(*acRefineBufs)[c * numBands + b].end());
}
}
write_eoi(enc->outputBuffer);
});
} else {
submitSingleTask(ctx, [enc, compBufs, compStrides, coeffs, progressive]() {
for (u32 c = 0; c < enc->componentCount; c++) {
(*compBufs)[c] = {};
}
if (progressive) {
enc->writeProgressiveStream(*coeffs);
} else {
enc->writeBaselineStream(*coeffs);
}
});
}
if (asyncOutput && ctx && ctx->sem) {
submitSingleTask(ctx,
[enc, asyncOutput]() { *asyncOutput = std::move(enc->outputBuffer); });
return {};
}
return std::move(enc->outputBuffer);
}
}