Haio 0.2.X
Homebrew Asset IO/CDN
Loading...
Searching...
No Matches
convert.cpp
Go to the documentation of this file.
1#include <haio.hpp>
2
3#include <optional>
4
5namespace Haio {
6
7/**
8 * a conversion described at runtime, which is what a url query builds. it decodes
9 * once, applies whatever transforms were asked for, and encodes at the end.
10 *
11 * @todo everything travels as rgba8888 here, so a pipeline with no transforms still
12 * decompresses a gpu payload and recompresses it. the typed pipe in haio_pipe.hpp
13 * keeps the colour; only this runtime path flattens it.
14 */
15Result<Blob> runPipeline(Blob input, const Pipeline& pipeline,
16 std::optional<std::chrono::steady_clock::time_point> deadline) {
17 bool hasImage = false;
19 Format outputFormat = Format::RAW;
20 std::optional<Color> outputColor;
21 std::optional<Error> failure;
22
23 /**
24 * the picture as indices, kept beside the full colour one from the moment a
25 * palette is applied.
26 *
27 * quantising and then expanding back loses nothing about the picture and
28 * everything about how it is stored, and a container that keeps a palette wants
29 * the second thing. the two travel together rather than the pipeline switching
30 * over, because every transform downstream is written against full colour and
31 * only some of them mean anything to a page of indices.
32 */
33 std::optional<Image<Color::PALETTE>> indexed;
34
35 auto ensureImage = [&] {
36 if (hasImage || failure) return;
37 auto decoded = Decode(input);
38 if (!decoded) {
39 failure = decoded.error();
40 return;
41 }
42 image = *std::move(decoded);
43 hasImage = true;
44 };
45
46 for (const auto& token : pipeline.tokens()) {
47 if (deadline && std::chrono::steady_clock::now() >= *deadline) {
48 return std::unexpected(Error{ErrorCode::Timeout, "the conversion took too long and was stopped"});
49 }
50
51 switch (token.kind) {
53 break;
56 ensureImage();
57 break;
58 case TokenKind::Crop:
59 ensureImage();
60 if (failure) break;
61 image = cropImage(image, token.rect);
62 // the same rectangle of the same picture: indices are addressable, so
63 // there is nothing here that full colour has and they do not
64 if (indexed) indexed = cropImage(*indexed, token.rect);
65 break;
66 case TokenKind::Resize: {
67 ensureImage();
68 if (failure) break;
69
70 // a share is worked out here and not at parsing, because this is the
71 // first moment anybody knows what it is a share of
72 auto wanted = token.size;
73 if (token.percent != 0) {
74 wanted = Size{std::max(1, image.width * token.percent / 100),
75 std::max(1, image.height * token.percent / 100)};
76 }
77 image = resizeImage(image, wanted);
78 if (indexed) indexed = resizeImage(*indexed, wanted);
79 break;
80 }
82 ensureImage();
83 if (failure) break;
84 image = roundImageCorners(image, token.radius);
85 // rounding a corner away is done by clearing an alpha, and a palette
86 // has nowhere to keep one: from here the picture is full colour only
87 indexed.reset();
88 break;
89
90 /**
91 * quantise, keep the indices, and expand back to full colour.
92 *
93 * the picture afterwards holds only the palette's colours, which is the
94 * point, and it stays rgba8888 so that everything downstream keeps
95 * working. the indices are kept as well rather than thrown away, because
96 * a container that stores a palette is the one place where they are not
97 * simply a longer way of saying the same picture.
98 */
99 case TokenKind::Palette: {
100 ensureImage();
101 if (failure) break;
102
103 auto colours = paletteNamed(token.palette, 0);
104 if (!colours) {
105 failure = colours.error();
106 break;
107 }
108 // cutting the palette down comes first, so the dither only ever sees
109 // the colours that survived and spreads error among those
110 if (token.limit != 0) {
111 auto fewer = limitPalette(image, *std::move(colours), token.limit, token.limitHow);
112 if (!fewer) {
113 failure = fewer.error();
114 break;
115 }
116 colours = *std::move(fewer);
117 }
118
119 auto fitted = toPalette(image, *std::move(colours), token.dither);
120 if (!fitted) {
121 failure = fitted.error();
122 break;
123 }
125 if (!expanded) {
126 failure = expanded.error();
127 break;
128 }
129 image = *std::move(expanded);
130 indexed = *std::move(fitted);
131 break;
132 }
134 outputFormat = token.format;
135 outputColor = token.color;
136 break;
137 }
138 if (failure) return std::unexpected(*failure);
139 }
140
141 /**
142 * no container asked for, so the answer is the pixels themselves.
143 *
144 * a colour without a container is still worth doing: "-pix_fmt rgb565 raw:out.bin"
145 * is a texture in the layout it will be uploaded in, and it is the same thing
146 * ffmpeg writes when it is given a pixel format and no muxer.
147 */
148 if (outputFormat == Format::RAW) {
149 if (!hasImage && !outputColor) return input;
150
151 ensureImage();
152 if (failure) return std::unexpected(*failure);
153
154 auto pixels = outputColor ? Convert(image, *outputColor)
155 : Result<std::vector<uint8_t>>{std::move(image.data)};
156 if (!pixels) return std::unexpected(pixels.error());
157
158 return Blob{Format::RAW, outputColor.value_or(Color::RGBA8888),
159 std::string(contentTypeFor(Format::RAW)), input.path, *std::move(pixels)};
160 }
161
162 ensureImage();
163 if (failure) return std::unexpected(*failure);
164
165 /**
166 * a picture that was fitted to a palette goes out as indices wherever the
167 * container keeps them, which is the whole reason the indices were kept. asking
168 * for any other colour is honoured: somebody who says "-palete cga -pix_fmt
169 * bgr888" wants sixteen colours stored the long way, and that is a real thing to
170 * want when the reader on the other end has no palette support.
171 */
172 const bool asIndices = indexed && encodable(outputFormat, Color::PALETTE)
173 && (!outputColor || *outputColor == Color::PALETTE);
174 if (outputColor == Color::PALETTE && !indexed) {
175 return std::unexpected(Error{ErrorCode::InvalidInput,
176 "a palette is colours a picture was fitted to, and this one was not; "
177 "name some with -palete"});
178 }
179
180 auto encoded = asIndices ? Encode(*std::move(indexed), outputFormat)
181 : Encode(std::move(image), outputFormat, outputColor);
182 if (encoded) encoded->path = input.path;
183 return encoded;
184}
185
186}
const std::vector< Token > & tokens() const
Definition pipeline.cpp:16
Result< Image< Color::RGBA8888 > > Convert< Color::PALETTE, Color::RGBA8888 >(Image< Color::PALETTE > src)
Definition rgba8888.cpp:12
Definition haio.hpp:11
Result< Blob > Encode(Image< Color::RGBA8888 > image, Format to, std::optional< Color > as=std::nullopt)
Result< std::vector< uint32_t > > limitPalette(const Image< Color::RGBA8888 > &image, std::vector< uint32_t > entries, size_t most, Limit how)
Definition quantise.cpp:70
Result< Image< Color::PALETTE > > toPalette(const Image< Color::RGBA8888 > &image, std::vector< uint32_t > entries, Dither how)
Definition quantise.cpp:146
Result< std::vector< uint32_t > > paletteNamed(std::string_view spec, size_t wanted)
Definition known.cpp:91
Image< P > cropImage(const Image< P > &image, Rect rect)
Result< Blob > runPipeline(Blob input, const Pipeline &pipeline, std::optional< std::chrono::steady_clock::time_point > deadline=std::nullopt)
Definition convert.cpp:15
constexpr bool encodable(Format format, Color color)
std::expected< T, Error > Result
Result< Image< Color::RGBA8888 > > Decode(const Blob &blob)
std::string_view contentTypeFor(Format format)
Definition format.cpp:62
Image< P > roundImageCorners(const Image< P > &image, int radius)
Result< std::vector< uint8_t > > Convert(const Image< Color::RGBA8888 > &image, Color to)
std::string path
Image< P > resizeImage(const Image< P > &image, Size size)
std::vector< uint8_t > data