Viewing is open to everyone. Downloading and starring need a GitHub sign-in. Sign in with GitHub

scalable_render.inc

v0.1.0-build.85 Latest Commit ede38f5

Size
40 KB
Lines
684
Language
cpp
SHA-256
41d4f3151c9b…
View on GitHub

Sign in with GitHub to download. Each download is recorded with your GitHub username.

1// Profile 0 geometry engine and its native backends: direct raster, PNG and
2// PDF (specification §2). Included by ckey.cpp after the Profile 0 layout and
3// the style checker; it uses their internal definitions.
4//
5// The scene builder is the canonical geometry. ckey_render_scalable_styled_svg
6// emits the same shapes as SVG text, and tests/render_tests.cpp checks that
7// rasterizing that SVG and rasterizing this scene give the same pixels.
8
9namespace {
10
11ckey_shape make_shape(ckey_shape_kind kind, ckey_layer layer, std::initializer_list<double> p, double width = 0,
12 bool round_cap = true, int32_t paint = CKEY_PAINT_INK) {
13 ckey_shape s{};
14 s.kind = kind; s.layer = layer; s.paint = paint; s.round_cap = round_cap ? 1u : 0u; s.width = width;
15 size_t i = 0; for (double v : p) if (i < 5) s.p[i++] = v;
16 return s;
17}
18
19// A payload or Grid Key module in its cell (same sizes as emit_module).
20ckey_shape module_shape(double cx, double cy, double p, ckey_module_shape shape, ckey_layer layer, int32_t paint) {
21 const double h = 0.34 * p;
22 switch (shape) {
23 case CKEY_MODULE_CIRCLE: return make_shape(CKEY_SHAPE_CIRCLE, layer, {cx, cy, h}, 0, true, paint);
24 case CKEY_MODULE_DIAMOND: return make_shape(CKEY_SHAPE_DIAMOND, layer, {cx, cy, 0.40 * p}, 0, true, paint);
25 default: return make_shape(CKEY_SHAPE_RECT, layer, {cx - h, cy - h, 2 * h, 2 * h, shape == CKEY_MODULE_SQUARE ? 0.0 : 0.16 * p}, 0, true, paint);
26 }
27}
28
29// A radial stroke at clock angle a (0 = 12:00, clockwise) from radius r0 to r1.
30ckey_shape ray_shape(double a, double r0, double r1, double width, bool round_cap, ckey_layer layer) {
31 return make_shape(CKEY_SHAPE_LINE, layer, {kCenter + r0 * std::sin(a), kCenter - r0 * std::cos(a), kCenter + r1 * std::sin(a), kCenter - r1 * std::cos(a)},
32 width, round_cap, CKEY_PAINT_INK);
33}
34
35// A run of ring sites, as svg_ring_run: slot positions from..to on a ring of
36// `slots`, the centerline shortened by one dot radius at each end for the caps.
37ckey_shape ring_run_shape(double radius, double from, double to, double slots, double dot, ckey_layer layer, int32_t paint) {
38 const double cap = dot / radius;
39 const double a0 = kTau * from / slots + cap, a1 = kTau * to / slots - cap;
40 return make_shape(CKEY_SHAPE_ARC, layer, {kCenter, kCenter, radius, a0 - kTau / 4, a1 - a0}, 2 * dot, true, paint);
41}
42
43std::vector<ckey_shape> build_scalable_scene(const ScalableLayout &L, const ckey_scalable_symbol &sym, const ckey_style &st) {
44 std::vector<ckey_shape> out;
45 const double p = L.pitch, dot = L.dot;
46 const bool round = st.rays != CKEY_RAY_FLAT;
47 out.reserve(L.nodes.size() + 64);
48 out.push_back(make_shape(CKEY_SHAPE_RECT_OUTLINE, CKEY_LAYER_FRAME, {0.5, 0.5, 28, 28, 3.5}, 0.65));
49 // Finders: one round-capped quarter arc per corner (radius 2.3 about the
50 // point 4 units in from each corner, 0.8 wide); the top-left arc is split
51 // by a 0.95-unit visible gap, as svg_corner_finders.
52 {
53 const double inset = 4.0, radius = 2.30, thickness = 0.80, gap = 0.95, side = 29.0;
54 const double half_gap = (gap + thickness) / (2.0 * radius), middle = 5.0 * kTau / 8.0;
55 const double a1 = middle - half_gap, a2 = middle + half_gap;
56 out.push_back(make_shape(CKEY_SHAPE_ARC, CKEY_LAYER_FINDERS, {inset, inset, radius, kTau / 2, a1 - kTau / 2}, thickness));
57 out.push_back(make_shape(CKEY_SHAPE_ARC, CKEY_LAYER_FINDERS, {inset, inset, radius, a2, 3 * kTau / 4 - a2}, thickness));
58 // Mirror images of the unsplit quarter arc (start pi, sweep +pi/2).
59 out.push_back(make_shape(CKEY_SHAPE_ARC, CKEY_LAYER_FINDERS, {side - inset, inset, radius, 0, -kTau / 4}, thickness));
60 out.push_back(make_shape(CKEY_SHAPE_ARC, CKEY_LAYER_FINDERS, {side - inset, side - inset, radius, 0, kTau / 4}, thickness));
61 out.push_back(make_shape(CKEY_SHAPE_ARC, CKEY_LAYER_FINDERS, {inset, side - inset, radius, kTau / 2, -kTau / 4}, thickness));
62 }
63 for (int r = 0; r < 2; ++r) for (int c = 0; c < 2; ++c)
64 out.push_back(module_shape((L.col0 + c + .5) * p, (L.row0 + r + .5) * p, p, st.modules, CKEY_LAYER_GRID_KEY, CKEY_PAINT_INK));
65 for (const auto &[slot, fraction] : {std::pair{0, 1.0}, std::pair{L.ray_key_half(), 0.5}, std::pair{L.ray_key_three(), 0.75}})
66 out.push_back(ray_shape(kTau * slot / L.dial_slots, kScalableDialInner, kScalableDialInner + L.key_ray() * fraction, 0.44 * p, round, CKEY_LAYER_POLAR_KEYS));
67 const double rk = L.ring_radius(L.rings - 1); const int k = L.ring_key_slot(); const double SK = L.ring_slots.back();
68 for (int slot : L.ring_key_dots()) {
69 const double a = kTau * (k + slot) / SK;
70 out.push_back(make_shape(CKEY_SHAPE_CIRCLE, CKEY_LAYER_POLAR_KEYS, {kCenter + rk * std::sin(a), kCenter - rk * std::cos(a), dot}));
71 }
72 out.push_back(ring_run_shape(rk, k + L.ring_key_run().first, k + L.ring_key_run().second, SK, dot, CKEY_LAYER_POLAR_KEYS, CKEY_PAINT_INK));
73
74 const size_t stride = static_cast<size_t>(L.ring_stride);
75 std::vector<uint8_t> ring_bits(static_cast<size_t>(L.rings) * stride, 0);
76 std::vector<int> ring_accent(static_cast<size_t>(L.rings) * stride, -1);
77 std::vector<ckey_shape> cartesian;
78 for (size_t i = 0; i < L.nodes.size(); ++i) {
79 const auto &n = L.nodes[i]; const bool bit = scalable_site_value(L, &sym, i);
80 const int accent = accent_of(n.id, st);
81 if (n.kind == 0) { if (bit) cartesian.push_back(module_shape(n.x, n.y, p, st.modules, CKEY_LAYER_DATA_CARTESIAN, accent >= 0 ? accent : CKEY_PAINT_INK)); }
82 else if (n.kind == 1) out.push_back(ray_shape(kTau * n.slot / L.dial_slots, kScalableDialInner, kScalableDialInner + (bit ? L.long_ray() : L.short_ray()), L.ray_width, round, CKEY_LAYER_DATA_POLAR));
83 else { ring_bits[static_cast<size_t>(n.ring) * stride + n.slot] = bit; ring_accent[static_cast<size_t>(n.ring) * stride + n.slot] = accent; }
84 }
85 for (int ring = 0; ring < L.rings; ++ring) {
86 const double radius = L.ring_radius(ring); const int S = L.ring_slots[static_cast<size_t>(ring)];
87 for (int slot = 0; slot < S;) {
88 const size_t at = static_cast<size_t>(ring) * stride + slot;
89 if (!ring_bits[at]) { ++slot; continue; }
90 int end = slot + 1;
91 if (st.rings == CKEY_RING_JOINED) while (end < S && ring_bits[static_cast<size_t>(ring) * stride + end]) ++end;
92 const int32_t paint = ring_accent[at] >= 0 ? ring_accent[at] : CKEY_PAINT_INK;
93 if (end - slot == 1) {
94 const double a = kTau * slot / S;
95 out.push_back(make_shape(CKEY_SHAPE_CIRCLE, CKEY_LAYER_DATA_POLAR, {kCenter + radius * std::sin(a), kCenter - radius * std::cos(a), dot}, 0, true, paint));
96 } else out.push_back(ring_run_shape(radius, slot - .5, end - .5, S, dot, CKEY_LAYER_DATA_POLAR, paint));
97 slot = end;
98 }
99 }
100 out.insert(out.end(), cartesian.begin(), cartesian.end());
101 if (st.logo_href && *st.logo_href) {
102 const double r = st.logo_radius > 0 ? st.logo_radius : logo_quiet_radius(L);
103 if (st.logo_plate) out.push_back(make_shape(CKEY_SHAPE_CIRCLE, CKEY_LAYER_LOGO_PLATE, {kCenter, kCenter, r + 0.1}, 0, true, CKEY_PAINT_BACKGROUND));
104 out.push_back(make_shape(CKEY_SHAPE_LOGO, CKEY_LAYER_LOGO, {kCenter, kCenter, r}, 0, true, CKEY_PAINT_INK));
105 }
106 return out;
107}
108
109// Validates arguments shared by every scene consumer and returns the layout.
110const ScalableLayout *scene_layout(const ckey_scalable_symbol *s, const ckey_style *style, ckey_style &st) {
111 if (!s || s->abi_version != CKEY_ABI_VERSION || s->version < 1 || s->version > CKEY_SCALABLE_VERSION_COUNT ||
112 s->ecc_level > 3 || s->layout >= static_cast<uint32_t>(kScalableFamilies)) return nullptr;
113 const ScalableLayout &L = scalable_layout(static_cast<int>(s->version), static_cast<int>(s->layout));
114 if (s->codeword_length != L.codeword_bytes) return nullptr;
115 if (style) st = effective_style(L, *s, *style); else ckey_style_default(&st);
116 double contrast = 0, load = 0;
117 if (!check_style(L, *s, st, contrast, load).empty()) return nullptr;
118 return &L;
119}
120
121// ---- Coverage tests, in canonical units (same rules as src/svg_raster.h) ----
122
123double sdf_round_rect(double px, double py, double x, double y, double w, double h, double r) {
124 const double cx = x + w / 2, cy = y + h / 2;
125 const double qx = std::abs(px - cx) - (w / 2 - r), qy = std::abs(py - cy) - (h / 2 - r);
126 return std::hypot(std::max(qx, 0.0), std::max(qy, 0.0)) + std::min(std::max(qx, qy), 0.0) - r;
127}
128
129double arc_angle_offset(double a, double from, double sweep) {
130 double d = a - from;
131 if (sweep >= 0) { while (d < 0) d += kTau; while (d >= kTau) d -= kTau; }
132 else { while (d > 0) d -= kTau; while (d <= -kTau) d += kTau; }
133 return d;
134}
135
136struct PreparedShape {
137 ckey_shape s;
138 double hw = 0; // half stroke width
139 double ex0 = 0, ey0 = 0, ex1 = 0, ey1 = 0; // arc endpoints
140 double bx0 = 0, by0 = 0, bx1 = 0, by1 = 0; // bounding box
141};
142
143PreparedShape prepare(const ckey_shape &shape, double min_feature) {
144 PreparedShape q; q.s = shape; ckey_shape &s = q.s;
145 if (min_feature > 0) { // HYBRID: widen marks thinner than the minimum feature
146 switch (s.kind) {
147 case CKEY_SHAPE_CIRCLE: if (s.layer != CKEY_LAYER_LOGO_PLATE) s.p[2] = std::max(s.p[2], min_feature / 2); break;
148 case CKEY_SHAPE_DIAMOND: s.p[2] = std::max(s.p[2], min_feature * 0.7071067811865476); break;
149 case CKEY_SHAPE_RECT: {
150 if (s.p[2] < min_feature) { s.p[0] -= (min_feature - s.p[2]) / 2; s.p[2] = min_feature; }
151 if (s.p[3] < min_feature) { s.p[1] -= (min_feature - s.p[3]) / 2; s.p[3] = min_feature; }
152 break;
153 }
154 case CKEY_SHAPE_RECT_OUTLINE: case CKEY_SHAPE_LINE: case CKEY_SHAPE_ARC: s.width = std::max(s.width, min_feature); break;
155 default: break;
156 }
157 }
158 q.hw = s.width / 2;
159 double x0 = 1e300, y0 = 1e300, x1 = -1e300, y1 = -1e300;
160 const auto add = [&](double x, double y, double pad) { x0 = std::min(x0, x - pad); y0 = std::min(y0, y - pad); x1 = std::max(x1, x + pad); y1 = std::max(y1, y + pad); };
161 switch (s.kind) {
162 case CKEY_SHAPE_RECT: add(s.p[0], s.p[1], 0); add(s.p[0] + s.p[2], s.p[1] + s.p[3], 0); break;
163 case CKEY_SHAPE_RECT_OUTLINE: add(s.p[0], s.p[1], q.hw); add(s.p[0] + s.p[2], s.p[1] + s.p[3], q.hw); break;
164 case CKEY_SHAPE_CIRCLE: case CKEY_SHAPE_DIAMOND: case CKEY_SHAPE_LOGO: add(s.p[0], s.p[1], s.p[2]); break;
165 case CKEY_SHAPE_LINE: add(s.p[0], s.p[1], q.hw); add(s.p[2], s.p[3], q.hw); break;
166 case CKEY_SHAPE_ARC: {
167 const double cx = s.p[0], cy = s.p[1], r = s.p[2], a = s.p[3], sw = s.p[4];
168 q.ex0 = cx + r * std::cos(a); q.ey0 = cy + r * std::sin(a);
169 q.ex1 = cx + r * std::cos(a + sw); q.ey1 = cy + r * std::sin(a + sw);
170 add(q.ex0, q.ey0, q.hw); add(q.ex1, q.ey1, q.hw);
171 for (int k = -8; k <= 8; ++k) {
172 const double e = k * kTau / 4;
173 if (std::abs(arc_angle_offset(e, a, sw)) <= std::abs(sw)) add(cx + r * std::cos(e), cy + r * std::sin(e), q.hw);
174 }
175 break;
176 }
177 default: break;
178 }
179 q.bx0 = x0; q.by0 = y0; q.bx1 = x1; q.by1 = y1;
180 return q;
181}
182
183bool shape_hit(const PreparedShape &q, double x, double y) {
184 const ckey_shape &s = q.s;
185 switch (s.kind) {
186 case CKEY_SHAPE_RECT: return sdf_round_rect(x, y, s.p[0], s.p[1], s.p[2], s.p[3], std::min(s.p[4], std::min(s.p[2], s.p[3]) / 2)) <= 0;
187 case CKEY_SHAPE_RECT_OUTLINE: return std::abs(sdf_round_rect(x, y, s.p[0], s.p[1], s.p[2], s.p[3], s.p[4])) <= q.hw;
188 case CKEY_SHAPE_CIRCLE: return std::hypot(x - s.p[0], y - s.p[1]) <= s.p[2];
189 case CKEY_SHAPE_DIAMOND: { // even-odd crossing test, so samples on an edge resolve as in the SVG reference
190 const double cx = s.p[0], cy = s.p[1], d = s.p[2];
191 const double vx[4] = {cx, cx + d, cx, cx - d}, vy[4] = {cy - d, cy, cy + d, cy};
192 bool inside = false;
193 for (int i = 0, j = 3; i < 4; j = i++)
194 if ((vy[i] > y) != (vy[j] > y) && x < (vx[j] - vx[i]) * (y - vy[i]) / (vy[j] - vy[i]) + vx[i]) inside = !inside;
195 return inside;
196 }
197 case CKEY_SHAPE_LINE: {
198 const double vx = s.p[2] - s.p[0], vy = s.p[3] - s.p[1], len2 = vx * vx + vy * vy;
199 const double t = len2 > 0 ? ((x - s.p[0]) * vx + (y - s.p[1]) * vy) / len2 : 0;
200 if (!s.round_cap && (t < 0 || t > 1)) return false;
201 const double c = std::clamp(t, 0.0, 1.0);
202 return std::hypot(s.p[0] + c * vx - x, s.p[1] + c * vy - y) <= q.hw;
203 }
204 case CKEY_SHAPE_ARC: {
205 const double d = arc_angle_offset(std::atan2(y - s.p[1], x - s.p[0]), s.p[3], s.p[4]);
206 if (std::abs(d) <= std::abs(s.p[4])) return std::abs(std::hypot(x - s.p[0], y - s.p[1]) - s.p[2]) <= q.hw;
207 if (!s.round_cap) return false;
208 return std::hypot(x - q.ex0, y - q.ey0) <= q.hw || std::hypot(x - q.ex1, y - q.ey1) <= q.hw;
209 }
210 default: return false;
211 }
212}
213
214struct Coverage {
215 int side = 0, samples = 0;
216 std::vector<uint8_t> ink; // covered samples per pixel (0..samples)
217 std::vector<int8_t> paint; // last paint drawn into the pixel
218};
219
220// Rasterizes a scene's ink into per-pixel sample counts. Sample positions
221// are the centres of an ss x ss grid in each pixel, the same as the
222// reference SVG rasterizer, so the two backends agree sample for sample.
223// Rows are processed in bands so the per-sample masks stay small.
224Coverage rasterize_scene(const std::vector<ckey_shape> &scene, int side, int ss, double min_feature_units) {
225 Coverage cov; cov.side = side; cov.samples = ss * ss;
226 cov.ink.assign(static_cast<size_t>(side) * side, 0);
227 cov.paint.assign(static_cast<size_t>(side) * side, CKEY_PAINT_INK);
228 std::vector<PreparedShape> shapes;
229 shapes.reserve(scene.size());
230 for (const auto &s : scene) if (s.kind != CKEY_SHAPE_LOGO) shapes.push_back(prepare(s, min_feature_units));
231 const int W = side * ss;
232 const double k = 31.0 / W; // canonical units per sample
233 constexpr int kBandPixels = 32;
234 const int band_samples = kBandPixels * ss;
235 std::vector<uint8_t> hit(static_cast<size_t>(W) * band_samples);
236 std::vector<int8_t> paint(static_cast<size_t>(W) * band_samples);
237 for (int band = 0; band * kBandPixels < side; ++band) {
238 const int sy0 = band * band_samples, sy1 = std::min(side * ss, sy0 + band_samples) - 1;
239 std::fill(hit.begin(), hit.end(), 0);
240 const double uy0 = -1 + sy0 * k, uy1 = -1 + (sy1 + 1) * k;
241 for (const auto &q : shapes) {
242 if (q.by1 < uy0 - k || q.by0 > uy1 + k) continue;
243 const int x0 = std::max(0, static_cast<int>(std::floor((q.bx0 + 1) / k)) - 1);
244 const int x1 = std::min(W - 1, static_cast<int>(std::ceil((q.bx1 + 1) / k)) + 1);
245 const int y0 = std::max(sy0, static_cast<int>(std::floor((q.by0 + 1) / k)) - 1);
246 const int y1 = std::min(sy1, static_cast<int>(std::ceil((q.by1 + 1) / k)) + 1);
247 const bool erase = q.s.paint == CKEY_PAINT_BACKGROUND;
248 for (int y = y0; y <= y1; ++y) {
249 const double uy = -1 + (y + .5) * k;
250 uint8_t *row = &hit[static_cast<size_t>(y - sy0) * W];
251 int8_t *prow = &paint[static_cast<size_t>(y - sy0) * W];
252 for (int x = x0; x <= x1; ++x) {
253 if (erase) { if (row[x] && shape_hit(q, -1 + (x + .5) * k, uy)) row[x] = 0; }
254 else if (!row[x] && shape_hit(q, -1 + (x + .5) * k, uy)) { row[x] = 1; prow[x] = static_cast<int8_t>(q.s.paint); }
255 }
256 }
257 }
258 const int py0 = band * kBandPixels, py1 = std::min(side, py0 + kBandPixels);
259 for (int py = py0; py < py1; ++py) for (int px = 0; px < side; ++px) {
260 int count = 0, accent = CKEY_PAINT_INK;
261 for (int sy = 0; sy < ss; ++sy) for (int sx = 0; sx < ss; ++sx) {
262 const size_t at = static_cast<size_t>((py - py0) * ss + sy) * W + px * ss + sx;
263 if (hit[at]) { ++count; if (paint[at] >= 0) accent = paint[at]; }
264 }
265 cov.ink[static_cast<size_t>(py) * side + px] = static_cast<uint8_t>(count);
266 cov.paint[static_cast<size_t>(py) * side + px] = static_cast<int8_t>(accent);
267 }
268 }
269 return cov;
270}
271
272// Separable box blur, three passes approximating a Gaussian of sigma pixels.
273void blur(std::vector<float> &img, int side, double sigma) {
274 const int r = std::max(1, static_cast<int>(std::lround(sigma * 0.8660254))); // 3 box passes: sigma^2 = r(r+1)
275 std::vector<float> tmp(img.size());
276 for (int pass = 0; pass < 3; ++pass) {
277 for (int y = 0; y < side; ++y) { // horizontal
278 double acc = 0; const size_t row = static_cast<size_t>(y) * side;
279 for (int x = -r; x <= r; ++x) acc += img[row + std::clamp(x, 0, side - 1)];
280 for (int x = 0; x < side; ++x) {
281 tmp[row + x] = static_cast<float>(acc / (2 * r + 1));
282 acc += img[row + std::clamp(x + r + 1, 0, side - 1)] - img[row + std::clamp(x - r, 0, side - 1)];
283 }
284 }
285 for (int x = 0; x < side; ++x) { // vertical
286 double acc = 0;
287 for (int y = -r; y <= r; ++y) acc += tmp[static_cast<size_t>(std::clamp(y, 0, side - 1)) * side + x];
288 for (int y = 0; y < side; ++y) {
289 img[static_cast<size_t>(y) * side + x] = static_cast<float>(acc / (2 * r + 1));
290 acc += tmp[static_cast<size_t>(std::clamp(y + r + 1, 0, side - 1)) * side + x] - tmp[static_cast<size_t>(std::clamp(y - r, 0, side - 1)) * side + x];
291 }
292 }
293 }
294}
295
296// Deterministic value noise for the background grain (non-normative paint).
297double grain_noise(double x, double y) {
298 const auto lattice = [](int ix, int iy) { return site_hash(static_cast<uint32_t>(ix) * 0x9e3779b1u ^ static_cast<uint32_t>(iy) * 0x85ebca77u ^ 0x47524149u) / 4294967295.0; };
299 double total = 0, amp = 0.5;
300 for (int octave = 0; octave < 2; ++octave, x *= 2, y *= 2, amp /= 2) {
301 const int ix = static_cast<int>(std::floor(x)), iy = static_cast<int>(std::floor(y));
302 const double fx = x - ix, fy = y - iy;
303 const double a = lattice(ix, iy), b = lattice(ix + 1, iy), c = lattice(ix, iy + 1), d = lattice(ix + 1, iy + 1);
304 total += amp * ((a * (1 - fx) + b * fx) * (1 - fy) + (c * (1 - fx) + d * fx) * fy);
305 }
306 return total / 0.75;
307}
308
309struct Rgbaf { double r, g, b, a; };
310
311Rgbaf mix(const Rgbaf &a, const Rgbaf &b, double t) { return {a.r + (b.r - a.r) * t, a.g + (b.g - a.g) * t, a.b + (b.b - a.b) * t, a.a + (b.a - a.a) * t}; }
312Rgbaf rgbf(ckey_rgb c) { return {c.r / 255.0, c.g / 255.0, c.b / 255.0, 1.0}; }
313// Straight-alpha "source over".
314Rgbaf over(const Rgbaf &src, double src_alpha, const Rgbaf &dst) {
315 const double a = src_alpha + dst.a * (1 - src_alpha);
316 if (a <= 0) return {0, 0, 0, 0};
317 return {(src.r * src_alpha + dst.r * dst.a * (1 - src_alpha)) / a, (src.g * src_alpha + dst.g * dst.a * (1 - src_alpha)) / a,
318 (src.b * src_alpha + dst.b * dst.a * (1 - src_alpha)) / a, a};
319}
320
321void draw_logo(std::vector<Rgbaf> &px, int side, const ckey_logo_image &logo, double cx, double cy, double r) {
322 if (!logo.rgba || !logo.width || !logo.height) return;
323 const size_t stride = logo.stride ? logo.stride : static_cast<size_t>(logo.width) * 4;
324 const double scale = 2 * r / std::max(logo.width, logo.height); // "meet": fit inside the circle's box
325 const double w = logo.width * scale, h = logo.height * scale, x0 = cx - w / 2, y0 = cy - h / 2;
326 const double unit = 31.0 / side;
327 for (int py = 0; py < side; ++py) for (int pxi = 0; pxi < side; ++pxi) {
328 const double ux = -1 + (pxi + .5) * unit, uy = -1 + (py + .5) * unit;
329 if (std::hypot(ux - cx, uy - cy) > r || ux < x0 || uy < y0 || ux >= x0 + w || uy >= y0 + h) continue;
330 const double sx = (ux - x0) / scale - .5, sy = (uy - y0) / scale - .5;
331 const int ix = static_cast<int>(std::floor(sx)), iy = static_cast<int>(std::floor(sy));
332 const double fx = sx - ix, fy = sy - iy;
333 Rgbaf acc{0, 0, 0, 0};
334 for (int dy = 0; dy < 2; ++dy) for (int dx = 0; dx < 2; ++dx) {
335 const int X = std::clamp(ix + dx, 0, static_cast<int>(logo.width) - 1), Y = std::clamp(iy + dy, 0, static_cast<int>(logo.height) - 1);
336 const uint8_t *s = logo.rgba + static_cast<size_t>(Y) * stride + static_cast<size_t>(X) * 4;
337 const double wgt = (dx ? fx : 1 - fx) * (dy ? fy : 1 - fy), a = s[3] / 255.0;
338 acc.r += wgt * a * s[0] / 255.0; acc.g += wgt * a * s[1] / 255.0; acc.b += wgt * a * s[2] / 255.0; acc.a += wgt * a;
339 }
340 if (acc.a <= 0) continue;
341 Rgbaf &d = px[static_cast<size_t>(py) * side + pxi];
342 d = over({acc.r / acc.a, acc.g / acc.a, acc.b / acc.a, 1}, acc.a, d);
343 }
344}
345
346ckey_status render_raster(const ScalableLayout &L, const ckey_scalable_symbol &sym, const ckey_style &st,
347 const ckey_raster_options &o, uint8_t *out) {
348 const int side = static_cast<int>(o.size_px), ss = static_cast<int>(o.supersample);
349 const double unit = 31.0 / side; // canonical units per pixel
350 const auto scene = build_scalable_scene(L, sym, st);
351 const Coverage cov = rasterize_scene(scene, side, ss, o.policy == CKEY_RASTER_HYBRID ? o.min_feature_px * unit : 0.0);
352 const size_t n = static_cast<size_t>(side) * side;
353 std::vector<float> alpha(n);
354 for (size_t i = 0; i < n; ++i) {
355 const int c = cov.ink[i];
356 alpha[i] = o.policy == CKEY_RASTER_BINARY ? (2 * c >= cov.samples ? 1.0f : 0.0f) : static_cast<float>(c) / cov.samples;
357 }
358 const bool materials = o.materials != 0, transparent = o.transparent != 0;
359 const bool sheen = materials && !same_colour(st.ink, st.ink_shade);
360 const bool vignette = materials && !same_colour(st.background, st.background_edge);
361 // Depth: drop shadow and embossed highlight, from the ink coverage.
362 std::vector<float> shadow, highlight;
363 if (materials && st.shadow > 0) {
364 shadow.assign(n, 0.f);
365 const int dx = static_cast<int>(std::lround(0.05 / unit)), dy = static_cast<int>(std::lround(0.09 / unit));
366 for (int y = 0; y < side; ++y) for (int x = 0; x < side; ++x) {
367 const int sx = x - dx, sy = y - dy;
368 if (sx >= 0 && sy >= 0 && sx < side && sy < side) shadow[static_cast<size_t>(y) * side + x] = alpha[static_cast<size_t>(sy) * side + sx];
369 }
370 blur(shadow, side, 0.07 / unit);
371 for (auto &v : shadow) v = static_cast<float>(v * 0.75 * st.shadow);
372 }
373 if (materials && st.bevel > 0) {
374 std::vector<float> bump = alpha;
375 blur(bump, side, std::max(0.6, 0.045 / unit));
376 highlight.assign(n, 0.f);
377 // Light from azimuth 225 deg (upper left), elevation 40 deg; Blinn
378 // half-vector specular with exponent 16, scaled like the SVG filter
379 // at its reference 8 pixels per unit.
380 const double el = 40.0 * kTau / 360, az = 225.0 * kTau / 360;
381 const double lx = std::cos(az) * std::cos(el), ly = std::sin(az) * std::cos(el), lz = std::sin(el);
382 double hx = lx, hy = ly, hz = lz + 1; const double hn = std::sqrt(hx * hx + hy * hy + hz * hz); hx /= hn; hy /= hn; hz /= hn;
383 const double surface = 3.0 * (8.0 * unit);
384 for (int y = 1; y + 1 < side; ++y) for (int x = 1; x + 1 < side; ++x) {
385 const size_t i = static_cast<size_t>(y) * side + x;
386 const double gx = (bump[i + 1] - bump[i - 1]) / (2 * unit) * unit * surface / unit / 8.0 * 8.0;
387 const double gy = (bump[i + side] - bump[i - side]) / (2 * unit) * unit * surface / unit / 8.0 * 8.0;
388 double nx = -gx, ny = -gy, nz = 1; const double nn = std::sqrt(nx * nx + ny * ny + nz * nz); nx /= nn; ny /= nn; nz /= nn;
389 const double ndoth = std::max(0.0, nx * hx + ny * hy + nz * hz);
390 highlight[i] = static_cast<float>(std::min(1.0, 1.1 * st.bevel * std::pow(ndoth, 16)) * alpha[i]);
391 }
392 }
393 std::vector<Rgbaf> px(n);
394 const Rgbaf bg_centre = rgbf(st.background), bg_edge = rgbf(st.background_edge), ink = rgbf(st.ink), shade = rgbf(st.ink_shade);
395 for (int y = 0; y < side; ++y) for (int x = 0; x < side; ++x) {
396 const size_t i = static_cast<size_t>(y) * side + x;
397 const double ux = -1 + (x + .5) * unit, uy = -1 + (y + .5) * unit;
398 Rgbaf c{0, 0, 0, 0};
399 if (!transparent) {
400 c = vignette ? mix(bg_centre, bg_edge, std::clamp(std::hypot(ux - kCenter, uy - kCenter) / 21.0, 0.0, 1.0)) : bg_centre;
401 if (materials && st.texture > 0) { const double g = grain_noise(ux * 1.7 * 4, uy * 1.7 * 4); c = mix(c, {g, g, g, 1}, 0.18 * st.texture); }
402 }
403 if (!shadow.empty() && shadow[i] > 0) c = over({0, 0, 0, 1}, shadow[i], c);
404 if (alpha[i] > 0) {
405 Rgbaf m;
406 if (cov.paint[i] >= 0) m = rgbf(st.accents[cov.paint[i]]);
407 else if (sheen) {
408 const double t = std::clamp((ux + uy) / 58.0, 0.0, 1.0);
409 m = t < 0.55 ? mix(ink, shade, t / 0.55) : mix(shade, ink, (t - 0.55) / 0.45);
410 } else m = ink;
411 if (!highlight.empty()) { const double h = highlight[i] / std::max(1e-6f, alpha[i]); m = {std::min(1.0, m.r + h), std::min(1.0, m.g + h), std::min(1.0, m.b + h), 1}; }
412 c = over(m, alpha[i], c);
413 }
414 px[i] = c;
415 }
416 if (o.logo) for (const auto &s : scene) if (s.kind == CKEY_SHAPE_LOGO) draw_logo(px, side, *o.logo, s.p[0], s.p[1], s.p[2]);
417 const auto byte = [](double v) { return static_cast<uint8_t>(std::lround(std::clamp(v, 0.0, 1.0) * 255.0)); };
418 for (size_t i = 0; i < n; ++i) {
419 const Rgbaf &c = px[i];
420 if (o.format == CKEY_PIXELS_GRAY8) out[i] = byte(0.2126 * c.r + 0.7152 * c.g + 0.0722 * c.b);
421 else { out[4 * i] = byte(c.r); out[4 * i + 1] = byte(c.g); out[4 * i + 2] = byte(c.b); out[4 * i + 3] = byte(c.a); }
422 }
423 return CKEY_OK;
424}
425
426// ---- PNG: zlib stream with fixed-Huffman DEFLATE and greedy LZ77 ----
427
428uint32_t crc32_ieee(const uint8_t *data, size_t len, uint32_t crc = 0) {
429 static const auto table = [] {
430 std::array<uint32_t, 256> t{};
431 for (uint32_t i = 0; i < 256; ++i) { uint32_t c = i; for (int k = 0; k < 8; ++k) c = c & 1 ? 0xedb88320u ^ (c >> 1) : c >> 1; t[i] = c; }
432 return t;
433 }();
434 crc = ~crc;
435 for (size_t i = 0; i < len; ++i) crc = table[(crc ^ data[i]) & 0xff] ^ (crc >> 8);
436 return ~crc;
437}
438
439struct BitWriter {
440 std::vector<uint8_t> out; uint32_t acc = 0; int bits = 0;
441 void put(uint32_t value, int count) { acc |= value << bits; bits += count; while (bits >= 8) { out.push_back(static_cast<uint8_t>(acc)); acc >>= 8; bits -= 8; } }
442 void put_reversed(uint32_t code, int count) { uint32_t r = 0; for (int i = 0; i < count; ++i) r |= ((code >> i) & 1u) << (count - 1 - i); put(r, count); }
443 void flush() { if (bits) out.push_back(static_cast<uint8_t>(acc)); acc = 0; bits = 0; }
444};
445
446void put_literal(BitWriter &w, int lit) { // RFC 1951 §3.2.6 fixed codes
447 if (lit < 144) w.put_reversed(0x30 + lit, 8);
448 else if (lit < 256) w.put_reversed(0x190 + lit - 144, 9);
449 else if (lit < 280) w.put_reversed(lit - 256, 7);
450 else w.put_reversed(0xc0 + lit - 280, 8);
451}
452
453void put_match(BitWriter &w, int length, int distance) {
454 static constexpr int lbase[29] = {3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258};
455 static constexpr int lextra[29] = {0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0};
456 static constexpr int dbase[30] = {1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145, 8193, 12289, 16385, 24577};
457 static constexpr int dextra[30] = {0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13};
458 int li = 28; while (lbase[li] > length) --li;
459 put_literal(w, 257 + li); w.put(static_cast<uint32_t>(length - lbase[li]), lextra[li]);
460 int di = 29; while (dbase[di] > distance) --di;
461 w.put_reversed(static_cast<uint32_t>(di), 5); w.put(static_cast<uint32_t>(distance - dbase[di]), dextra[di]);
462}
463
464std::vector<uint8_t> zlib_compress(const std::vector<uint8_t> &data) {
465 BitWriter w; w.out = {0x78, 0x01};
466 w.put(1, 1); w.put(1, 2); // final block, fixed Huffman
467 constexpr int kWindow = 32768, kHashBits = 15, kChain = 32;
468 std::vector<int> head(1 << kHashBits, -1), prev(data.size(), -1);
469 const auto hash = [&](size_t i) { return ((data[i] << 10) ^ (data[i + 1] << 5) ^ data[i + 2]) & ((1 << kHashBits) - 1); };
470 size_t i = 0;
471 while (i < data.size()) {
472 int best_len = 0, best_dist = 0;
473 if (i + 2 < data.size()) {
474 const int h = hash(i);
475 int candidate = head[h], chain = 0;
476 while (candidate >= 0 && static_cast<int>(i) - candidate <= kWindow && chain++ < kChain) {
477 int len = 0;
478 while (len < 258 && i + len < data.size() && data[candidate + len] == data[i + len]) ++len;
479 if (len > best_len) { best_len = len; best_dist = static_cast<int>(i) - candidate; if (len == 258) break; }
480 candidate = prev[candidate];
481 }
482 }
483 const size_t step = best_len >= 3 ? static_cast<size_t>(best_len) : 1;
484 if (best_len >= 3) put_match(w, best_len, best_dist); else put_literal(w, data[i]);
485 for (size_t j = 0; j < step; ++j, ++i) if (i + 2 < data.size()) { const int h = hash(i); prev[i] = head[h]; head[h] = static_cast<int>(i); }
486 }
487 put_literal(w, 256);
488 w.flush();
489 uint32_t a = 1, b = 0;
490 for (uint8_t v : data) { a = (a + v) % 65521; b = (b + a) % 65521; }
491 const uint32_t adler = (b << 16) | a;
492 for (int s = 24; s >= 0; s -= 8) w.out.push_back(static_cast<uint8_t>(adler >> s));
493 return w.out;
494}
495
496void png_chunk(std::vector<uint8_t> &png, const char *type, const std::vector<uint8_t> &body) {
497 const auto be32 = [&](uint32_t v) { for (int s = 24; s >= 0; s -= 8) png.push_back(static_cast<uint8_t>(v >> s)); };
498 be32(static_cast<uint32_t>(body.size()));
499 const size_t start = png.size();
500 png.insert(png.end(), type, type + 4);
501 png.insert(png.end(), body.begin(), body.end());
502 be32(crc32_ieee(png.data() + start, png.size() - start));
503}
504
505std::vector<uint8_t> encode_png(const uint8_t *px, uint32_t w, uint32_t h, size_t stride, ckey_png_format format, double dpi) {
506 const int channels = format == CKEY_PNG_RGBA8 ? 4 : 1;
507 const size_t row_bytes = format == CKEY_PNG_GRAY1 ? (w + 7) / 8 : static_cast<size_t>(w) * channels;
508 std::vector<uint8_t> raw; raw.reserve((row_bytes + 1) * h);
509 std::vector<uint8_t> line(row_bytes), above(row_bytes, 0);
510 for (uint32_t y = 0; y < h; ++y) {
511 const uint8_t *src = px + static_cast<size_t>(y) * stride;
512 if (format == CKEY_PNG_GRAY1) {
513 std::fill(line.begin(), line.end(), 0);
514 for (uint32_t x = 0; x < w; ++x) if (src[x] >= 128) line[x / 8] |= static_cast<uint8_t>(0x80u >> (x % 8));
515 } else std::memcpy(line.data(), src, row_bytes);
516 // Filter: Up (2) for continuous images compresses flat regions well;
517 // None (0) for 1-bit rows.
518 if (format == CKEY_PNG_GRAY1) { raw.push_back(0); raw.insert(raw.end(), line.begin(), line.end()); }
519 else { raw.push_back(2); for (size_t i = 0; i < row_bytes; ++i) raw.push_back(static_cast<uint8_t>(line[i] - above[i])); }
520 above = line;
521 }
522 std::vector<uint8_t> png = {0x89, 'P', 'N', 'G', 0x0d, 0x0a, 0x1a, 0x0a};
523 std::vector<uint8_t> ihdr;
524 const auto be32 = [](std::vector<uint8_t> &v, uint32_t x) { for (int s = 24; s >= 0; s -= 8) v.push_back(static_cast<uint8_t>(x >> s)); };
525 be32(ihdr, w); be32(ihdr, h);
526 ihdr.push_back(format == CKEY_PNG_GRAY1 ? 1 : 8);
527 ihdr.push_back(format == CKEY_PNG_RGBA8 ? 6 : 0);
528 ihdr.push_back(0); ihdr.push_back(0); ihdr.push_back(0);
529 png_chunk(png, "IHDR", ihdr);
530 if (dpi > 0) {
531 std::vector<uint8_t> phys; const uint32_t ppm = static_cast<uint32_t>(std::lround(dpi / 0.0254));
532 be32(phys, ppm); be32(phys, ppm); phys.push_back(1);
533 png_chunk(png, "pHYs", phys);
534 }
535 png_chunk(png, "IDAT", zlib_compress(raw));
536 png_chunk(png, "IEND", {});
537 return png;
538}
539
540// ---- PDF ----
541
542std::string pdf_num(double v) {
543 char b[32]; std::snprintf(b, sizeof(b), "%.4f", v);
544 std::string s = b; while (s.size() > 1 && s.back() == '0') s.pop_back(); if (s.back() == '.') s.pop_back();
545 return s == "-0" ? "0" : s;
546}
547
548// Cubic Bézier approximation of a circular arc, in at most 90-degree pieces.
549void pdf_arc_path(std::ostringstream &o, const std::function<std::string(double, double)> &pt, double cx, double cy, double r, double a0, double sweep, bool move) {
550 const int pieces = std::max(1, static_cast<int>(std::ceil(std::abs(sweep) / (kTau / 4) - 1e-9)));
551 const double step = sweep / pieces, k = 4.0 / 3.0 * std::tan(step / 4);
552 double a = a0;
553 if (move) o << pt(cx + r * std::cos(a), cy + r * std::sin(a)) << " m\n";
554 for (int i = 0; i < pieces; ++i, a += step) {
555 const double b = a + step;
556 const double x0 = cx + r * std::cos(a), y0 = cy + r * std::sin(a), x3 = cx + r * std::cos(b), y3 = cy + r * std::sin(b);
557 o << pt(x0 - k * r * std::sin(a), y0 + k * r * std::cos(a)) << ' ' << pt(x3 + k * r * std::sin(b), y3 - k * r * std::cos(b)) << ' ' << pt(x3, y3) << " c\n";
558 }
559}
560
561void pdf_round_rect(std::ostringstream &o, const std::function<std::string(double, double)> &pt, double x, double y, double w, double h, double r) {
562 r = std::min(r, std::min(w, h) / 2);
563 if (r <= 0) { o << pt(x, y) << " m " << pt(x + w, y) << " l " << pt(x + w, y + h) << " l " << pt(x, y + h) << " l h\n"; return; }
564 o << pt(x + r, y) << " m " << pt(x + w - r, y) << " l\n";
565 pdf_arc_path(o, pt, x + w - r, y + r, r, -kTau / 4, kTau / 4, false);
566 o << pt(x + w, y + h - r) << " l\n";
567 pdf_arc_path(o, pt, x + w - r, y + h - r, r, 0, kTau / 4, false);
568 o << pt(x + r, y + h) << " l\n";
569 pdf_arc_path(o, pt, x + r, y + h - r, r, kTau / 4, kTau / 4, false);
570 o << pt(x, y + r) << " l\n";
571 pdf_arc_path(o, pt, x + r, y + r, r, kTau / 2, kTau / 4, false);
572 o << "h\n";
573}
574
575std::string render_pdf(const std::vector<ckey_shape> &scene, const ckey_style &st, double width_pt, bool transparent) {
576 const double s = width_pt / 31.0; // points per canonical unit; PDF y grows up
577 const auto pt = [&](double x, double y) { return pdf_num((x + 1) * s) + ' ' + pdf_num((30 - y) * s); };
578 const auto colour = [](ckey_rgb c, const char *op) { return pdf_num(c.r / 255.0) + ' ' + pdf_num(c.g / 255.0) + ' ' + pdf_num(c.b / 255.0) + ' ' + op + '\n'; };
579 std::ostringstream o;
580 if (!transparent) o << colour(st.background, "rg") << "0 0 " << pdf_num(width_pt) << ' ' << pdf_num(width_pt) << " re f\n";
581 o << "1 J 1 j\n";
582 for (const auto &sh : scene) {
583 if (sh.kind == CKEY_SHAPE_LOGO) continue;
584 const ckey_rgb c = sh.paint >= 0 ? st.accents[sh.paint] : sh.paint == CKEY_PAINT_BACKGROUND ? st.background : st.ink;
585 if (sh.paint == CKEY_PAINT_BACKGROUND && transparent) continue; // nothing to erase onto: plate omitted
586 const bool stroke = sh.kind == CKEY_SHAPE_RECT_OUTLINE || sh.kind == CKEY_SHAPE_LINE || sh.kind == CKEY_SHAPE_ARC;
587 o << colour(c, stroke ? "RG" : "rg");
588 if (stroke) o << pdf_num(sh.width * s) << " w " << (sh.round_cap || sh.kind == CKEY_SHAPE_RECT_OUTLINE ? 1 : 0) << " J\n";
589 switch (sh.kind) {
590 case CKEY_SHAPE_RECT: pdf_round_rect(o, pt, sh.p[0], sh.p[1], sh.p[2], sh.p[3], sh.p[4]); o << "f\n"; break;
591 case CKEY_SHAPE_RECT_OUTLINE: pdf_round_rect(o, pt, sh.p[0], sh.p[1], sh.p[2], sh.p[3], sh.p[4]); o << "S\n"; break;
592 case CKEY_SHAPE_CIRCLE: pdf_arc_path(o, pt, sh.p[0], sh.p[1], sh.p[2], 0, kTau, true); o << "h f\n"; break;
593 case CKEY_SHAPE_DIAMOND: {
594 const double x = sh.p[0], y = sh.p[1], d = sh.p[2];
595 o << pt(x, y - d) << " m " << pt(x + d, y) << " l " << pt(x, y + d) << " l " << pt(x - d, y) << " l h f\n"; break;
596 }
597 case CKEY_SHAPE_LINE: o << pt(sh.p[0], sh.p[1]) << " m " << pt(sh.p[2], sh.p[3]) << " l S\n"; break;
598 case CKEY_SHAPE_ARC: pdf_arc_path(o, pt, sh.p[0], sh.p[1], sh.p[2], sh.p[3], sh.p[4], true); o << "S\n"; break;
599 default: break;
600 }
601 }
602 const std::string content = o.str();
603 std::vector<std::string> objects = {
604 "<< /Type /Catalog /Pages 2 0 R >>",
605 "<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
606 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 " + pdf_num(width_pt) + ' ' + pdf_num(width_pt) + "] /Contents 4 0 R /Resources << >> >>",
607 "<< /Length " + std::to_string(content.size()) + " >>\nstream\n" + content + "endstream",
608 "<< /Producer (CKey " CKEY_SOFTWARE_VERSION ") >>"};
609 std::string pdf = "%PDF-1.4\n%\xE2\xE3\xCF\xD3\n";
610 std::vector<size_t> offsets;
611 for (size_t i = 0; i < objects.size(); ++i) { offsets.push_back(pdf.size()); pdf += std::to_string(i + 1) + " 0 obj\n" + objects[i] + "\nendobj\n"; }
612 const size_t xref = pdf.size();
613 pdf += "xref\n0 " + std::to_string(objects.size() + 1) + "\n0000000000 65535 f \n";
614 for (size_t off : offsets) { char b[24]; std::snprintf(b, sizeof(b), "%010zu 00000 n \n", off); pdf += b; }
615 pdf += "trailer\n<< /Size " + std::to_string(objects.size() + 1) + " /Root 1 0 R /Info 5 0 R >>\nstartxref\n" + std::to_string(xref) + "\n%%EOF\n";
616 return pdf;
617}
618
619} // namespace
620
621extern "C" {
622
623ckey_status ckey_scalable_geometry(const ckey_scalable_symbol *s, const ckey_style *style, ckey_shape *out, size_t cap, size_t *count) {
624 ckey_style st{};
625 const ScalableLayout *L = count ? scene_layout(s, style, st) : nullptr;
626 if (!L) return CKEY_INVALID_ARGUMENT;
627 const auto scene = build_scalable_scene(*L, *s, st);
628 *count = scene.size();
629 if (!out || cap < scene.size()) return CKEY_INSUFFICIENT_BUFFER;
630 std::copy(scene.begin(), scene.end(), out);
631 return CKEY_OK;
632}
633
634void ckey_raster_options_default(ckey_raster_options *o) {
635 if (!o) return;
636 *o = ckey_raster_options{};
637 o->abi_version = CKEY_ABI_VERSION; o->size_px = 512; o->policy = CKEY_RASTER_COVERAGE; o->format = CKEY_PIXELS_RGBA8;
638 o->transparent = 0; o->supersample = 4; o->min_feature_px = 1.0; o->materials = 1; o->logo = nullptr;
639}
640
641ckey_status ckey_render_scalable_raster(const ckey_scalable_symbol *s, const ckey_style *style, const ckey_raster_options *o,
642 uint8_t *out, size_t cap, size_t *required) {
643 if (!o || !required || o->abi_version != CKEY_ABI_VERSION || o->size_px < 16 || o->size_px > 16384 || o->policy > CKEY_RASTER_HYBRID ||
644 o->format > CKEY_PIXELS_GRAY8 || o->supersample < 1 || o->supersample > 8 || !(o->min_feature_px > 0) || !std::isfinite(o->min_feature_px) ||
645 (o->transparent && o->format == CKEY_PIXELS_GRAY8)) return CKEY_INVALID_ARGUMENT;
646 ckey_style st{};
647 const ScalableLayout *L = scene_layout(s, style, st);
648 if (!L) return CKEY_INVALID_ARGUMENT;
649 *required = static_cast<size_t>(o->size_px) * o->size_px * (o->format == CKEY_PIXELS_RGBA8 ? 4 : 1);
650 if (!out || cap < *required) return CKEY_INSUFFICIENT_BUFFER;
651 try { return render_raster(*L, *s, st, *o, out); } catch (...) { return CKEY_INTERNAL_ERROR; }
652}
653
654ckey_status ckey_encode_png(const uint8_t *px, uint32_t w, uint32_t h, size_t stride, ckey_png_format format, double dpi,
655 uint8_t *out, size_t cap, size_t *required) {
656 if (!px || !required || w == 0 || h == 0 || w > 65535 || h > 65535 || format > CKEY_PNG_GRAY1 || !std::isfinite(dpi) || dpi < 0) return CKEY_INVALID_ARGUMENT;
657 const size_t min_stride = static_cast<size_t>(w) * (format == CKEY_PNG_RGBA8 ? 4 : 1);
658 if (stride == 0) stride = min_stride;
659 if (stride < min_stride) return CKEY_INVALID_ARGUMENT;
660 try {
661 const auto png = encode_png(px, w, h, stride, format, dpi);
662 *required = png.size();
663 if (!out || cap < png.size()) return CKEY_INSUFFICIENT_BUFFER;
664 std::memcpy(out, png.data(), png.size());
665 return CKEY_OK;
666 } catch (...) { return CKEY_INTERNAL_ERROR; }
667}
668
669ckey_status ckey_render_scalable_pdf(const ckey_scalable_symbol *s, const ckey_style *style, double width_pt, uint32_t transparent,
670 uint8_t *out, size_t cap, size_t *required) {
671 if (!required || !std::isfinite(width_pt) || width_pt < 1 || width_pt > 14400) return CKEY_INVALID_ARGUMENT;
672 ckey_style st{};
673 const ScalableLayout *L = scene_layout(s, style, st);
674 if (!L) return CKEY_INVALID_ARGUMENT;
675 try {
676 const std::string pdf = render_pdf(build_scalable_scene(*L, *s, st), st, width_pt, transparent != 0);
677 *required = pdf.size();
678 if (!out || cap < pdf.size()) return CKEY_INSUFFICIENT_BUFFER;
679 std::memcpy(out, pdf.data(), pdf.size());
680 return CKEY_OK;
681 } catch (...) { return CKEY_INTERNAL_ERROR; }
682}
683
684} // extern "C"