Viewing is open to everyone. Downloading and starring need a GitHub sign-in.
Sign in with GitHub
api_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 | // Public C ABI: render functions. Included after core_internal.inc. |
| 2 | |
| 3 | extern "C" { |
| 4 | |
| 5 | ckey_status ckey_render_svg(const ckey_symbol *s, uint32_t scale, char *out, |
| 6 | size_t cap, size_t *required) { |
| 7 | if (!s || !required || scale == 0 || scale > CKEY_MAX_RENDER_SCALE || s->abi_version != CKEY_ABI_VERSION || s->side != kSide) return CKEY_INVALID_ARGUMENT; |
| 8 | const auto &nodes = data_nodes(); |
| 9 | if (nodes.size() < kBits) return CKEY_INTERNAL_ERROR; |
| 10 | std::ostringstream o; o << std::fixed << std::setprecision(4); |
| 11 | std::ostringstream c; c << std::fixed << std::setprecision(4); // Cartesian payload layer |
| 12 | o << "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"" << 31 * scale << "\" height=\"" << 31 * scale << "\" viewBox=\"-1 -1 31 31\" role=\"img\" aria-label=\"CKey locator symbol\">" |
| 13 | << "<rect x=\"-1\" y=\"-1\" width=\"31\" height=\"31\" fill=\"#ffffff\"/><g fill=\"#000000\" stroke=\"#000000\">" |
| 14 | << "<g class=\"ckey-frame\"><rect x=\"0.5\" y=\"0.5\" width=\"28\" height=\"28\" rx=\"3.5\" fill=\"none\" stroke-width=\"0.65\"/></g>" |
| 15 | << "<g class=\"ckey-finders\">"; |
| 16 | svg_corner_finders(o, 29.0, 4.0, {2.30, 0.0, 0.0}, .80, .95); |
| 17 | // Grid Key: fixed 2x2 Cartesian reference block at the left matrix edge. |
| 18 | o << "</g><g class=\"ckey-grid-key\">"; |
| 19 | for (int r : {11, 12}) for (int c : {2, 3}) o << "<rect x=\"" << c + .13 << "\" y=\"" << r + .13 << "\" width=\"0.74\" height=\"0.74\" rx=\"0.16\" stroke=\"none\"/>"; |
| 20 | o << "</g><g class=\"ckey-polar-keys\">"; |
| 21 | // Permanent Ray Key is contained in the outer dial band (r=7.6..9.5). |
| 22 | for (const auto &[slot, fraction] : {std::pair{0, 1.0}, {54, .5}, {55, .75}}) { |
| 23 | const double a = kTau * slot / 72.0, r0 = 7.6, r1 = r0 + 1.9 * fraction; |
| 24 | o << "<path d=\"M " << kCenter + r0 * std::sin(a) << ' ' << kCenter - r0 * std::cos(a) << " L " << kCenter + r1 * std::sin(a) << ' ' << kCenter - r1 * std::cos(a) |
| 25 | << "\" fill=\"none\" stroke=\"#000000\" stroke-width=\"0.22\"/>"; |
| 26 | } |
| 27 | // Permanent Ring Key on the outermost inner data ring. |
| 28 | const double rr = 6.0; |
| 29 | for (int slot : {18, 19, 21}) { const double a = kTau * slot / 72.0; svg_circle(o, kCenter + rr * std::sin(a), kCenter - rr * std::cos(a), polar_dot_radius()); } |
| 30 | const double ring_key_dot_diameter = 2.0 * polar_dot_radius(); |
| 31 | // Round caps add one dot diameter to the visible length. Shorten the arc |
| 32 | // centerline accordingly so its total visible length remains four slots. |
| 33 | const double cap_angle = polar_dot_radius() / rr; |
| 34 | const double a0 = kTau * 21.5 / 72.0 + cap_angle; |
| 35 | const double a1 = kTau * 25.5 / 72.0 - cap_angle; |
| 36 | o << "<path d=\"M " << kCenter + rr * std::sin(a0) << ' ' << kCenter - rr * std::cos(a0) << " A " << rr << ' ' << rr << " 0 0 1 " |
| 37 | << kCenter + rr * std::sin(a1) << ' ' << kCenter - rr * std::cos(a1) << "\" fill=\"none\" stroke=\"#000000\" stroke-width=\"" << ring_key_dot_diameter << "\" stroke-linecap=\"round\"/>"; |
| 38 | o << "</g><g class=\"ckey-data-polar\">"; |
| 39 | for (size_t i = 0; i < kBits; ++i) { |
| 40 | const Node &n = nodes[i]; |
| 41 | if (n.dial) { |
| 42 | const double a = kTau * n.slot / 72.0; |
| 43 | // Every data-bearing dial slot is marked. A short ray encodes 0; |
| 44 | // a long ray encodes 1. Both remain distinct from reserved keys. |
| 45 | const double length = code_bit(s, i) ? 1.35 : 0.65; |
| 46 | o << "<path d=\"M " << kCenter + 7.6 * std::sin(a) << ' ' << kCenter - 7.6 * std::cos(a) << " L " << kCenter + (7.6 + length) * std::sin(a) << ' ' << kCenter - (7.6 + length) * std::cos(a) |
| 47 | << "\" fill=\"none\" stroke=\"#000000\" stroke-width=\"0.15\" stroke-linecap=\"round\"/>"; |
| 48 | continue; |
| 49 | } |
| 50 | if (!code_bit(s, i)) continue; |
| 51 | if (n.polar) svg_circle(o, n.x, n.y, polar_dot_radius()); |
| 52 | else c << "<rect x=\"" << n.col + .13 << "\" y=\"" << n.row + .13 << "\" width=\"0.74\" height=\"0.74\" rx=\"0.16\" stroke=\"none\"/>"; |
| 53 | } |
| 54 | o << "</g><g class=\"ckey-data-cartesian\">" << c.str() << "</g></g></svg>"; |
| 55 | const std::string text = o.str(); *required = text.size() + 1; |
| 56 | if (!out || cap < *required) return CKEY_INSUFFICIENT_BUFFER; |
| 57 | std::memcpy(out, text.c_str(), *required); |
| 58 | return CKEY_OK; |
| 59 | } |
| 60 | |
| 61 | ckey_status ckey_render_text_svg(const ckey_text_symbol *s, uint32_t scale, char *out, |
| 62 | size_t cap, size_t *required) { |
| 63 | if (!s || !required || scale == 0 || scale > CKEY_MAX_RENDER_SCALE || |
| 64 | s->abi_version != CKEY_ABI_VERSION || s->side != kSide || s->payload_length > CKEY_TEXT_PAYLOAD_MAX_BYTES) return CKEY_INVALID_ARGUMENT; |
| 65 | const auto &nodes = text_nodes(); |
| 66 | if (nodes.size() < kTextCodewordBytes * 8) return CKEY_INTERNAL_ERROR; |
| 67 | std::ostringstream o; o << std::fixed << std::setprecision(4); |
| 68 | std::ostringstream c; c << std::fixed << std::setprecision(4); // Cartesian payload layer |
| 69 | o << "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"" << 31 * scale << "\" height=\"" << 31 * scale << "\" viewBox=\"-1 -1 31 31\" role=\"img\" aria-label=\"CKey text symbol profile 2\">" |
| 70 | << "<rect x=\"-1\" y=\"-1\" width=\"31\" height=\"31\" fill=\"#ffffff\"/><g fill=\"#000000\" stroke=\"#000000\">" |
| 71 | << "<g class=\"ckey-frame\"><rect x=\"0.5\" y=\"0.5\" width=\"28\" height=\"28\" rx=\"3.5\" fill=\"none\" stroke-width=\"0.65\"/></g>" |
| 72 | << "<g class=\"ckey-finders\">"; |
| 73 | svg_corner_finders(o, 29.0, 4.0, {2.30, 0.0, 0.0}, .80, .95); |
| 74 | o << "</g><g class=\"ckey-grid-key\">"; |
| 75 | for (int r : {11,12}) for (int c : {2,3}) o << "<rect x=\"" << c+.13 << "\" y=\"" << r+.13 << "\" width=\"0.74\" height=\"0.74\" rx=\"0.16\" stroke=\"none\"/>"; |
| 76 | o << "</g><g class=\"ckey-polar-keys\">"; |
| 77 | for (const auto &[slot, fraction] : {std::pair{0,1.0}, std::pair{108,.5}, std::pair{110,.75}}) { |
| 78 | const double a=kTau*slot/144.0, r0=7.6, r1=r0+1.9*fraction; |
| 79 | o << "<path d=\"M " << kCenter+r0*std::sin(a) << ' ' << kCenter-r0*std::cos(a) << " L " << kCenter+r1*std::sin(a) << ' ' << kCenter-r1*std::cos(a) << "\" fill=\"none\" stroke-width=\"0.22\" stroke-linecap=\"round\"/>"; |
| 80 | } |
| 81 | constexpr double key_radius=6.41, dot_radius=.10; |
| 82 | for (int slot : {36,38,42}) { const double a=kTau*slot/144.0; svg_circle(o,kCenter+key_radius*std::sin(a),kCenter-key_radius*std::cos(a),dot_radius); } |
| 83 | svg_ring_run(o,kCenter,key_radius,43.5,51.5,144.0,dot_radius,"0.2000"); |
| 84 | o << "</g><g class=\"ckey-data-polar\">"; |
| 85 | std::array<std::array<bool,kTextSlots>,kTextRings> ring_bits{}; |
| 86 | for (size_t i=0;i<nodes.size();++i) { |
| 87 | const auto &n=nodes[i]; const bool bit=text_site_value(s,i); |
| 88 | if (n.cart) { if (bit) c << "<rect x=\"" << n.x-.17 << "\" y=\"" << n.y-.17 << "\" width=\"0.34\" height=\"0.34\" rx=\"0.08\" stroke=\"none\"/>"; } |
| 89 | else if (n.dial) { const double a=kTau*n.slot/144.0, length=bit?1.20:.52; o << "<path d=\"M " << kCenter+7.6*std::sin(a) << ' ' << kCenter-7.6*std::cos(a) << " L " << kCenter+(7.6+length)*std::sin(a) << ' ' << kCenter-(7.6+length)*std::cos(a) << "\" fill=\"none\" stroke-width=\"0.1000\" stroke-linecap=\"round\"/>"; } |
| 90 | else ring_bits[n.ring][n.slot]=bit; |
| 91 | } |
| 92 | for (int ring=0;ring<kTextRings;++ring) { |
| 93 | const double radius=3.4+.43*ring; |
| 94 | for (int slot=0;slot<kTextSlots;) { |
| 95 | if (!ring_bits[ring][slot]) { ++slot; continue; } |
| 96 | int end=slot+1; while (end<kTextSlots && ring_bits[ring][end]) ++end; |
| 97 | if (end-slot==1) { const double a=kTau*slot/kTextSlots; svg_circle(o,kCenter+radius*std::sin(a),kCenter-radius*std::cos(a),dot_radius); } |
| 98 | else svg_ring_run(o,kCenter,radius,slot-.5,end-.5,kTextSlots,dot_radius,"0.2000"); |
| 99 | slot=end; |
| 100 | } |
| 101 | } |
| 102 | o << "</g><g class=\"ckey-data-cartesian\">" << c.str() << "</g></g></svg>"; |
| 103 | const std::string text=o.str(); *required=text.size()+1; |
| 104 | if (!out || cap<*required) return CKEY_INSUFFICIENT_BUFFER; |
| 105 | std::memcpy(out,text.c_str(),*required); return CKEY_OK; |
| 106 | } |
| 107 | |
| 108 | ckey_status ckey_render_extended_svg(const ckey_extended_symbol *s, uint32_t scale, char *out, |
| 109 | size_t cap, size_t *required) { |
| 110 | if (!s || !required || scale == 0 || scale > CKEY_MAX_RENDER_SCALE || s->abi_version != CKEY_ABI_VERSION || s->side != kExtendedSide || s->payload_length == 0 || s->payload_length > CKEY_EXTENDED_PAYLOAD_MAX_BYTES) return CKEY_INVALID_ARGUMENT; |
| 111 | const auto &nodes=extended_nodes(); if (nodes.size()<kExtendedBits) return CKEY_INTERNAL_ERROR; |
| 112 | constexpr double center=58.0, dot_radius=.075; |
| 113 | std::ostringstream o; o << std::fixed << std::setprecision(4); |
| 114 | std::ostringstream c; c << std::fixed << std::setprecision(4); // Cartesian payload layer |
| 115 | o << "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"" << 124*scale << "\" height=\"" << 124*scale << "\" viewBox=\"-4 -4 124 124\" role=\"img\" aria-label=\"CKey extended symbol profile 3\">" |
| 116 | << "<rect x=\"-4\" y=\"-4\" width=\"124\" height=\"124\" fill=\"#ffffff\"/><g fill=\"#000000\" stroke=\"#000000\">" |
| 117 | << "<g class=\"ckey-frame\"><rect x=\"2\" y=\"2\" width=\"112\" height=\"112\" rx=\"14\" fill=\"none\" stroke-width=\"2.6\"/></g>" |
| 118 | << "<g class=\"ckey-finders\">"; |
| 119 | svg_corner_finders(o, 116.0, 16.0, {9.20, 0.0, 0.0}, 3.2, 3.8); |
| 120 | // Fixed Grid Key: four ordinary 0.34-unit Cartesian modules on the same |
| 121 | // 0.5-unit pitch as payload, so it calibrates payload pitch and contrast. |
| 122 | o << "</g><g class=\"ckey-grid-key\">"; |
| 123 | for (int r=0;r<2;++r) for (int c=0;c<2;++c) o << "<rect x=\"" << 8.08+c*.5 << "\" y=\"" << 45.08+r*.5 << "\" width=\".34\" height=\".34\" rx=\".08\" stroke=\"none\"/>"; |
| 124 | // Fixed Ray Key: 12:00 full; 9:00 half; 9:05 three-quarter length. |
| 125 | o << "</g><g class=\"ckey-polar-keys\">"; |
| 126 | for (const auto &[slot,fraction] : {std::pair{0,1.0},std::pair{432,.5},std::pair{440,.75}}) { |
| 127 | const double a=kTau*slot/576.0,r0=26.30,r1=r0+3.6*fraction; |
| 128 | o << "<path d=\"M " << center+r0*std::sin(a) << ' ' << center-r0*std::cos(a) << " L " << center+r1*std::sin(a) << ' ' << center-r1*std::cos(a) << "\" fill=\"none\" stroke-width=\".28\" stroke-linecap=\"round\"/>"; |
| 129 | } |
| 130 | // Fixed Ring Key: two adjacent dots, one empty slot, one dot, then a |
| 131 | // continuous four-slot arc of equal angular occupancy. |
| 132 | constexpr double key_r=kExtendedRingKeyRadius; |
| 133 | for (int slot : {72,73,75}) { const double a=kTau*slot/kExtendedRingSlots; svg_circle(o,center+key_r*std::sin(a),center-key_r*std::cos(a),dot_radius); } |
| 134 | svg_ring_run(o,center,key_r,75.5,79.5,kExtendedRingSlots,dot_radius,".1500"); |
| 135 | o << "</g><g class=\"ckey-data-polar\">"; |
| 136 | std::array<std::array<bool,kExtendedRingSlots>,kExtendedRings> ring_bits{}; std::array<std::array<bool,3>,kExtendedRaySlots> ray_bits{}; |
| 137 | for (size_t i=0;i<nodes.size();++i) { |
| 138 | const auto &n=nodes[i]; const bool bit=extended_site_value(s,i); |
| 139 | if (n.kind==ExtendedKind::cart) { if (bit) c << "<rect x=\"" << n.x-.17 << "\" y=\"" << n.y-.17 << "\" width=\".34\" height=\".34\" rx=\".08\" stroke=\"none\"/>"; } |
| 140 | else if (n.kind==ExtendedKind::ring) ring_bits[n.ring][n.slot]=bit; |
| 141 | else ray_bits[n.slot][n.component]=bit; |
| 142 | } |
| 143 | // The ray alphabet is (Gray length, width): 4 calibrated lengths x 2 |
| 144 | // calibrated weights, three site values per ray. No state is an absent ray. |
| 145 | for (int slot=0;slot<kExtendedRaySlots;++slot) { |
| 146 | if (extended_reserved_ray(slot)) continue; |
| 147 | const int gray=(ray_bits[slot][0]?2:0)|(ray_bits[slot][1]?1:0), level=gray^(gray>>1); |
| 148 | const double a=kTau*slot/576.0,r0=26.30,length=.80+.85*level,width=ray_bits[slot][2]?.28:.16; |
| 149 | o << "<path d=\"M " << center+r0*std::sin(a) << ' ' << center-r0*std::cos(a) << " L " << center+(r0+length)*std::sin(a) << ' ' << center-(r0+length)*std::cos(a) << "\" fill=\"none\" stroke-width=\"" << width << "\" stroke-linecap=\"round\"/>"; |
| 150 | } |
| 151 | for (int ring=0;ring<kExtendedRings;++ring) { |
| 152 | const double radius=kExtendedRingInner+kExtendedRingPitch*ring; |
| 153 | for (int slot=0;slot<kExtendedRingSlots;) { |
| 154 | if (!ring_bits[ring][slot]) { ++slot; continue; } |
| 155 | int end=slot+1; while(end<kExtendedRingSlots && ring_bits[ring][end]) ++end; |
| 156 | if (end-slot==1) { const double a=kTau*slot/kExtendedRingSlots; svg_circle(o,center+radius*std::sin(a),center-radius*std::cos(a),dot_radius); } |
| 157 | else svg_ring_run(o,center,radius,slot-.5,end-.5,kExtendedRingSlots,dot_radius,".1500"); |
| 158 | slot=end; |
| 159 | } |
| 160 | } |
| 161 | o << "</g><g class=\"ckey-data-cartesian\">" << c.str() << "</g></g></svg>"; const std::string text=o.str(); *required=text.size()+1; |
| 162 | if (!out || cap<*required) return CKEY_INSUFFICIENT_BUFFER; |
| 163 | std::memcpy(out,text.c_str(),*required); |
| 164 | return CKEY_OK; |
| 165 | } |
| 166 | |
| 167 | void ckey_style_default(ckey_style *st) { if (st) fill_default_style(*st); } |
| 168 | |
| 169 | ckey_status ckey_style_preset_fill(ckey_style *st, ckey_style_preset preset) { |
| 170 | if (!st || (preset != CKEY_PRESET_PLAIN && preset != CKEY_PRESET_CONCEPT)) return CKEY_INVALID_ARGUMENT; |
| 171 | ckey_style_default(st); |
| 172 | if (preset == CKEY_PRESET_CONCEPT) { |
| 173 | st->background = {0x14, 0x16, 0x1e}; st->background_edge = {0x07, 0x08, 0x0c}; |
| 174 | st->ink = {0xf6, 0xee, 0xe0}; st->ink_shade = {0xc8, 0xb6, 0x9a}; |
| 175 | st->accents[0] = {0xa9, 0xb8, 0x96}; st->accents[1] = {0x9e, 0xa1, 0xa6}; st->accent_count = 2; st->accent_percent = 18; |
| 176 | st->bevel = 0.6; st->shadow = 0.5; st->texture = 0.4; |
| 177 | } |
| 178 | return CKEY_OK; |
| 179 | } |
| 180 | |
| 181 | double ckey_scalable_logo_max_radius(uint32_t version, ckey_scalable_layout layout) { |
| 182 | return version >= 1 && version <= CKEY_SCALABLE_VERSION_COUNT && static_cast<int>(layout) >= 0 && static_cast<int>(layout) < kScalableFamilies |
| 183 | ? logo_max_radius(scalable_layout(static_cast<int>(version), static_cast<int>(layout))) : -1.0; |
| 184 | } |
| 185 | |
| 186 | double ckey_scalable_logo_load(const ckey_scalable_symbol *s, double radius) { |
| 187 | if (!s || s->version < 1 || s->version > CKEY_SCALABLE_VERSION_COUNT || s->ecc_level > 3 || s->layout >= kScalableFamilies || !std::isfinite(radius) || radius < 0) return -1.0; |
| 188 | return logo_load(scalable_layout(static_cast<int>(s->version), static_cast<int>(s->layout)), static_cast<int>(s->ecc_level), radius); |
| 189 | } |
| 190 | |
| 191 | ckey_status ckey_check_scalable_style(const ckey_scalable_symbol *s, const ckey_style *st, ckey_diagnostics *diag) { |
| 192 | if (!s || !st || s->version < 1 || s->version > CKEY_SCALABLE_VERSION_COUNT || s->ecc_level > 3 || s->layout >= kScalableFamilies) { |
| 193 | set_status(diag, CKEY_INVALID_ARGUMENT, "style check needs a Profile 0 symbol and a style"); return CKEY_INVALID_ARGUMENT; |
| 194 | } |
| 195 | double contrast = 0, load = 0; |
| 196 | const ScalableLayout &L = scalable_layout(static_cast<int>(s->version), static_cast<int>(s->layout)); |
| 197 | const std::string structural = structural_problem(L, *st); |
| 198 | if (!structural.empty()) { set_status(diag, CKEY_INVALID_ARGUMENT, structural.c_str()); return CKEY_INVALID_ARGUMENT; } |
| 199 | ckey_style eff = *st; uint32_t adjusted = 0; |
| 200 | if (st->policy == CKEY_POLICY_ADAPTIVE && !safety_problem(L, *s, eff, {}, contrast, load).empty()) adjusted = adapt_style(L, *s, eff, {}); |
| 201 | std::string advice; |
| 202 | const std::string problem = safety_problem(L, *s, eff, {}, contrast, load, &advice); |
| 203 | char message[96]; |
| 204 | if (!problem.empty()) { |
| 205 | if (st->policy != CKEY_POLICY_OVERRIDE) { set_status(diag, CKEY_INVALID_ARGUMENT, problem.c_str()); return CKEY_INVALID_ARGUMENT; } |
| 206 | std::snprintf(message, sizeof(message), "override, NOT scan-safe: %s", problem.c_str()); |
| 207 | set_status(diag, CKEY_OK, message); |
| 208 | return CKEY_OK; |
| 209 | } |
| 210 | if (!advice.empty()) std::snprintf(message, sizeof(message), "style OK%s, but %s; certify it to check that it scans", adjusted ? " after adaptive changes" : "", advice.c_str()); |
| 211 | else std::snprintf(message, sizeof(message), "style OK%s: contrast >= %.1f:1; logo uses %.0f%% of block correction", |
| 212 | adjusted ? " after adaptive changes" : "", contrast, 100 * load); |
| 213 | set_status(diag, CKEY_OK, message); |
| 214 | return CKEY_OK; |
| 215 | } |
| 216 | |
| 217 | ckey_status ckey_render_scalable_svg(const ckey_scalable_symbol *s, uint32_t scale, char *out, size_t cap, size_t *required) { |
| 218 | ckey_style st; ckey_style_default(&st); |
| 219 | return ckey_render_scalable_styled_svg(s, &st, scale, out, cap, required); |
| 220 | } |
| 221 | |
| 222 | ckey_status ckey_render_scalable_styled_svg(const ckey_scalable_symbol *s, const ckey_style *style_in, uint32_t scale, |
| 223 | char *out, size_t cap, size_t *required) { |
| 224 | if (!s || !style_in || !required || scale == 0 || scale > CKEY_MAX_RENDER_SCALE || s->abi_version != CKEY_ABI_VERSION || |
| 225 | s->version < 1 || s->version > CKEY_SCALABLE_VERSION_COUNT || s->layout >= kScalableFamilies) return CKEY_INVALID_ARGUMENT; |
| 226 | const ScalableLayout &L = scalable_layout(static_cast<int>(s->version), static_cast<int>(s->layout)); |
| 227 | if (s->codeword_length != L.codeword_bytes) return CKEY_INVALID_ARGUMENT; |
| 228 | const ckey_style eff = effective_style(L, *s, *style_in); |
| 229 | const ckey_style *st = &eff; |
| 230 | double contrast = 0, load = 0; |
| 231 | if (!check_style(L, *s, eff, contrast, load).empty()) return CKEY_INVALID_ARGUMENT; |
| 232 | const double p = L.pitch, dot = L.dot; |
| 233 | const std::string dot_stroke = fixed4(2 * dot), cap_style = st->rays == CKEY_RAY_FLAT ? "butt" : "round"; |
| 234 | const bool sheen = !same_colour(st->ink, st->ink_shade), vignette = !same_colour(st->background, st->background_edge); |
| 235 | const bool depth = st->bevel > 0 || st->shadow > 0; |
| 236 | const std::string ink = sheen ? "url(#ckey-ink)" : hex_colour(st->ink); |
| 237 | std::ostringstream o; o << std::fixed << std::setprecision(4); |
| 238 | std::ostringstream c; c << std::fixed << std::setprecision(4); |
| 239 | o << "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"" << 31 * scale << "\" height=\"" << 31 * scale << "\" viewBox=\"-1 -1 31 31\" role=\"img\" aria-label=\"CKey scalable symbol profile 0" << (L.family ? ".1" : "") << " version " << s->version << "\">"; |
| 240 | // ---- Materials ---- |
| 241 | if (sheen || vignette || depth || st->texture > 0 || (st->logo_href && *st->logo_href)) { |
| 242 | o << "<defs>"; |
| 243 | if (sheen) o << "<linearGradient id=\"ckey-ink\" gradientUnits=\"userSpaceOnUse\" x1=\"0\" y1=\"0\" x2=\"29\" y2=\"29\"><stop offset=\"0\" stop-color=\"" << hex_colour(st->ink) |
| 244 | << "\"/><stop offset=\"0.55\" stop-color=\"" << hex_colour(st->ink_shade) << "\"/><stop offset=\"1\" stop-color=\"" << hex_colour(st->ink) << "\"/></linearGradient>"; |
| 245 | if (vignette) o << "<radialGradient id=\"ckey-bg\" gradientUnits=\"userSpaceOnUse\" cx=\"14.5\" cy=\"14.5\" r=\"21\"><stop offset=\"0\" stop-color=\"" << hex_colour(st->background) |
| 246 | << "\"/><stop offset=\"1\" stop-color=\"" << hex_colour(st->background_edge) << "\"/></radialGradient>"; |
| 247 | if (st->texture > 0) o << "<filter id=\"ckey-grain\" x=\"0\" y=\"0\" width=\"100%\" height=\"100%\"><feTurbulence type=\"fractalNoise\" baseFrequency=\"1.7\" numOctaves=\"2\" seed=\"7\"/><feColorMatrix type=\"saturate\" values=\"0\"/></filter>"; |
| 248 | if (depth) { |
| 249 | o << "<filter id=\"ckey-depth\" x=\"-2%\" y=\"-2%\" width=\"104%\" height=\"104%\" color-interpolation-filters=\"sRGB\">"; |
| 250 | std::string base = "SourceGraphic"; |
| 251 | if (st->shadow > 0) { o << "<feDropShadow dx=\"0.05\" dy=\"0.09\" stdDeviation=\"0.07\" flood-color=\"#000000\" flood-opacity=\"" << 0.75 * st->shadow << "\" result=\"shadowed\"/>"; base = "shadowed"; } |
| 252 | if (st->bevel > 0) |
| 253 | o << "<feGaussianBlur in=\"SourceAlpha\" stdDeviation=\"0.045\" result=\"bump\"/><feSpecularLighting in=\"bump\" surfaceScale=\"3\" specularConstant=\"" << 1.1 * st->bevel |
| 254 | << "\" specularExponent=\"16\" lighting-color=\"#ffffff\" result=\"spec\"><feDistantLight azimuth=\"225\" elevation=\"40\"/></feSpecularLighting>" |
| 255 | << "<feComposite in=\"spec\" in2=\"SourceAlpha\" operator=\"in\" result=\"shine\"/><feComposite in=\"shine\" in2=\"" << base << "\" operator=\"arithmetic\" k1=\"0\" k2=\"1\" k3=\"1\" k4=\"0\"/>"; |
| 256 | o << "</filter>"; |
| 257 | } |
| 258 | if (st->logo_href && *st->logo_href) { |
| 259 | const double r = st->logo_radius > 0 ? st->logo_radius : logo_quiet_radius(L); |
| 260 | o << "<clipPath id=\"ckey-logo-clip\"><circle cx=\"14.5\" cy=\"14.5\" r=\"" << r << "\"/></clipPath>"; |
| 261 | } |
| 262 | o << "</defs>"; |
| 263 | } |
| 264 | o << "<rect x=\"-1\" y=\"-1\" width=\"31\" height=\"31\" fill=\"" << (vignette ? "url(#ckey-bg)" : hex_colour(st->background)) << "\"/>"; |
| 265 | if (st->texture > 0) o << "<rect x=\"-1\" y=\"-1\" width=\"31\" height=\"31\" filter=\"url(#ckey-grain)\" opacity=\"" << 0.18 * st->texture << "\"/>"; |
| 266 | o << "<g fill=\"" << ink << "\" stroke=\"" << ink << "\"" << (depth ? " filter=\"url(#ckey-depth)\"" : "") << ">" |
| 267 | << "<g class=\"ckey-frame\"><rect x=\"0.5\" y=\"0.5\" width=\"28\" height=\"28\" rx=\"3.5\" fill=\"none\" stroke-width=\"0.65\"/></g>" |
| 268 | << "<g class=\"ckey-finders\">"; |
| 269 | svg_corner_finders(o, 29.0, 4.0, {2.30, 0.0, 0.0}, .80, .95); |
| 270 | o << "</g><g class=\"ckey-grid-key\">"; |
| 271 | for (int r = 0; r < 2; ++r) for (int cc = 0; cc < 2; ++cc) emit_module(o, (L.col0 + cc + .5) * p, (L.row0 + r + .5) * p, p, st->modules, ""); |
| 272 | o << "</g><g class=\"ckey-polar-keys\">"; |
| 273 | 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}}) { |
| 274 | const double a = kTau * slot / L.dial_slots, r0 = kScalableDialInner, r1 = r0 + L.key_ray() * fraction; |
| 275 | o << "<path d=\"M " << kCenter + r0 * std::sin(a) << ' ' << kCenter - r0 * std::cos(a) << " L " << kCenter + r1 * std::sin(a) << ' ' << kCenter - r1 * std::cos(a) |
| 276 | << "\" fill=\"none\" stroke-width=\"" << 0.44 * p << "\" stroke-linecap=\"" << cap_style << "\"/>"; |
| 277 | } |
| 278 | // The Ring Key keeps its dot/gap/run pattern in every style: it is a key. |
| 279 | const double rk = L.ring_radius(L.rings - 1); const int k = L.ring_key_slot(); const double SK = L.ring_slots.back(); |
| 280 | for (int slot : L.ring_key_dots()) { const double a = kTau * (k + slot) / SK; svg_circle(o, kCenter + rk * std::sin(a), kCenter - rk * std::cos(a), dot); } |
| 281 | svg_ring_run(o, kCenter, rk, k + L.ring_key_run().first, k + L.ring_key_run().second, SK, dot, dot_stroke.c_str()); |
| 282 | o << "</g><g class=\"ckey-data-polar\">"; |
| 283 | const size_t stride = static_cast<size_t>(L.ring_stride); |
| 284 | std::vector<uint8_t> ring_bits(static_cast<size_t>(L.rings) * stride, 0); |
| 285 | std::vector<int> ring_accent(static_cast<size_t>(L.rings) * stride, -1); |
| 286 | for (size_t i = 0; i < L.nodes.size(); ++i) { |
| 287 | const auto &n = L.nodes[i]; const bool bit = scalable_site_value(L, s, i); |
| 288 | const int accent = accent_of(n.id, *st); |
| 289 | if (n.kind == 0) { if (bit) emit_module(c, n.x, n.y, p, st->modules, accent >= 0 ? hex_colour(st->accents[accent]) : ""); } |
| 290 | else if (n.kind == 1) { |
| 291 | const double a = kTau * n.slot / L.dial_slots, r0 = kScalableDialInner, r1 = r0 + (bit ? L.long_ray() : L.short_ray()); |
| 292 | o << "<path d=\"M " << kCenter + r0 * std::sin(a) << ' ' << kCenter - r0 * std::cos(a) << " L " << kCenter + r1 * std::sin(a) << ' ' << kCenter - r1 * std::cos(a) |
| 293 | << "\" fill=\"none\" stroke-width=\"" << L.ray_width << "\" stroke-linecap=\"" << cap_style << "\"/>"; |
| 294 | } 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; } |
| 295 | } |
| 296 | for (int ring = 0; ring < L.rings; ++ring) { |
| 297 | const double radius = L.ring_radius(ring); const int S = L.ring_slots[static_cast<size_t>(ring)]; |
| 298 | for (int slot = 0; slot < S;) { |
| 299 | const size_t at = static_cast<size_t>(ring) * stride + slot; |
| 300 | if (!ring_bits[at]) { ++slot; continue; } |
| 301 | int end = slot + 1; |
| 302 | if (st->rings == CKEY_RING_JOINED) while (end < S && ring_bits[static_cast<size_t>(ring) * stride + end]) ++end; |
| 303 | const int accent = ring_accent[at]; |
| 304 | const std::string colour = accent >= 0 ? hex_colour(st->accents[accent]) : ""; |
| 305 | if (end - slot == 1) { |
| 306 | const double a = kTau * slot / S; |
| 307 | o << "<circle cx=\"" << kCenter + radius * std::sin(a) << "\" cy=\"" << kCenter - radius * std::cos(a) << "\" r=\"" << dot << "\" stroke=\"none\"" |
| 308 | << (colour.empty() ? "" : " fill=\"" + colour + "\"") << "/>"; |
| 309 | } else if (colour.empty()) svg_ring_run(o, kCenter, radius, slot - .5, end - .5, S, dot, dot_stroke.c_str()); |
| 310 | else { o << "<g stroke=\"" << colour << "\">"; svg_ring_run(o, kCenter, radius, slot - .5, end - .5, S, dot, dot_stroke.c_str()); o << "</g>"; } |
| 311 | slot = end; |
| 312 | } |
| 313 | } |
| 314 | o << "</g><g class=\"ckey-data-cartesian\">" << c.str() << "</g></g>"; |
| 315 | if (st->logo_href && *st->logo_href) { |
| 316 | const double r = st->logo_radius > 0 ? st->logo_radius : logo_quiet_radius(L); |
| 317 | if (st->logo_plate) o << "<g class=\"ckey-logo-plate\"><circle cx=\"14.5\" cy=\"14.5\" r=\"" << r + 0.1 << "\" fill=\"" << hex_colour(st->background) << "\"/></g>"; |
| 318 | o << "<image href=\"" << xml_escape(st->logo_href) << "\" x=\"" << 14.5 - r << "\" y=\"" << 14.5 - r << "\" width=\"" << 2 * r << "\" height=\"" << 2 * r |
| 319 | << "\" preserveAspectRatio=\"xMidYMid meet\" clip-path=\"url(#ckey-logo-clip)\"/>"; |
| 320 | } |
| 321 | o << "</svg>"; |
| 322 | const std::string text = o.str(); *required = text.size() + 1; |
| 323 | if (!out || cap < *required) return CKEY_INSUFFICIENT_BUFFER; |
| 324 | std::memcpy(out, text.c_str(), *required); |
| 325 | return CKEY_OK; |
| 326 | } |
| 327 | |
| 328 | ckey_status ckey_render_scalable_print(const ckey_scalable_symbol *s, const ckey_style *style, double width_in, double dpi, |
| 329 | uint8_t *out, size_t cap, size_t *out_side) { |
| 330 | if (!s || !out_side || !std::isfinite(width_in) || !std::isfinite(dpi) || width_in <= 0 || dpi < 72 || dpi > 4800 || width_in * dpi > 20000) |
| 331 | return CKEY_INVALID_ARGUMENT; |
| 332 | ckey_style st; ckey_style_default(&st); |
| 333 | if (style) { st.modules = style->modules; st.rings = style->rings; st.rays = style->rays; } // shapes only |
| 334 | const int side = std::max(16, static_cast<int>(std::lround(width_in * dpi))); |
| 335 | if (side > 16384) return CKEY_INVALID_ARGUMENT; |
| 336 | // Direct geometry -> printer dots: black where at least half of a 3x3 |
| 337 | // sample grid is ink (no SVG in this path; specification §2.2). |
| 338 | ckey_raster_options o; ckey_raster_options_default(&o); |
| 339 | o.size_px = static_cast<uint32_t>(side); o.policy = CKEY_RASTER_BINARY; o.format = CKEY_PIXELS_GRAY8; |
| 340 | o.supersample = 3; o.materials = 0; |
| 341 | size_t needed = 0; |
| 342 | const ckey_status probe = ckey_render_scalable_raster(s, &st, &o, nullptr, 0, &needed); |
| 343 | if (probe != CKEY_INSUFFICIENT_BUFFER) return probe == CKEY_OK ? CKEY_INTERNAL_ERROR : probe; |
| 344 | *out_side = static_cast<size_t>(side); |
| 345 | if (!out || cap < needed) return CKEY_INSUFFICIENT_BUFFER; |
| 346 | return ckey_render_scalable_raster(s, &st, &o, out, cap, &needed); |
| 347 | } |
| 348 | |
| 349 | } // extern "C" |
| 350 | |
| 351 | "scalable_render.inc" |