48 if (value ==
"auto") {
51 if (value ==
"truecolor") {
57 if (value ==
"ansi16") {
60 if (value ==
"mono") {
63 throw std::invalid_argument(
"Invalid mode: " + std::string(value));
69 if (options.
width <= 0) {
70 throw std::invalid_argument(
"width must be positive");
72 if (!std::filesystem::exists(image_path)) {
73 throw std::runtime_error(
"Image path does not exist: " + image_path.string());
77 enable_ansi_on_windows();
80 const Image image = load_image(image_path.string());
82 render_to_stream(image, options.
width, resolved_mode, out);
85 static void render_from_path(
const std::filesystem::path& image_path, std::ostream& out) {
100 std::vector<Color> pixels;
104 static void enable_ansi_on_windows() {
105 HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
106 if (handle == INVALID_HANDLE_VALUE || handle ==
nullptr) {
111 if (!GetConsoleMode(handle, &mode)) {
115 mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING;
116 SetConsoleMode(handle, mode);
125 if (std::getenv(
"NO_COLOR") !=
nullptr) {
129 const std::string colorterm = std::getenv(
"COLORTERM") ? std::getenv(
"COLORTERM") :
"";
130 const std::string term = std::getenv(
"TERM") ? std::getenv(
"TERM") :
"";
132 if (colorterm.find(
"truecolor") != std::string::npos || colorterm.find(
"24bit") != std::string::npos) {
135 if (term.find(
"256color") != std::string::npos) {
138 if (!term.empty() && term !=
"dumb") {
149 static Image load_image(
const std::string& path) {
153 unsigned char* data =
stbi_load(path.c_str(), &width, &height, &channels, 4);
154 if (data ==
nullptr) {
155 throw std::runtime_error(
"Failed to load image: " + path);
160 image.height = height;
161 image.pixels.resize(
static_cast<std::size_t
>(width) *
static_cast<std::size_t
>(height));
163 for (
int y = 0; y < height; ++y) {
164 for (
int x = 0; x < width; ++x) {
165 const std::size_t idx =
static_cast<std::size_t
>(y) *
static_cast<std::size_t
>(width) +
static_cast<std::size_t
>(x);
166 const std::size_t
off = idx * 4;
167 image.pixels[idx] = Color{
168 static_cast<std::uint8_t
>(data[
off + 0]),
169 static_cast<std::uint8_t
>(data[off + 1]),
170 static_cast<std::uint8_t
>(data[
off + 2]),
171 static_cast<std::uint8_t
>(data[off + 3])};
179 static Color sample_bilinear(
const Image& image,
float x,
float y) {
180 x = std::clamp(x, 0.0f,
static_cast<float>(image.width - 1));
181 y = std::clamp(y, 0.0f,
static_cast<float>(image.height - 1));
183 const int x0 =
static_cast<int>(std::floor(x));
184 const int y0 =
static_cast<int>(std::floor(y));
185 const int x1 = std::min(x0 + 1, image.width - 1);
186 const int y1 = std::min(y0 + 1, image.height - 1);
188 const float tx = x -
static_cast<float>(x0);
189 const float ty = y -
static_cast<float>(y0);
191 const auto pixel = [&](
int px,
int py) -> Color {
192 const std::size_t idx =
static_cast<std::size_t
>(py) *
static_cast<std::size_t
>(image.width) +
static_cast<std::size_t
>(px);
193 return image.pixels[idx];
196 const Color c00 = pixel(x0, y0);
197 const Color c10 = pixel(x1, y0);
198 const Color c01 = pixel(x0, y1);
199 const Color c11 = pixel(x1, y1);
201 auto lerp = [](
float a,
float b,
float t) {
202 return a + (b - a) * t;
205 const float r0 = lerp(
static_cast<float>(c00.r),
static_cast<float>(c10.r), tx);
206 const float g0 = lerp(
static_cast<float>(c00.g),
static_cast<float>(c10.g), tx);
207 const float b0 = lerp(
static_cast<float>(c00.b),
static_cast<float>(c10.b), tx);
208 const float a0 = lerp(
static_cast<float>(c00.a),
static_cast<float>(c10.a), tx);
210 const float r1 = lerp(
static_cast<float>(c01.r),
static_cast<float>(c11.r), tx);
211 const float g1 = lerp(
static_cast<float>(c01.g),
static_cast<float>(c11.g), tx);
212 const float b1 = lerp(
static_cast<float>(c01.b),
static_cast<float>(c11.b), tx);
213 const float a1 = lerp(
static_cast<float>(c01.a),
static_cast<float>(c11.a), tx);
216 static_cast<std::uint8_t
>(std::round(lerp(r0, r1, ty))),
217 static_cast<std::uint8_t
>(std::round(lerp(g0, g1, ty))),
218 static_cast<std::uint8_t
>(std::round(lerp(b0, b1, ty))),
219 static_cast<std::uint8_t
>(std::round(lerp(a0, a1, ty)))};
222 static std::vector<Color> resize_image(
const Image& image,
int out_w,
int out_h) {
223 std::vector<Color> out(
static_cast<std::size_t
>(out_w) *
static_cast<std::size_t
>(out_h));
224 for (
int y = 0; y < out_h; ++y) {
225 const float src_y = (
static_cast<float>(y) + 0.5f) *
static_cast<float>(image.height) /
static_cast<float>(out_h) - 0.5f;
226 for (
int x = 0; x < out_w; ++x) {
227 const float src_x = (
static_cast<float>(x) + 0.5f) *
static_cast<float>(image.width) /
static_cast<float>(out_w) - 0.5f;
228 out[
static_cast<std::size_t
>(y) *
static_cast<std::size_t
>(out_w) +
static_cast<std::size_t
>(x)] =
229 sample_bilinear(image, src_x, src_y);
235 static int ansi256_index(Color c) {
236 const int r =
static_cast<int>(std::round((c.r / 255.0) * 5.0));
237 const int g =
static_cast<int>(std::round((c.g / 255.0) * 5.0));
238 const int b =
static_cast<int>(std::round((c.b / 255.0) * 5.0));
239 return 16 + 36 * r + 6 * g + b;
242 static int ansi16_index(Color c,
bool bright) {
243 const int r = c.r >= 128 ? 1 : 0;
244 const int g = c.g >= 128 ? 1 : 0;
245 const int b = c.b >= 128 ? 1 : 0;
246 int idx = r + 2 * g + 4 * b;
247 if (idx == 0 && bright) {
253 static std::string fg_escape(Color c,
ColorMode mode) {
255 return "\x1b[38;2;" + std::to_string(c.r) +
";" + std::to_string(c.g) +
";" + std::to_string(c.b) +
"m";
258 return "\x1b[38;5;" + std::to_string(ansi256_index(c)) +
"m";
261 const int idx = ansi16_index(c,
true);
262 return "\x1b[" + std::to_string((idx < 8 ? 30 : 90) + (idx % 8)) +
"m";
267 static std::string bg_escape(Color c,
ColorMode mode) {
269 return "\x1b[48;2;" + std::to_string(c.r) +
";" + std::to_string(c.g) +
";" + std::to_string(c.b) +
"m";
272 return "\x1b[48;5;" + std::to_string(ansi256_index(c)) +
"m";
275 const int idx = ansi16_index(c,
false);
276 return "\x1b[" + std::to_string((idx < 8 ? 40 : 100) + (idx % 8)) +
"m";
281 static char mono_char(Color top, Color bottom) {
282 if (top.a < 16 && bottom.a < 16) {
285 const int lum_top = (
static_cast<int>(top.r) * 2126 +
static_cast<int>(top.g) * 7152 +
static_cast<int>(top.b) * 722) / 10000;
286 const int lum_bottom =
287 (
static_cast<int>(bottom.r) * 2126 +
static_cast<int>(bottom.g) * 7152 +
static_cast<int>(bottom.b) * 722) / 10000;
288 const int avg = (lum_top + lum_bottom) / 2;
289 constexpr std::array<char, 10> ramp = {
' ',
'.',
':',
'-',
'=',
'+',
'*',
'#',
'%',
'@'};
290 const std::size_t idx =
static_cast<std::size_t
>(std::clamp(avg, 0, 255)) * (ramp.size() - 1) / 255;
294 static void render_to_stream(
const Image& image,
int out_w,
ColorMode mode, std::ostream& out) {
295 constexpr float pixel_aspect = 0.5f;
296 const float out_h_f =
static_cast<float>(image.height) *
static_cast<float>(out_w) * pixel_aspect /
297 static_cast<float>(image.width);
298 const int out_h = std::max(2,
static_cast<int>(std::round(out_h_f)));
300 const std::vector<Color> scaled = resize_image(image, out_w, out_h);
302 for (
int y = 0; y < out_h; y += 2) {
303 for (
int x = 0; x < out_w; ++x) {
304 const Color top = scaled[
static_cast<std::size_t
>(y) *
static_cast<std::size_t
>(out_w) +
static_cast<std::size_t
>(x)];
305 const Color bottom = scaled[
static_cast<std::size_t
>(std::min(y + 1, out_h - 1)) *
static_cast<std::size_t
>(out_w) +
306 static_cast<std::size_t
>(x)];
307 const bool top_visible = top.a >= 16;
308 const bool bottom_visible = bottom.a >= 16;
311 out << mono_char(top, bottom);
313 if (!top_visible && !bottom_visible) {
315 }
else if (top_visible && bottom_visible) {
316 out << fg_escape(top, mode) << bg_escape(bottom, mode) <<
"▀";
317 }
else if (top_visible) {
319 out <<
"\x1b[0m" << fg_escape(top, mode) <<
"▀";
322 out <<
"\x1b[0m" << fg_escape(bottom, mode) <<
"▄";