16std::uint32_t big32(std::span<const std::uint8_t>
bytes, std::size_t
offset) {
22 for (
unsigned shift = 0; shift != 32; shift += 8)
23 bytes.push_back(
static_cast<std::uint8_t
>(
value >> shift));
27 const auto found =
object.find(std::string(
name));
28 return found ==
object.end() ? nullptr : &
found->second;
31std::uint8_t paeth(std::uint8_t
left, std::uint8_t above, std::uint8_t upperLeft) {
32 const int prediction = int(
left) + int(above) - int(upperLeft);
33 const int leftDistance = std::abs(prediction -
int(
left));
34 const int aboveDistance = std::abs(prediction -
int(above));
35 const int cornerDistance = std::abs(prediction -
int(upperLeft));
36 if (leftDistance <= aboveDistance && leftDistance <= cornerDistance)
return left;
37 return aboveDistance <= cornerDistance ? above : upperLeft;
40Result<std::vector<std::uint8_t>> decodePng(std::span<const std::uint8_t>
bytes,
41 std::uint32_t expectedWidth,
42 std::uint32_t expectedHeight,
43 std::uint64_t maximumDecodedBytes) {
44 static constexpr std::uint8_t signature[] = {0x89,
'P',
'N',
'G', 0x0d, 0x0a, 0x1a, 0x0a};
45 if (
bytes.size() < 33 || !std::equal(std::begin(signature), std::end(signature),
bytes.begin()))
49 bool sawHeader =
false;
53 std::vector<std::uint8_t> compressed;
65 uLong
crc = crc32(0, Z_NULL, 0);
68 if (
static_cast<std::uint32_t
>(
crc) != storedCrc)
71 const std::string_view
name(
reinterpret_cast<const char*
>(
type.data()), 4);
78 const std::uint8_t bitDepth =
payload[8];
79 const std::uint8_t colorType =
payload[9];
81 bitDepth != 8 || (colorType != 2 && colorType != 6) ||
payload[10] != 0 ||
83 return Result<std::vector<std::uint8_t>>::failure(
85 "PNG Cook supports non-interlaced 8-bit RGB/RGBA matching the image definition",
86 {}, {},
"asset.cook.image"));
89 }
else if (
name ==
"IHDR") {
90 return Result<std::vector<std::uint8_t>>::failure(
92 }
else if (
name ==
"IDAT") {
93 if (compressed.size() > maximumDecodedBytes ||
94 payload.size() > maximumDecodedBytes - compressed.size())
95 return Result<std::vector<std::uint8_t>>::failure(
97 {},
"asset.cook.image"));
98 compressed.insert(compressed.end(),
payload.begin(),
payload.end());
99 }
else if (
name ==
"IEND") {
104 }
else if ((
type[0] & 0x20u) == 0) {
105 return Result<std::vector<std::uint8_t>>::failure(
107 std::string(
name), {},
"asset.cook.image"));
112 const std::uint64_t rowBytes = std::uint64_t(
width) *
channels;
113 const std::uint64_t filteredSize = std::uint64_t(
height) * (rowBytes + 1);
114 const std::uint64_t rgbaSize = std::uint64_t(
width) *
height * 4;
115 if (!sawHeader || !sawEnd || compressed.empty() || compressed.size() > (std::numeric_limits<uLong>::max)() ||
116 filteredSize > maximumDecodedBytes ||
117 rgbaSize > maximumDecodedBytes || rgbaSize + 24 > maximumDecodedBytes ||
118 filteredSize > std::numeric_limits<uLongf>::max())
121 std::vector<std::uint8_t> filtered(
static_cast<std::size_t
>(filteredSize));
122 uLongf outputSize =
static_cast<uLongf
>(filtered.size());
123 if (uncompress(filtered.data(), &outputSize, compressed.data(),
static_cast<uLong
>(compressed.size())) != Z_OK ||
124 outputSize != filtered.size())
125 return Result<std::vector<std::uint8_t>>::failure(
127 "asset.cook.image"));
128 std::vector<std::uint8_t> raw(
static_cast<std::size_t
>(rowBytes) *
height);
130 const std::size_t sourceBase = std::size_t(row) * (std::size_t(rowBytes) + 1);
131 const std::uint8_t
filter = filtered[sourceBase];
135 const std::size_t destinationBase = std::size_t(row) * std::size_t(rowBytes);
137 const std::uint8_t encoded = filtered[sourceBase + 1 +
column];
139 const std::uint8_t above =
row ? raw[destinationBase +
column - rowBytes] : 0;
143 std::uint8_t predictor = 0;
145 else if (
filter == 2) predictor = above;
146 else if (
filter == 3) predictor =
static_cast<std::uint8_t
>((int(
left) + int(above)) / 2);
147 else if (
filter == 4) predictor = paeth(
left, above, corner);
148 raw[destinationBase +
column] =
static_cast<std::uint8_t
>(encoded + predictor);
151 std::vector<std::uint8_t> rgba;
152 rgba.reserve(
static_cast<std::size_t
>(rgbaSize));
153 for (std::size_t pixel = 0; pixel < std::size_t(
width) *
height; ++pixel) {
154 rgba.insert(rgba.end(), raw.begin() + pixel *
channels,
156 if (
channels == 3) rgba.push_back(255);
158 return Result<std::vector<std::uint8_t>>::success(std::move(rgba));
164 std::span<const std::uint8_t> definition,
165 std::span<const std::uint8_t> encodedSource,
166 std::uint64_t maximumDecodedBytes) {
167 auto parsed =
Value::fromJson(std::string_view(
reinterpret_cast<const char*
>(definition.data()),
171 const Value* schema =
object ? field(*
object,
"schema") :
nullptr;
172 const Value* version =
object ? field(*
object,
"schemaVersion") :
nullptr;
173 const Value* widthValue =
object ? field(*
object,
"width") :
nullptr;
174 const Value* heightValue =
object ? field(*
object,
"height") :
nullptr;
175 const Value*
encoding =
object ? field(*
object,
"encoding") :
nullptr;
176 const Value*
color =
object ? field(*
object,
"color") :
nullptr;
178 const Value* transfer = colorObject ? field(*colorObject,
"transfer") :
nullptr;
179 if (!schema || !schema->isString() || schema->asString() !=
"eve.image" || !version || !version->
isInt64() ||
180 (version->
asInt() != 2 && version->
asInt() != 3) || !widthValue || !widthValue->
isInt64() ||
186 const bool explicitSchema = version->
asInt() == 3;
187 const Value* mipValue = field(*
object,
"mipCount");
188 if ((!explicitSchema && mipValue) ||
189 (explicitSchema && (!mipValue || !mipValue->
isInt64() || mipValue->
asInt() < 1 || mipValue->
asInt() > 32)))
192 "asset.cook.image"));
193 const uint32_t mipCount = explicitSchema ? uint32_t(mipValue->
asInt()) : 1;
194 const bool packedMips =
encoding->asString() ==
"rgba8-mips";
196 !(explicitSchema && packedMips))
199 {},
"asset.cook.image"));
200 if (widthValue->
asInt() > UINT32_MAX || heightValue->
asInt() > UINT32_MAX)
203 const auto width =
static_cast<std::uint32_t
>(widthValue->
asInt());
204 const auto height =
static_cast<std::uint32_t
>(heightValue->
asInt());
205 const uint64_t headerBytes = explicitSchema ? 28 : 24;
206 uint64_t expectedBytes = 0;
207 uint32_t fullCount = 0, mipWidth =
width, mipHeight =
height;
209 const uint64_t
pixels = uint64_t(mipWidth) * mipHeight;
210 if (fullCount < mipCount) {
211 if (maximumDecodedBytes < headerBytes || expectedBytes > maximumDecodedBytes - headerBytes ||
212 pixels > (maximumDecodedBytes - headerBytes - expectedBytes) / 4)
214 "image mip chain exceeds Cook budget",
215 {}, {},
"asset.cook.image"));
216 expectedBytes +=
pixels * 4;
219 if (mipWidth == 1 && mipHeight == 1)
break;
220 mipWidth = std::max(mipWidth / 2, 1u);
221 mipHeight = std::max(mipHeight / 2, 1u);
223 if ((mipCount != 1 && mipCount != fullCount) || (!packedMips && mipCount != 1))
226 {},
"asset.cook.image"));
227 auto rgba = [&]() ->
Result<std::vector<std::uint8_t>> {
229 if (encodedSource.size() != expectedBytes)
230 return Result<std::vector<std::uint8_t>>::failure(
232 {},
"asset.cook.image"));
235 if (
encoding->asString() ==
"png")
return decodePng(encodedSource,
width,
height, maximumDecodedBytes);
236 if (
encoding->asString() ==
"tga") {
239 if (tga.value().width !=
width || tga.value().height !=
height)
246 if (tiff.value().width !=
width || tiff.value().height !=
height)
252 const bool sourceSrgb = transfer->
asString() ==
"srgb";
255 for (std::size_t channel = 0; channel < 3; ++channel) {
256 const double encoded = rgba.value()[
index + channel] / 255.0;
257 const double linear = encoded <= 0.04045
259 : std::pow((encoded + 0.055) / 1.055, 2.4);
260 rgba.value()[
index + channel] =
static_cast<std::uint8_t
>(
261 std::clamp(std::lround(linear * 255.0), 0l, 255l));
265 std::vector<std::uint8_t> bulk = {
'E',
'V',
'I',
'M',
'G', 0, 1, 0};
266 if (explicitSchema) bulk[6] = 2;
270 put32(bulk, explicitSchema ? mipCount : 0);
271 if (explicitSchema) put32(bulk, 0);
272 bulk.insert(bulk.end(), rgba.value().begin(), rgba.value().end());
273 (*object)[
"sourceEncoding"] = *
encoding;
274 (*object)[
"encoding"] =
Value(
"rgba8");
275 (*object)[
"blob"] =
Value(
"chunk:1");
276 auto runtimeColor = *colorObject;
277 runtimeColor[
"sourceTransfer"] =
Value(transfer->
asString());
278 runtimeColor[
"transfer"] =
Value(
"linear");
279 (*object)[
"color"] =
Value(std::move(runtimeColor));
280 auto runtimeDefinition = parsed.value().toJson();
283 {{runtimeDefinition.value().
begin(), runtimeDefinition.value().end()}, std::move(bulk)});
std::map< std::string, std::vector< Key >, std::less<> > channels
Safe source-image conversion to runtime EVIMG payloads.
Owning, renderer-independent dynamic values.
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.
Move-only operation result carrying either a value or Status.
static Result success(T value)
Construct a successful result owning value.
static Result failure(Status status)
Construct a failed result from a structured status.
The canonical owning dynamic value used by data-facing protocols.
const std::string & asString() const
Return the string payload; the caller must have checked the kind.
bool isInt64() const noexcept
Return true when this value is an Int64.
static Result< Value > fromJson(std::string_view json)
Parse one strict JSON value into an owning Value.
std::map< std::string, Value > Object
bool isString() const noexcept
Return true when this value is a string.
std::int64_t asInt() const
Return the Int64 payload; the caller must have checked the kind.
Result< TgaPixels > decodeTgaRgba8(std::span< const std::uint8_t > b, std::uint64_t budget)
Decode true-color 24/32-bit raw or RLE TGA with bounded allocation.
Result< TiffPixels > decodeTiffRgba8(std::span< const std::uint8_t > bytes, std::uint64_t maximumDecodedBytes)
Decode bounded strip-based unsigned RGB TIFF (8/16 bits, raw/LZW/Deflate).
Result< CookedCanonicalImage > cookCanonicalImageRgba8(std::span< const std::uint8_t > definition, std::span< const std::uint8_t > encodedSource, std::uint64_t maximumDecodedBytes)
Decode image/2 base images or image/3 explicit linear RGBA8 mip chains into bounded EVIMG.
void put32(std::vector< char > &b, uint32_t v)
Put 32.
std::variant< std::monostate, std::int64_t, double, std::string, bool > Value
const EditorValue * field(const EditorValue &value, const char *name)
WidgetDesc row(std::vector< WidgetDesc > children, std::string id)
Horizontal elastic layout row.