feat: support for IPP printing + test page

This commit is contained in:
2026-04-01 21:15:27 +02:00
parent 868d0fd4f5
commit 550d4d2a19
28 changed files with 4121 additions and 62 deletions
+79
View File
@@ -0,0 +1,79 @@
# Makefile for printd (userspace print spooler) on MontaukOS
MAKEFLAGS += -rR
.SUFFIXES:
TOOLCHAIN_PREFIX := $(shell cd ../../.. && pwd)/toolchain/local/bin/x86_64-elf-
ifneq ($(wildcard $(TOOLCHAIN_PREFIX)gcc),)
CXX := $(TOOLCHAIN_PREFIX)g++
else
CXX := g++
endif
PROG_INC := ../../include
LIBC_LIB := ../../lib/libc
TLS_LIB := ../../lib/tls
BEARSSL := ../../lib/bearssl
JPEGWRITE := ../../lib/libjpegwrite
LINK_LD := ../../link.ld
BINDIR := ../../bin
OBJDIR := obj
CXXFLAGS := \
-std=gnu++20 \
-g -O2 -pipe \
-Wall \
-Wextra \
-Wno-unused-parameter \
-nostdinc \
-ffreestanding \
-fno-stack-protector \
-fno-stack-check \
-fno-PIC \
-fno-rtti \
-fno-exceptions \
-ffunction-sections \
-fdata-sections \
-m64 \
-march=x86-64 \
-msse \
-msse2 \
-mno-red-zone \
-mcmodel=small \
-I $(PROG_INC) \
-isystem $(PROG_INC)/libc \
-I $(BEARSSL)/inc \
-isystem ../../../kernel/freestnd-c-hdrs/x86_64/include \
-isystem ../../../kernel/freestnd-cxx-hdrs/x86_64/include
LDFLAGS := \
-nostdlib \
-static \
-Wl,--build-id=none \
-Wl,--gc-sections \
-Wl,-m,elf_x86_64 \
-z max-page-size=0x1000 \
-T $(LINK_LD)
SRCS := main.cpp test_page_jpeg.cpp font_data.cpp stb_truetype_impl.cpp
OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
LIBS := $(TLS_LIB)/libtls.a $(BEARSSL)/libbearssl.a $(JPEGWRITE)/libjpegwrite.a $(LIBC_LIB)/liblibc.a
TARGET := $(BINDIR)/os/printd.elf
DEPS := $(OBJS:.o=.d)
.PHONY: all clean
all: $(TARGET)
$(TARGET): $(OBJS) $(LIBS) $(LINK_LD) Makefile
mkdir -p $(BINDIR)/os
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBS) -o $@
$(OBJDIR)/%.o: %.cpp Makefile
mkdir -p $(OBJDIR)
$(CXX) $(CXXFLAGS) -MMD -MP -c $< -o $@
-include $(DEPS)
clean:
rm -rf $(OBJDIR) $(TARGET)
+1
View File
@@ -0,0 +1 @@
#include "../desktop/font_data.cpp"
+273
View File
@@ -0,0 +1,273 @@
/*
* main.cpp
* MontaukOS userspace print spooler daemon
* Copyright (c) 2026 Daniel Hammer
*/
#include <montauk/syscall.h>
#include <print/print.hpp>
#include "test_page_jpeg.hpp"
extern "C" {
#include <stdio.h>
#include <string.h>
}
using namespace print;
static void log_msg(const char* fmt, ...) {
va_list ap;
va_start(ap, fmt);
printf("[printd] ");
vprintf(fmt, ap);
printf("\n");
va_end(ap);
}
static void set_job_state(JobMeta* job, const char* state, const char* message) {
safe_copy(job->state, sizeof(job->state), state);
if (message && *message) safe_copy(job->status_message, sizeof(job->status_message), message);
now_string(job->updated_at, sizeof(job->updated_at));
}
static void set_job_debug(JobMeta* job, const char* detail) {
if (job == nullptr) return;
safe_copy(job->debug_info, sizeof(job->debug_info), detail ? detail : "");
}
static void set_job_debug_from_result(JobMeta* job, const IppPrintResult* result, const char* prefix) {
if (job == nullptr) return;
char detail[MAX_DEBUG_LEN] = {};
summarize_ipp_print_result(result, detail, sizeof(detail));
if (prefix && *prefix) {
if (detail[0]) {
char joined[MAX_DEBUG_LEN];
snprintf(joined, sizeof(joined), "%s; %s", prefix, detail);
safe_copy(job->debug_info, sizeof(job->debug_info), joined);
} else {
safe_copy(job->debug_info, sizeof(job->debug_info), prefix);
}
} else {
safe_copy(job->debug_info, sizeof(job->debug_info), detail);
}
}
static void persist_job_debug(const char* active_path, JobMeta* job, const char* detail) {
if (active_path == nullptr || *active_path == '\0' || job == nullptr) return;
set_job_debug(job, detail);
now_string(job->updated_at, sizeof(job->updated_at));
save_job_to_path_atomic(active_path, job);
}
static bool write_pid_file() {
char pid[32];
snprintf(pid, sizeof(pid), "%d\n", montauk::getpid());
return write_text_file_atomic(SPOOL_DAEMON_PID_PATH, pid);
}
static void requeue_stale_active_jobs() {
DIR* dir = opendir(SPOOL_ACTIVE_DIR);
if (!dir) return;
struct dirent* ent = nullptr;
while ((ent = readdir(dir)) != nullptr) {
if (ent->d_name[0] == '.') continue;
char src[MAX_PATH_LEN];
char dst[MAX_PATH_LEN];
path_join(src, sizeof(src), SPOOL_ACTIVE_DIR, ent->d_name);
path_join(dst, sizeof(dst), SPOOL_QUEUE_DIR, ent->d_name);
JobMeta job = {};
if (load_job_from_path(src, &job)) {
set_job_state(&job, "queued", "Re-queued after daemon restart");
save_job_to_path_atomic(src, &job);
}
rename(src, dst);
}
closedir(dir);
}
static bool claim_next_job(char* out_active_path, int out_active_path_len) {
DIR* dir = opendir(SPOOL_QUEUE_DIR);
if (!dir) return false;
bool claimed = false;
struct dirent* ent = nullptr;
while ((ent = readdir(dir)) != nullptr) {
if (ent->d_name[0] == '.') continue;
char queued[MAX_PATH_LEN];
char active[MAX_PATH_LEN];
path_join(queued, sizeof(queued), SPOOL_QUEUE_DIR, ent->d_name);
path_join(active, sizeof(active), SPOOL_ACTIVE_DIR, ent->d_name);
if (rename(queued, active) == 0) {
safe_copy(out_active_path, out_active_path_len, active);
claimed = true;
break;
}
}
closedir(dir);
return claimed;
}
static void finalize_job(const char* active_path, const char* target_dir, JobMeta* job) {
save_job_to_path_atomic(active_path, job);
char target[MAX_PATH_LEN];
make_job_file_path(target_dir, job->id, target, sizeof(target));
if (rename(active_path, target) != 0) {
log_msg("warning: failed to move job %s to %s", job->id, target_dir);
}
}
static bool try_print_test_page(const char* active_path, JobMeta* job,
IppPrintResult* result, char* err, int err_len) {
persist_job_debug(active_path, job, "querying printer capabilities");
IppCapabilities caps = {};
bool have_caps = ipp_get_printer_capabilities(job->printer_uri, &caps, err, err_len);
if (have_caps) {
char detail[MAX_DEBUG_LEN] = {};
summarize_ipp_capabilities(&caps, detail, sizeof(detail));
persist_job_debug(active_path, job, detail);
} else if (err && err[0]) {
char detail[MAX_DEBUG_LEN];
snprintf(detail, sizeof(detail), "capability probe failed; attempting direct submit (%s)", err);
persist_job_debug(active_path, job, detail);
}
const struct {
const char* mime;
bool (*generate)(uint8_t**, int*);
bool allowed;
} attempts[] = {
{"application/pdf", generate_test_page_pdf, !have_caps || caps.supports_pdf},
{"image/jpeg", generate_test_page_jpeg, !have_caps || caps.supports_jpeg},
{"text/plain", generate_test_page_text, !have_caps || caps.supports_text},
};
if (have_caps && !caps.supports_pdf && !caps.supports_jpeg && !caps.supports_text) {
if (caps.supported_formats[0]) {
snprintf(err, (size_t)err_len,
"printer does not advertise a built-in test-page format (supports: %s)",
caps.supported_formats);
} else {
safe_copy(err, err_len, "printer does not advertise a built-in test-page format");
}
return false;
}
for (size_t i = 0; i < sizeof(attempts) / sizeof(attempts[0]); i++) {
if (!attempts[i].allowed) continue;
uint8_t* data = nullptr;
int len = 0;
if (!attempts[i].generate(&data, &len)) {
safe_copy(err, err_len, "failed to generate test page");
return false;
}
char detail[MAX_DEBUG_LEN];
snprintf(detail, sizeof(detail), "submitting test page as %s (%d bytes)",
attempts[i].mime, len);
persist_job_debug(active_path, job, detail);
bool ok = ipp_print_buffer(job->printer_uri, job->job_name, job->user_name,
attempts[i].mime, data, len, result, err, err_len);
free(data);
if (ok) return true;
}
return false;
}
static bool try_print_document(const char* active_path, JobMeta* job,
IppPrintResult* result, char* err, int err_len) {
persist_job_debug(active_path, job, "loading document bytes");
uint8_t* data = nullptr;
int len = 0;
if (!read_file_bytes(job->doc_path, &data, &len, err, err_len)) return false;
char detail[MAX_DEBUG_LEN];
snprintf(detail, sizeof(detail), "submitting %s document (%d bytes)",
job->doc_format[0] ? job->doc_format : "application/octet-stream", len);
persist_job_debug(active_path, job, detail);
bool ok = ipp_print_buffer(job->printer_uri, job->job_name, job->user_name,
job->doc_format, data, len, result, err, err_len);
free(data);
return ok;
}
static void process_job(const char* active_path) {
JobMeta job = {};
if (!load_job_from_path(active_path, &job)) {
log_msg("dropping unreadable job file: %s", active_path);
remove(active_path);
return;
}
log_msg("processing job %s (%s)", job.id, job.job_name);
set_job_state(&job, "printing", "Sending to printer");
set_job_debug(&job, "dispatching queued job");
save_job_to_path_atomic(active_path, &job);
IppPrintResult result = {};
char err[128] = {};
bool ok = false;
if (strcmp(job.source_kind, "test-page") == 0)
ok = try_print_test_page(active_path, &job, &result, err, sizeof(err));
else
ok = try_print_document(active_path, &job, &result, err, sizeof(err));
if (ok) {
job.remote_job_id = result.job_id;
set_job_state(&job, "completed",
result.status_message[0] ? result.status_message : "Printed");
set_job_debug_from_result(&job, &result, nullptr);
finalize_job(active_path, SPOOL_DONE_DIR, &job);
log_msg("job %s completed (printer job id %d)", job.id, result.job_id);
} else {
set_job_state(&job, "failed", err[0] ? err : "Print failed");
set_job_debug_from_result(&job, &result, err[0] ? err : nullptr);
finalize_job(active_path, SPOOL_FAILED_DIR, &job);
log_msg("job %s failed: %s", job.id, job.status_message);
if (job.debug_info[0]) log_msg("job %s debug: %s", job.id, job.debug_info);
}
if (job.doc_path[0] != '\0') remove(job.doc_path);
}
extern "C" void _start() {
if (!ensure_spool_dirs()) {
log_msg("failed to initialize spool directories");
montauk::exit(1);
}
int running_pid = -1;
if (daemon_is_running(&running_pid) && running_pid != montauk::getpid()) {
log_msg("another print daemon is already running (pid %d)", running_pid);
montauk::exit(0);
}
if (!write_pid_file()) {
log_msg("failed to write daemon pid file");
montauk::exit(1);
}
requeue_stale_active_jobs();
log_msg("ready");
for (;;) {
char active_path[MAX_PATH_LEN];
if (claim_next_job(active_path, sizeof(active_path))) {
process_job(active_path);
continue;
}
montauk::sleep_ms(1000);
}
}
+32
View File
@@ -0,0 +1,32 @@
/*
* stb_truetype_impl.cpp
* Single compilation unit for stb_truetype in MontaukOS freestanding environment
*/
#include <cstdint>
#include <cstddef>
#include <montauk/heap.h>
#include <montauk/string.h>
#include <gui/stb_math.h>
#define STBTT_ifloor(x) ((int) stb_floor(x))
#define STBTT_iceil(x) ((int) stb_ceil(x))
#define STBTT_sqrt(x) stb_sqrt(x)
#define STBTT_pow(x,y) stb_pow(x,y)
#define STBTT_fmod(x,y) stb_fmod(x,y)
#define STBTT_cos(x) stb_cos(x)
#define STBTT_acos(x) stb_acos(x)
#define STBTT_fabs(x) stb_fabs(x)
#define STBTT_malloc(x,u) ((void)(u), montauk::malloc(x))
#define STBTT_free(x,u) ((void)(u), montauk::mfree(x))
#define STBTT_memcpy(d,s,n) montauk::memcpy(d,s,n)
#define STBTT_memset(d,v,n) montauk::memset(d,v,n)
#define STBTT_strlen(x) montauk::slen(x)
#define STBTT_assert(x) ((void)(x))
#define STB_TRUETYPE_IMPLEMENTATION
#include <gui/stb_truetype.h>
+311
View File
@@ -0,0 +1,311 @@
/*
* test_page_jpeg.cpp
* JPEG test-page generator for printers that do not accept PDF/text jobs.
*/
#include "test_page_jpeg.hpp"
#include <gui/truetype.hpp>
#include <montauk/string.h>
#include <print/print.hpp>
extern "C" {
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define STBI_WRITE_NO_STDIO
#include <gui/stb_image_write.h>
}
namespace gui {
extern const uint8_t font_data[256 * 16];
}
namespace {
static constexpr int PAGE_W = 1240;
static constexpr int PAGE_H = 1754;
static constexpr const char* ROBOTO_MEDIUM_PATH = "0:/fonts/Roboto-Medium.ttf";
static constexpr const char* ROBOTO_BOLD_PATH = "0:/fonts/Roboto-Bold.ttf";
struct JpegBuffer {
uint8_t* data;
int size;
int capacity;
bool failed;
};
struct PageFonts {
gui::TrueTypeFont* medium;
gui::TrueTypeFont* bold;
};
static void jpeg_write_callback(void* context, void* data, int size) {
JpegBuffer* buf = (JpegBuffer*)context;
if (!buf || !data || size <= 0 || buf->failed) return;
int needed = buf->size + size;
if (needed > buf->capacity) {
int new_cap = buf->capacity > 0 ? buf->capacity * 2 : 4096;
if (new_cap < needed) new_cap = needed;
uint8_t* new_data = (uint8_t*)realloc(buf->data, (size_t)new_cap);
if (!new_data) {
buf->failed = true;
return;
}
buf->data = new_data;
buf->capacity = new_cap;
}
memcpy(buf->data + buf->size, data, (size_t)size);
buf->size += size;
}
static void fill_rect(uint8_t* rgb, int x, int y, int w, int h, uint8_t r, uint8_t g, uint8_t b) {
if (!rgb || w <= 0 || h <= 0) return;
if (x < 0) { w += x; x = 0; }
if (y < 0) { h += y; y = 0; }
if (x + w > PAGE_W) w = PAGE_W - x;
if (y + h > PAGE_H) h = PAGE_H - y;
if (w <= 0 || h <= 0) return;
for (int yy = y; yy < y + h; yy++) {
uint8_t* row = rgb + (yy * PAGE_W + x) * 3;
for (int xx = 0; xx < w; xx++) {
row[xx * 3 + 0] = r;
row[xx * 3 + 1] = g;
row[xx * 3 + 2] = b;
}
}
}
static void draw_rect_outline(uint8_t* rgb, int x, int y, int w, int h,
int thickness, uint8_t r, uint8_t g, uint8_t b) {
fill_rect(rgb, x, y, w, thickness, r, g, b);
fill_rect(rgb, x, y + h - thickness, w, thickness, r, g, b);
fill_rect(rgb, x, y, thickness, h, r, g, b);
fill_rect(rgb, x + w - thickness, y, thickness, h, r, g, b);
}
static void blend_pixel(uint8_t* rgb, int x, int y,
uint8_t r, uint8_t g, uint8_t b, uint8_t alpha) {
if (!rgb || alpha == 0 || x < 0 || x >= PAGE_W || y < 0 || y >= PAGE_H) return;
uint8_t* p = rgb + (y * PAGE_W + x) * 3;
if (alpha == 255) {
p[0] = r;
p[1] = g;
p[2] = b;
return;
}
uint32_t a = alpha;
uint32_t inv = 255 - a;
p[0] = (uint8_t)((a * r + inv * p[0] + 127) / 255);
p[1] = (uint8_t)((a * g + inv * p[1] + 127) / 255);
p[2] = (uint8_t)((a * b + inv * p[2] + 127) / 255);
}
static void draw_char_scaled(uint8_t* rgb, int x, int y, int scale, char ch,
uint8_t r, uint8_t g, uint8_t b) {
const uint8_t* glyph = &gui::font_data[(unsigned char)ch * 16];
for (int row = 0; row < 16; row++) {
uint8_t bits = glyph[row];
for (int col = 0; col < 8; col++) {
if (!(bits & (0x80 >> col))) continue;
fill_rect(rgb, x + col * scale, y + row * scale, scale, scale, r, g, b);
}
}
}
static void draw_text_scaled(uint8_t* rgb, int x, int y, int scale, const char* text,
uint8_t r, uint8_t g, uint8_t b) {
if (!rgb || !text) return;
for (int i = 0; text[i] != '\0'; i++)
draw_char_scaled(rgb, x + i * 8 * scale, y, scale, text[i], r, g, b);
}
static bool init_page_font(gui::TrueTypeFont* font, const char* path) {
if (!font || !path) return false;
memset(font, 0, sizeof(*font));
return font->init(path);
}
static gui::TrueTypeFont* load_page_font(const char* path) {
gui::TrueTypeFont* font = (gui::TrueTypeFont*)montauk::malloc(sizeof(gui::TrueTypeFont));
if (!font) return nullptr;
if (!init_page_font(font, path)) {
montauk::mfree(font);
return nullptr;
}
return font;
}
static void free_page_font(gui::TrueTypeFont* font) {
if (!font) return;
for (int i = 0; i < font->cache_count; i++) {
for (int g = 0; g < 256; g++) {
if (font->caches[i].glyphs[g].bitmap) {
montauk::mfree(font->caches[i].glyphs[g].bitmap);
font->caches[i].glyphs[g].bitmap = nullptr;
}
}
}
if (font->data) {
montauk::free(font->data);
font->data = nullptr;
}
montauk::mfree(font);
}
static void destroy_page_fonts(PageFonts* fonts) {
if (!fonts) return;
free_page_font(fonts->medium);
free_page_font(fonts->bold);
fonts->medium = nullptr;
fonts->bold = nullptr;
}
static PageFonts load_page_fonts() {
PageFonts fonts = {};
fonts.medium = load_page_font(ROBOTO_MEDIUM_PATH);
fonts.bold = load_page_font(ROBOTO_BOLD_PATH);
return fonts;
}
static gui::TrueTypeFont* pick_font(PageFonts* fonts, bool bold) {
if (!fonts) return nullptr;
if (bold && fonts->bold && fonts->bold->valid) return fonts->bold;
if (fonts->medium && fonts->medium->valid) return fonts->medium;
if (fonts->bold && fonts->bold->valid) return fonts->bold;
return nullptr;
}
static int measure_text(uint8_t* rgb, PageFonts* fonts, bool bold,
int pixel_size, int fallback_scale, const char* text) {
(void)rgb;
if (!text) return 0;
gui::TrueTypeFont* font = pick_font(fonts, bold);
if (font && font->valid) return font->measure_text(text, pixel_size);
return (int)strlen(text) * 8 * fallback_scale;
}
static void draw_text(uint8_t* rgb, PageFonts* fonts, bool bold,
int x, int y, int pixel_size, int fallback_scale,
const char* text, uint8_t r, uint8_t g, uint8_t b) {
if (!rgb || !text) return;
gui::TrueTypeFont* font = pick_font(fonts, bold);
if (!font || !font->valid) {
draw_text_scaled(rgb, x, y, fallback_scale, text, r, g, b);
return;
}
gui::GlyphCache* gc = font->get_cache(pixel_size);
if (!gc) {
draw_text_scaled(rgb, x, y, fallback_scale, text, r, g, b);
return;
}
int cx = x;
int baseline = y + gc->ascent;
for (int i = 0; text[i] != '\0'; i++) {
gui::CachedGlyph* glyph = font->get_glyph(gc, (unsigned char)text[i]);
if (!glyph) continue;
if (glyph->bitmap) {
int gx = cx + glyph->xoff;
int gy = baseline + glyph->yoff;
for (int row = 0; row < glyph->height; row++) {
for (int col = 0; col < glyph->width; col++) {
uint8_t alpha = glyph->bitmap[row * glyph->width + col];
if (alpha == 0) continue;
blend_pixel(rgb, gx + col, gy + row, r, g, b, alpha);
}
}
}
cx += glyph->advance;
}
}
static void draw_centered_text(uint8_t* rgb, PageFonts* fonts, bool bold,
int y, int pixel_size, int fallback_scale,
const char* text, uint8_t r, uint8_t g, uint8_t b) {
if (!text) return;
int width = measure_text(rgb, fonts, bold, pixel_size, fallback_scale, text);
int x = (PAGE_W - width) / 2;
draw_text(rgb, fonts, bold, x, y, pixel_size, fallback_scale, text, r, g, b);
}
} // namespace
bool generate_test_page_jpeg(uint8_t** out_data, int* out_len) {
if (out_data) *out_data = nullptr;
if (out_len) *out_len = 0;
uint8_t* rgb = (uint8_t*)malloc((size_t)PAGE_W * PAGE_H * 3);
if (!rgb) return false;
memset(rgb, 0xFF, (size_t)PAGE_W * PAGE_H * 3);
PageFonts fonts = load_page_fonts();
draw_centered_text(rgb, &fonts, true, 50, 52, 3,
"Montauk Operating System", 0x22, 0x22, 0x22);
draw_centered_text(rgb, &fonts, false, 112, 26, 2,
"Printer Test Page for IPP Printers", 0x66, 0x66, 0x66);
fill_rect(rgb, 120, 170, PAGE_W - 240, 4, 0x29, 0x8F, 0x45);
fill_rect(rgb, 80, 220, PAGE_W - 160, 240, 0xFA, 0xFA, 0xFA);
draw_rect_outline(rgb, 80, 220, PAGE_W - 160, 240, 3, 0xD9, 0xD9, 0xD9);
char timestamp[32];
print::now_string(timestamp, sizeof(timestamp));
char line[128];
snprintf(line, sizeof(line), "Generated: %s", timestamp);
draw_text(rgb, &fonts, false, 120, 268, 30, 2, line, 0x22, 0x22, 0x22);
draw_text(rgb, &fonts, false, 120, 320, 30, 2, "Format: image/jpeg", 0x22, 0x22, 0x22);
draw_text(rgb, &fonts, false, 120, 372, 30, 2, "If you can read this, printing works.", 0x22, 0x22, 0x22);
draw_text(rgb, &fonts, true, 100, 530, 32, 2, "Color Check", 0x22, 0x22, 0x22);
draw_rect_outline(rgb, 100, 590, PAGE_W - 200, 150, 3, 0xD0, 0xD0, 0xD0);
fill_rect(rgb, 130, 625, 150, 72, 0x30, 0x30, 0x30);
fill_rect(rgb, 320, 625, 150, 72, 0x25, 0xA8, 0xD8);
fill_rect(rgb, 510, 625, 150, 72, 0xD8, 0x3C, 0x82);
fill_rect(rgb, 700, 625, 150, 72, 0xE9, 0xCF, 0x4A);
fill_rect(rgb, 890, 625, 150, 72, 0x47, 0x98, 0x5E);
draw_text(rgb, &fonts, false, 150, 706, 18, 1, "Black", 0x33, 0x33, 0x33);
draw_text(rgb, &fonts, false, 352, 706, 18, 1, "Cyan", 0x33, 0x33, 0x33);
draw_text(rgb, &fonts, false, 518, 706, 18, 1, "Magenta", 0x33, 0x33, 0x33);
draw_text(rgb, &fonts, false, 715, 706, 18, 1, "Yellow", 0x33, 0x33, 0x33);
draw_text(rgb, &fonts, false, 932, 706, 18, 1, "Green", 0x33, 0x33, 0x33);
draw_text(rgb, &fonts, true, 100, 792, 32, 2, "Grayscale", 0x22, 0x22, 0x22);
for (int i = 0; i < 10; i++) {
uint8_t v = (uint8_t)(i * 255 / 9);
fill_rect(rgb, 100 + i * ((PAGE_W - 200) / 10), 855,
(PAGE_W - 200) / 10, 72, v, v, v);
}
draw_rect_outline(rgb, 100, 855, PAGE_W - 200, 72, 3, 0xC8, 0xC8, 0xC8);
draw_text(rgb, &fonts, true, 100, 998, 32, 2, "Alignment", 0x22, 0x22, 0x22);
for (int i = 0; i < 5; i++) {
int yy = 1070 + i * 60;
fill_rect(rgb, 100, yy, PAGE_W - 200, 3, 0x11, 0x11, 0x11);
fill_rect(rgb, 100 + i * 40, yy - 15, 3, 33, 0x11, 0x11, 0x11);
}
JpegBuffer jpeg = {};
int ok = stbi_write_jpg_to_func(jpeg_write_callback, &jpeg, PAGE_W, PAGE_H, 3, rgb, 82);
destroy_page_fonts(&fonts);
free(rgb);
if (!ok || jpeg.failed || !jpeg.data || jpeg.size <= 0) {
free(jpeg.data);
return false;
}
if (out_data) *out_data = jpeg.data;
else free(jpeg.data);
if (out_len) *out_len = jpeg.size;
return true;
}
+5
View File
@@ -0,0 +1,5 @@
#pragma once
#include <stdint.h>
bool generate_test_page_jpeg(uint8_t** out_data, int* out_len);