| // basisu_text_image.cpp |
| // Copyright (C) 2019-2025 Binomial LLC. All Rights Reserved. |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| // |
| // See basisu_text_image.h for the text format description. |
| #include "basisu_text_image.h" |
| #include "encoder/basisu_enc.h" |
| |
| #include <string> |
| #include <vector> |
| |
| namespace basisu |
| { |
| // Max image width/height accepted/emitted by the text format (these are intended for small test images). |
| const uint32_t TEXT_IMAGE_MAX_DIM = 1024; |
| |
| // Trim leading/trailing ASCII whitespace. Includes '\r', so the trailing CR of a CRLF line |
| // is stripped after the caller has split the buffer on '\n'. |
| static std::string trim_ws(const std::string& s) |
| { |
| size_t b = 0, e = s.size(); |
| const auto is_ws = [](char c) { return (c == ' ') || (c == '\t') || (c == '\r') || (c == '\n') || (c == '\f') || (c == '\v'); }; |
| while ((b < e) && is_ws(s[b])) b++; |
| while ((e > b) && is_ws(s[e - 1])) e--; |
| return s.substr(b, e - b); |
| } |
| |
| // Parses a non-empty run of decimal digits with value in [0, max_val]. Rejects empty strings, |
| // signs, and any non-digit character. Used for both header fields and pixel values. |
| static bool parse_decimal_uint(const std::string& tok, uint32_t max_val, uint32_t& out_val) |
| { |
| if (tok.empty()) |
| return false; |
| uint64_t v = 0; |
| for (size_t i = 0; i < tok.size(); i++) |
| { |
| const char c = tok[i]; |
| if ((c < '0') || (c > '9')) |
| return false; |
| v = v * 10 + (uint64_t)(c - '0'); |
| if (v > max_val) |
| return false; // out of range (also guards against overflow) |
| } |
| out_val = (uint32_t)v; |
| return true; |
| } |
| |
| // Splits a line on commas into trimmed tokens. |
| static void split_commas(const std::string& s, std::vector<std::string>& out_tokens) |
| { |
| out_tokens.resize(0); |
| size_t start = 0; |
| for (;;) |
| { |
| const size_t comma = s.find(',', start); |
| if (comma == std::string::npos) |
| { |
| out_tokens.push_back(trim_ws(s.substr(start))); |
| break; |
| } |
| out_tokens.push_back(trim_ws(s.substr(start, comma - start))); |
| start = comma + 1; |
| } |
| } |
| |
| bool text_to_png(const char* pInput_txt_filename, const char* pOutput_png_filename) |
| { |
| uint8_vec file_data; |
| if (!read_file_to_vec(pInput_txt_filename, file_data)) |
| { |
| error_printf("text_to_png: failed reading input text file \"%s\"\n", pInput_txt_filename); |
| return false; |
| } |
| |
| const char* pData = file_data.size() ? (const char*)&file_data[0] : ""; |
| const size_t data_size = file_data.size(); |
| |
| uint32_t width = 0, height = 0, channels = 0; |
| uint64_t total_pixels = 0, pixels_written = 0; |
| bool have_header = false; |
| image img; |
| std::vector<std::string> tokens; |
| |
| size_t pos = 0; |
| uint32_t line_no = 0; |
| while (pos < data_size) |
| { |
| // Carve out one physical line [pos, line_end). |
| size_t line_end = pos; |
| while ((line_end < data_size) && (pData[line_end] != '\n')) |
| line_end++; |
| line_no++; |
| |
| const std::string line = trim_ws(std::string(pData + pos, pData + line_end)); |
| pos = line_end + 1; |
| |
| // Skip blank lines and '#' comments. |
| if (line.empty() || (line[0] == '#')) |
| continue; |
| |
| split_commas(line, tokens); |
| |
| if (!have_header) |
| { |
| if (tokens.size() != 3) |
| { |
| error_printf("text_to_png: line %u: header must be \"W,H,C\" (got %u value(s))\n", line_no, (uint32_t)tokens.size()); |
| return false; |
| } |
| if (!parse_decimal_uint(tokens[0], TEXT_IMAGE_MAX_DIM, width) || (width < 1) || |
| !parse_decimal_uint(tokens[1], TEXT_IMAGE_MAX_DIM, height) || (height < 1) || |
| !parse_decimal_uint(tokens[2], 4, channels) || (channels < 1)) |
| { |
| error_printf("text_to_png: line %u: invalid header \"%s\"; expected W,H,C with W,H in [1,1024] and C in [1,4]\n", line_no, line.c_str()); |
| return false; |
| } |
| |
| total_pixels = (uint64_t)width * height; |
| img.resize(width, height); |
| have_header = true; |
| continue; |
| } |
| |
| // Pixel line: exactly 'channels' decimal values in [0,255]. |
| if (tokens.size() != channels) |
| { |
| error_printf("text_to_png: line %u: expected %u value(s) per pixel, got %u: \"%s\"\n", line_no, channels, (uint32_t)tokens.size(), line.c_str()); |
| return false; |
| } |
| |
| uint32_t v[4] = { 0, 0, 0, 0 }; |
| for (uint32_t c = 0; c < channels; c++) |
| { |
| if (!parse_decimal_uint(tokens[c], 255, v[c])) |
| { |
| error_printf("text_to_png: line %u: \"%s\" is not a decimal value in [0,255]\n", line_no, tokens[c].c_str()); |
| return false; |
| } |
| } |
| |
| if (pixels_written >= total_pixels) |
| { |
| error_printf("text_to_png: line %u: too many pixels (expected %llu for %ux%u)\n", line_no, (unsigned long long)total_pixels, width, height); |
| return false; |
| } |
| |
| const uint32_t x = (uint32_t)(pixels_written % width), y = (uint32_t)(pixels_written / width); |
| color_rgba& d = img(x, y); |
| switch (channels) |
| { |
| case 1: d.set((uint8_t)v[0], (uint8_t)v[0], (uint8_t)v[0], 255); break; // L |
| case 2: d.set((uint8_t)v[0], (uint8_t)v[0], (uint8_t)v[0], (uint8_t)v[1]); break; // LA |
| case 3: d.set((uint8_t)v[0], (uint8_t)v[1], (uint8_t)v[2], 255); break; // RGB |
| default: d.set((uint8_t)v[0], (uint8_t)v[1], (uint8_t)v[2], (uint8_t)v[3]); break; // RGBA |
| } |
| pixels_written++; |
| } |
| |
| if (!have_header) |
| { |
| error_printf("text_to_png: \"%s\" has no header line (expected W,H,C on the first non-blank, non-comment line)\n", pInput_txt_filename); |
| return false; |
| } |
| |
| if (pixels_written != total_pixels) |
| { |
| error_printf("text_to_png: expected %llu pixel(s) for %ux%u, got %llu\n", (unsigned long long)total_pixels, width, height, (unsigned long long)pixels_written); |
| return false; |
| } |
| |
| if (!save_png(pOutput_png_filename, img)) |
| { |
| error_printf("text_to_png: failed writing output PNG \"%s\"\n", pOutput_png_filename); |
| return false; |
| } |
| |
| printf("Wrote %ux%u %u-channel image to \"%s\" (%llu pixels)\n", width, height, channels, pOutput_png_filename, (unsigned long long)total_pixels); |
| return true; |
| } |
| |
| bool png_to_text(const char* pInput_png_filename, const char* pOutput_txt_filename) |
| { |
| image img; |
| if (!load_image(pInput_png_filename, img)) |
| { |
| error_printf("png_to_text: failed loading image \"%s\"\n", pInput_png_filename); |
| return false; |
| } |
| |
| const uint32_t width = img.get_width(), height = img.get_height(); |
| if ((width < 1) || (width > TEXT_IMAGE_MAX_DIM) || (height < 1) || (height > TEXT_IMAGE_MAX_DIM)) |
| { |
| error_printf("png_to_text: image is %ux%u; dimensions must be in [1,%u]\n", width, height, TEXT_IMAGE_MAX_DIM); |
| return false; |
| } |
| |
| // Auto-detect the minimal channel count: color if any pixel has R!=G or G!=B, alpha if any pixel has A!=255. |
| bool is_color = false, has_alpha = false; |
| for (uint32_t y = 0; y < height; y++) |
| { |
| for (uint32_t x = 0; x < width; x++) |
| { |
| const color_rgba& c = img(x, y); |
| if ((c.r != c.g) || (c.g != c.b)) |
| is_color = true; |
| if (c.a != 255) |
| has_alpha = true; |
| } |
| } |
| const uint32_t channels = is_color ? (has_alpha ? 4u : 3u) : (has_alpha ? 2u : 1u); |
| const char* pChannels_desc = (channels == 1) ? "L" : (channels == 2) ? "LA" : (channels == 3) ? "RGB" : "RGBA"; |
| |
| std::string out; |
| out.reserve((size_t)width * height * 12 + 64); |
| |
| char buf[256]; |
| snprintf(buf, sizeof(buf), "# %ux%u, %u channel(s) (%s)\n", width, height, channels, pChannels_desc); |
| out += buf; |
| snprintf(buf, sizeof(buf), "%u,%u,%u\n", width, height, channels); |
| out += buf; |
| |
| for (uint32_t y = 0; y < height; y++) |
| { |
| for (uint32_t x = 0; x < width; x++) |
| { |
| const color_rgba& c = img(x, y); |
| switch (channels) |
| { |
| case 1: snprintf(buf, sizeof(buf), "%u\n", (uint32_t)c.r); break; // L (== R since grayscale) |
| case 2: snprintf(buf, sizeof(buf), "%u,%u\n", (uint32_t)c.r, (uint32_t)c.a); break; // LA |
| case 3: snprintf(buf, sizeof(buf), "%u,%u,%u\n", (uint32_t)c.r, (uint32_t)c.g, (uint32_t)c.b); break; // RGB |
| default: snprintf(buf, sizeof(buf), "%u,%u,%u,%u\n", (uint32_t)c.r, (uint32_t)c.g, (uint32_t)c.b, (uint32_t)c.a); break; // RGBA |
| } |
| out += buf; |
| } |
| } |
| |
| if (!write_data_to_file(pOutput_txt_filename, out.data(), out.size())) |
| { |
| error_printf("png_to_text: failed writing output text file \"%s\"\n", pOutput_txt_filename); |
| return false; |
| } |
| |
| printf("Wrote %ux%u %u-channel (%s) image as text to \"%s\" (%llu pixels)\n", |
| width, height, channels, pChannels_desc, pOutput_txt_filename, (unsigned long long)((uint64_t)width * height)); |
| return true; |
| } |
| |
| } // namespace basisu |