载入中...
搜索中...
未找到
CanonicalImageCook.cpp
浏览该文件的文档.
2#include "asset/SourceTga.h"
3#include "asset/SourceTiff.h"
4
5#include "common/Value.h"
6
7#include <algorithm>
8#include <cmath>
9#include <limits>
10
11#include <zlib.h>
12
13namespace eve::asset {
14namespace {
15
16std::uint32_t big32(std::span<const std::uint8_t> bytes, std::size_t offset) {
17 return (std::uint32_t(bytes[offset]) << 24) | (std::uint32_t(bytes[offset + 1]) << 16) |
18 (std::uint32_t(bytes[offset + 2]) << 8) | bytes[offset + 3];
19}
20
21void put32(std::vector<std::uint8_t>& bytes, std::uint32_t value) {
22 for (unsigned shift = 0; shift != 32; shift += 8)
23 bytes.push_back(static_cast<std::uint8_t>(value >> shift));
24}
25
26const Value* field(const Value::Object& object, std::string_view name) {
27 const auto found = object.find(std::string(name));
28 return found == object.end() ? nullptr : &found->second;
29}
30
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;
38}
39
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()))
46 return Result<std::vector<std::uint8_t>>::failure(Diagnostic::error(
47 DiagnosticCode::ParseError, "PNG signature or header is invalid", {}, {}, "asset.cook.image"));
48 std::size_t cursor = 8;
49 bool sawHeader = false;
50 bool sawEnd = false;
51 std::uint32_t width = 0, height = 0;
52 std::uint8_t channels = 0;
53 std::vector<std::uint8_t> compressed;
54 while (cursor < bytes.size()) {
55 if (bytes.size() - cursor < 12)
56 return Result<std::vector<std::uint8_t>>::failure(Diagnostic::error(
57 DiagnosticCode::ParseError, "PNG chunk header is truncated", {}, {}, "asset.cook.image"));
58 const std::uint32_t size = big32(bytes, cursor);
59 if (size > bytes.size() - cursor - 12)
60 return Result<std::vector<std::uint8_t>>::failure(Diagnostic::error(
61 DiagnosticCode::ParseError, "PNG chunk payload is truncated", {}, {}, "asset.cook.image"));
62 const auto type = bytes.subspan(cursor + 4, 4);
63 const auto payload = bytes.subspan(cursor + 8, size);
64 const std::uint32_t storedCrc = big32(bytes, cursor + 8 + size);
65 uLong crc = crc32(0, Z_NULL, 0);
66 crc = crc32(crc, type.data(), 4);
67 if (!payload.empty()) crc = crc32(crc, payload.data(), static_cast<uInt>(payload.size()));
68 if (static_cast<std::uint32_t>(crc) != storedCrc)
69 return Result<std::vector<std::uint8_t>>::failure(Diagnostic::error(
70 DiagnosticCode::HashMismatch, "PNG chunk CRC does not match", {}, {}, "asset.cook.image"));
71 const std::string_view name(reinterpret_cast<const char*>(type.data()), 4);
72 if (!sawHeader) {
73 if (name != "IHDR" || size != 13)
74 return Result<std::vector<std::uint8_t>>::failure(Diagnostic::error(
75 DiagnosticCode::ParseError, "PNG IHDR must be first", {}, {}, "asset.cook.image"));
76 width = big32(payload, 0);
77 height = big32(payload, 4);
78 const std::uint8_t bitDepth = payload[8];
79 const std::uint8_t colorType = payload[9];
80 if (width == 0 || height == 0 || width != expectedWidth || height != expectedHeight ||
81 bitDepth != 8 || (colorType != 2 && colorType != 6) || payload[10] != 0 ||
82 payload[11] != 0 || payload[12] != 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"));
87 channels = colorType == 6 ? 4 : 3;
88 sawHeader = true;
89 } else if (name == "IHDR") {
90 return Result<std::vector<std::uint8_t>>::failure(
91 Diagnostic::error(DiagnosticCode::Conflict, "PNG contains duplicate IHDR", {}, {}, "asset.cook.image"));
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(
96 Diagnostic::error(DiagnosticCode::InvalidArgument, "PNG compressed data exceeds Cook budget", {},
97 {}, "asset.cook.image"));
98 compressed.insert(compressed.end(), payload.begin(), payload.end());
99 } else if (name == "IEND") {
100 if (size != 0 || cursor + 12 != bytes.size())
101 return Result<std::vector<std::uint8_t>>::failure(Diagnostic::error(
102 DiagnosticCode::ParseError, "PNG IEND or trailing data is invalid", {}, {}, "asset.cook.image"));
103 sawEnd = true;
104 } else if ((type[0] & 0x20u) == 0) {
105 return Result<std::vector<std::uint8_t>>::failure(
106 Diagnostic::error(DiagnosticCode::Unsupported, "PNG contains an unsupported critical chunk",
107 std::string(name), {}, "asset.cook.image"));
108 }
109 cursor += std::size_t(size) + 12;
110 if (sawEnd) break;
111 }
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())
119 return Result<std::vector<std::uint8_t>>::failure(Diagnostic::error(
120 DiagnosticCode::InvalidArgument, "PNG decoded dimensions exceed Cook budget", {}, {}, "asset.cook.image"));
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(
126 Diagnostic::error(DiagnosticCode::ParseError, "PNG zlib stream is invalid or has unexpected size", {}, {},
127 "asset.cook.image"));
128 std::vector<std::uint8_t> raw(static_cast<std::size_t>(rowBytes) * height);
129 for (std::uint32_t row = 0; row < height; ++row) {
130 const std::size_t sourceBase = std::size_t(row) * (std::size_t(rowBytes) + 1);
131 const std::uint8_t filter = filtered[sourceBase];
132 if (filter > 4)
133 return Result<std::vector<std::uint8_t>>::failure(Diagnostic::error(
134 DiagnosticCode::ParseError, "PNG row uses an invalid filter", {}, {}, "asset.cook.image"));
135 const std::size_t destinationBase = std::size_t(row) * std::size_t(rowBytes);
136 for (std::size_t column = 0; column < rowBytes; ++column) {
137 const std::uint8_t encoded = filtered[sourceBase + 1 + column];
138 const std::uint8_t left = column >= channels ? raw[destinationBase + column - channels] : 0;
139 const std::uint8_t above = row ? raw[destinationBase + column - rowBytes] : 0;
140 const std::uint8_t corner = row && column >= channels
141 ? raw[destinationBase + column - rowBytes - channels]
142 : 0;
143 std::uint8_t predictor = 0;
144 if (filter == 1) predictor = left;
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);
149 }
150 }
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,
155 raw.begin() + pixel * channels + channels);
156 if (channels == 3) rgba.push_back(255);
157 }
158 return Result<std::vector<std::uint8_t>>::success(std::move(rgba));
159}
160
161} // namespace
162
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()),
168 definition.size()));
169 if (!parsed) return Result<CookedCanonicalImage>::failure(parsed.status());
170 auto* object = parsed.value().getIf<Value::Object>();
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;
177 const auto* colorObject = color ? color->getIf<Value::Object>() : 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() ||
181 widthValue->asInt() <= 0 || !heightValue || !heightValue->isInt64() || heightValue->asInt() <= 0 || !encoding ||
182 !encoding->isString() || !transfer || !transfer->isString() ||
183 (transfer->asString() != "srgb" && transfer->asString() != "linear"))
185 DiagnosticCode::ParseError, "eve.image runtime Cook definition is malformed", {}, {}, "asset.cook.image"));
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)))
191 DiagnosticCode::ParseError, "image mip count requires schema 3 and a positive bounded integer", {}, {},
192 "asset.cook.image"));
193 const uint32_t mipCount = explicitSchema ? uint32_t(mipValue->asInt()) : 1;
194 const bool packedMips = encoding->asString() == "rgba8-mips";
195 if (encoding->asString() != "png" && encoding->asString() != "tiff" && encoding->asString() != "tga" &&
196 !(explicitSchema && packedMips))
198 DiagnosticCode::Unsupported, "runtime RGBA8 Cook supports PNG and admitted TIFF/TGA source encoding", {},
199 {}, "asset.cook.image"));
200 if (widthValue->asInt() > UINT32_MAX || heightValue->asInt() > UINT32_MAX)
202 DiagnosticCode::ParseError, "image dimensions exceed uint32", {}, {}, "asset.cook.image"));
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;
208 for (;;) {
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;
217 }
218 ++fullCount;
219 if (mipWidth == 1 && mipHeight == 1) break;
220 mipWidth = std::max(mipWidth / 2, 1u);
221 mipHeight = std::max(mipHeight / 2, 1u);
222 }
223 if ((mipCount != 1 && mipCount != fullCount) || (!packedMips && mipCount != 1))
225 Diagnostic::error(DiagnosticCode::InvalidArgument, "image requires a base level or full halving chain", {},
226 {}, "asset.cook.image"));
227 auto rgba = [&]() -> Result<std::vector<std::uint8_t>> {
228 if (packedMips) {
229 if (encodedSource.size() != expectedBytes)
230 return Result<std::vector<std::uint8_t>>::failure(
231 Diagnostic::error(DiagnosticCode::ParseError, "packed mip byte count differs from definition", {},
232 {}, "asset.cook.image"));
233 return Result<std::vector<std::uint8_t>>::success({encodedSource.begin(), encodedSource.end()});
234 }
235 if (encoding->asString() == "png") return decodePng(encodedSource, width, height, maximumDecodedBytes);
236 if (encoding->asString() == "tga") {
237 auto tga = detail::decodeTgaRgba8(encodedSource, maximumDecodedBytes);
238 if (!tga) return Result<std::vector<std::uint8_t>>::failure(tga.status());
239 if (tga.value().width != width || tga.value().height != height)
241 DiagnosticCode::ParseError, "TGA dimensions differ from definition", {}, {}, "asset.cook.image"));
242 return Result<std::vector<std::uint8_t>>::success(std::move(tga.value().rgba));
243 }
244 auto tiff = detail::decodeTiffRgba8(encodedSource, maximumDecodedBytes);
245 if (!tiff) return Result<std::vector<std::uint8_t>>::failure(tiff.status());
246 if (tiff.value().width != width || tiff.value().height != height)
248 DiagnosticCode::ParseError, "TIFF dimensions differ from definition", {}, {}, "asset.cook.image"));
249 return Result<std::vector<std::uint8_t>>::success(std::move(tiff.value().rgba));
250 }();
251 if (!rgba) return Result<CookedCanonicalImage>::failure(rgba.status());
252 const bool sourceSrgb = transfer->asString() == "srgb";
253 if (sourceSrgb) {
254 for (std::size_t index = 0; index < rgba.value().size(); index += 4) {
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
258 ? encoded / 12.92
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));
262 }
263 }
264 }
265 std::vector<std::uint8_t> bulk = {'E', 'V', 'I', 'M', 'G', 0, 1, 0};
266 if (explicitSchema) bulk[6] = 2;
267 put32(bulk, width);
268 put32(bulk, height);
269 put32(bulk, 0);
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();
281 if (!runtimeDefinition) return Result<CookedCanonicalImage>::failure(runtimeDefinition.status());
283 {{runtimeDefinition.value().begin(), runtimeDefinition.value().end()}, std::move(bulk)});
284}
285
286} // namespace eve::asset
double value
Value::Object payload
std::map< std::string, std::vector< Key >, std::less<> > channels
Safe source-image conversion to runtime EVIMG payloads.
int column
std::uint32_t crc
HexVec3 left
std::string encoding
std::uint32_t height
std::uint32_t width
size_t offset
std::uint64_t bytes
std::string name
float begin
uint8_t * pixels
bool found
std::string filter
std::size_t cursor
float size
Definition TreeMesh.cpp:156
uint32_t index
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.
Definition Diagnostic.h:125
Move-only operation result carrying either a value or Status.
Definition Result.h:155
static Result success(T value)
Construct a successful result owning value.
Definition Result.h:164
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
The canonical owning dynamic value used by data-facing protocols.
Definition Value.h:31
const std::string & asString() const
Return the string payload; the caller must have checked the kind.
Definition Value.cpp:87
bool isInt64() const noexcept
Return true when this value is an Int64.
Definition Value.h:91
static Result< Value > fromJson(std::string_view json)
Parse one strict JSON value into an owning Value.
Definition Value.cpp:57
std::map< std::string, Value > Object
Definition Value.h:34
bool isString() const noexcept
Return true when this value is a string.
Definition Value.h:95
std::int64_t asInt() const
Return the Int64 payload; the caller must have checked the kind.
Definition Value.cpp:69
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.
Definition SourceTga.cpp:7
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.
Definition zip_writer.h:56
std::variant< std::monostate, std::int64_t, double, std::string, bool > Value
Definition Database.h:26
const EditorValue * field(const EditorValue &value, const char *name)
WidgetDesc row(std::vector< WidgetDesc > children, std::string id)
Horizontal elastic layout row.
Definition Widget.cpp:679
glm::vec4 color