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
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 | |
| 9 | namespace { |
| 10 | |
| 11 | ckey_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). |
| 20 | ckey_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. |
| 30 | ckey_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. |
| 37 | ckey_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 | |
| 43 | std::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. |
| 110 | const 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 | |
| 123 | double 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 | |
| 129 | double 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 | |
| 136 | struct 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 | |
| 143 | PreparedShape 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 | |
| 183 | bool 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 | |
| 214 | struct 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. |
| 224 | Coverage 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. |
| 273 | void 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). |
| 297 | double 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 | |
| 309 | struct Rgbaf { double r, g, b, a; }; |
| 310 | |
| 311 | Rgbaf 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}; } |
| 312 | Rgbaf rgbf(ckey_rgb c) { return {c.r / 255.0, c.g / 255.0, c.b / 255.0, 1.0}; } |
| 313 | // Straight-alpha "source over". |
| 314 | Rgbaf 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 | |
| 321 | void 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 | |
| 346 | ckey_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 | |
| 428 | uint32_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 | |
| 439 | struct 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 | |
| 446 | void 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 | |
| 453 | void 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 | |
| 464 | std::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 | |
| 496 | void 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 | |
| 505 | std::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 | |
| 542 | std::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. |
| 549 | void 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 | |
| 561 | void 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 | |
| 575 | std::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 | |
| 621 | extern "C" { |
| 622 | |
| 623 | ckey_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 | |
| 634 | void 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 | |
| 641 | ckey_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 | |
| 654 | ckey_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 | |
| 669 | ckey_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" |