@@ 1,21 @@
+MIT License
+
+Copyright (c) 2023 Andrea Feletto
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
@@ 1,193 @@
+const std = @import("std");
+const debug = std.debug;
+const fmt = std.fmt;
+const fs = std.fs;
+const heap = std.heap;
+const io = std.io;
+const log = std.log;
+const mem = std.mem;
+const os = std.os;
+const process = std.process;
+
+const Pixel = struct { r: u8, g: u8, b: u8, a: u8 };
+
+const Mask = struct {
+ r: u32,
+ g: u32,
+ b: u32,
+ a: u32,
+
+ pub fn init(data: []const u8) Mask {
+ debug.assert(data.len == 16);
+ const masks = mem.bytesAsSlice(u32, data);
+ return .{ .r = masks[0], .g = masks[1], .b = masks[2], .a = masks[3] };
+ }
+
+ pub fn apply(self: Mask, data: u32) Pixel {
+ return .{
+ .r = @intCast(u8, (data & self.r) >> @intCast(u5, @ctz(self.r))),
+ .g = @intCast(u8, (data & self.g) >> @intCast(u5, @ctz(self.g))),
+ .b = @intCast(u8, (data & self.b) >> @intCast(u5, @ctz(self.b))),
+ .a = @intCast(u8, (data & self.a) >> @intCast(u5, @ctz(self.a))),
+ };
+ }
+};
+
+const Image = struct {
+ allocator: mem.Allocator,
+ pixels: []Pixel,
+ width: u32,
+ height: u32,
+
+ pub fn init(allocator: mem.Allocator, filename: []const u8) !Image {
+ const cwd = fs.cwd();
+ const stat = try cwd.statFile(filename);
+ const bmp = try cwd.readFileAlloc(allocator, filename, stat.size);
+ defer allocator.free(bmp);
+ if (!mem.eql(u8, bmp[0..2], "BM")) return error.WrongFormat;
+
+ const dib_size = mem.readIntSlice(u16, bmp[14..18], .Little);
+ if (dib_size < 40) return error.UnsupportedDibHeader;
+
+ const color_depth = mem.readIntSlice(u16, bmp[28..30], .Little);
+ if (color_depth != 32) return error.UnsupportedColorDepth;
+
+ const compression = mem.readIntSlice(u32, bmp[30..34], .Little);
+ if (compression != 3) return error.UnsupportedCompressionMethod;
+
+ const offset = mem.readIntSlice(u32, bmp[10..14], .Little);
+ const data = mem.bytesAsSlice(u32, bmp[offset..]);
+
+ const width = mem.readIntSlice(u32, bmp[18..22], .Little);
+ const height = mem.readIntSlice(u32, bmp[22..26], .Little);
+ if (width * height != data.len) return error.CorruptedFile;
+
+ const mask = Mask.init(bmp[0x36..0x46]);
+ const pixels = try allocator.alloc(Pixel, width * height);
+
+ var y: usize = 0;
+ while (y < height) : (y += 1) {
+ const mirror = height - y - 1;
+ const row = data[y * width .. (y + 1) * width];
+ for (row) |raw_pixel, x| {
+ pixels[mirror * width + x] = mask.apply(raw_pixel);
+ }
+ }
+
+ return Image{ .allocator = allocator, .pixels = pixels, .width = width, .height = height };
+ }
+
+ pub fn deinit(self: Image) void {
+ self.allocator.free(self.pixels);
+ }
+};
+
+const Compressed = struct {
+ allocator: mem.Allocator,
+ pixels: []u8,
+ width: u32,
+ height: u32,
+
+ pub fn init(allocator: mem.Allocator, image: Image, max_width: u32) !Compressed {
+ const width = @min(image.width, max_width);
+ const x_bin_size = @intToFloat(f64, image.width) / @intToFloat(f64, width);
+ const y_bin_size = x_bin_size * 2.3;
+ const height = @floatToInt(u32, @intToFloat(f64, image.height) / y_bin_size);
+ const pixels = try allocator.alloc(u8, width * height);
+
+ var y: u32 = 0;
+ while (y < height) : (y += 1) {
+ const y_bin_start = @floor(y_bin_size * @intToFloat(f64, y));
+ const y_bin_stop = @ceil(y_bin_size * @intToFloat(f64, y + 1));
+
+ const y_start = @floatToInt(u32, y_bin_start);
+ const y_stop = @floatToInt(u32, y_bin_stop);
+
+ var x: u32 = 0;
+ while (x < width) : (x += 1) {
+ const x_bin_start = @floor(x_bin_size * @intToFloat(f64, x));
+ const x_bin_stop = @ceil(x_bin_size * @intToFloat(f64, x + 1));
+
+ const x_start = @floatToInt(u32, x_bin_start);
+ const x_stop = @floatToInt(u32, x_bin_stop);
+
+ pixels[y * width + x] = binToPixel(image, x_start, x_stop, y_start, y_stop);
+ }
+ }
+ return Compressed{ .allocator = allocator, .pixels = pixels, .width = width, .height = height };
+ }
+
+ pub fn deinit(self: Compressed) void {
+ self.allocator.free(self.pixels);
+ }
+
+ fn binToPixel(image: Image, x_start: u32, x_stop: u32, y_start: u32, y_stop: u32) u8 {
+ var avg: f64 = 0;
+ const size = (x_stop - x_start) * (y_stop - y_start);
+
+ var y = y_start;
+ while (y < y_stop) : (y += 1) {
+ var x = x_start;
+ while (x < x_stop) : (x += 1) {
+ const pixel = image.pixels[y * image.width + x];
+ const r = @intToFloat(f64, pixel.r);
+ const g = @intToFloat(f64, pixel.g);
+ const b = @intToFloat(f64, pixel.b);
+
+ const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b;
+ avg += luminance;
+ }
+ }
+
+ return @floatToInt(u8, avg / @intToFloat(f32, size));
+ }
+};
+
+pub fn main() !void {
+ var args = process.args();
+ _ = args.next();
+ const filename = args.next() orelse {
+ log.err("missing filename argument", .{});
+ return;
+ };
+ const max_width_str = args.next() orelse {
+ log.err("missing max width argument", .{});
+ return;
+ };
+ const max_width = fmt.parseUnsigned(u32, max_width_str, 10) catch {
+ log.err("max width must be a valid decimal number", .{});
+ return;
+ };
+
+ var gpa: heap.GeneralPurposeAllocator(.{}) = .{};
+ defer _ = gpa.deinit();
+ const allocator = gpa.allocator();
+
+ const image = try Image.init(allocator, filename);
+ defer image.deinit();
+
+ const compressed = try Compressed.init(allocator, image, max_width);
+ defer compressed.deinit();
+
+ const ascii = try allocator.alloc([]const u8, compressed.pixels.len);
+ defer allocator.free(ascii);
+ for (compressed.pixels) |pixel, i| ascii[i] = switch (pixel) {
+ 0...10 => " ",
+ 11...30 => "-",
+ 31...50 => ":",
+ 51...80 => "+",
+ 81...100 => "o",
+ 101...150 => "#",
+ 151...255 => "0",
+ };
+
+ const stdout = io.getStdOut().writer();
+ var y: usize = 0;
+ while (y < compressed.height) : (y += 1) {
+ var x: usize = 0;
+ while (x < compressed.width) : (x += 1) {
+ try stdout.writeAll(ascii[y * compressed.width + x]);
+ }
+ try stdout.writeByte('\n');
+ }
+}