Viewing is open to everyone. Downloading and starring need a GitHub sign-in.
Sign in with GitHub
CKeySymbol.cs
v0.1.0-build.85
Latest
Commit ede38f5
Sign in with GitHub to download. Each download is recorded with your GitHub username.
| 1 | using System; |
| 2 | using System.Collections.Generic; |
| 3 | using System.Text; |
| 4 | using OpenCKey.Interop; |
| 5 | |
| 6 | namespace OpenCKey |
| 7 | { |
| 8 | /// <summary> |
| 9 | /// An encoded Profile 0 (scalable) symbol. This is a managed copy of the C |
| 10 | /// <c>ckey_scalable_symbol</c> (8,216 bytes, no native resources), so it |
| 11 | /// needs no disposal and is safe to share between threads. |
| 12 | /// </summary> |
| 13 | public sealed unsafe class CKeySymbol |
| 14 | { |
| 15 | private NativeScalableSymbol _symbol; |
| 16 | |
| 17 | private CKeySymbol() |
| 18 | { |
| 19 | } |
| 20 | |
| 21 | /// <summary>Highest Profile 0 version.</summary> |
| 22 | public const int MaxVersion = NativeConstants.ScalableVersionCount; |
| 23 | |
| 24 | /// <summary>Encodes UTF-8 text.</summary> |
| 25 | /// <param name="text">The payload text; encoded as UTF-8 without a BOM.</param> |
| 26 | /// <param name="version">1..31, or 0 for the smallest version that fits.</param> |
| 27 | /// <param name="level">Error-correction level.</param> |
| 28 | /// <param name="layout">Standard (screen) or print layout.</param> |
| 29 | public static CKeySymbol Encode(string text, int version = 0, EccLevel level = EccLevel.M, Layout layout = Layout.Standard) |
| 30 | { |
| 31 | if (text == null) throw new ArgumentNullException(nameof(text)); |
| 32 | return Encode(new UTF8Encoding(false).GetBytes(text), version, level, layout); |
| 33 | } |
| 34 | |
| 35 | /// <summary>Encodes bytes.</summary> |
| 36 | /// <param name="payload">The payload bytes.</param> |
| 37 | /// <param name="version">1..31, or 0 for the smallest version that fits.</param> |
| 38 | /// <param name="level">Error-correction level.</param> |
| 39 | /// <param name="layout">Standard (screen) or print layout.</param> |
| 40 | public static CKeySymbol Encode(ReadOnlySpan<byte> payload, int version = 0, EccLevel level = EccLevel.M, Layout layout = Layout.Standard) |
| 41 | { |
| 42 | if (version < 0 || version > MaxVersion) throw new ArgumentOutOfRangeException(nameof(version), "version must be 0 (auto) or 1..31."); |
| 43 | if (level < EccLevel.L || level > EccLevel.H) throw new ArgumentOutOfRangeException(nameof(level)); |
| 44 | if (layout != Layout.Standard && layout != Layout.Print) throw new ArgumentOutOfRangeException(nameof(layout)); |
| 45 | Native.EnsureAbi(); |
| 46 | var symbol = new CKeySymbol(); |
| 47 | NativeDiagnostics diagnostics = default; |
| 48 | byte empty = 0; |
| 49 | fixed (byte* p = payload) |
| 50 | fixed (NativeScalableSymbol* s = &symbol._symbol) |
| 51 | { |
| 52 | byte* data = p != null ? p : ∅ |
| 53 | int status = NativeMethods.ckey_encode_scalable(data, (nuint)payload.Length, (uint)version, (int)level, (int)layout, s, &diagnostics); |
| 54 | Native.Check(status, "ckey_encode_scalable", ref diagnostics); |
| 55 | } |
| 56 | return symbol; |
| 57 | } |
| 58 | |
| 59 | /// <summary>Maximum payload bytes for a version, level and layout; 0 if invalid.</summary> |
| 60 | public static int Capacity(int version, EccLevel level = EccLevel.M, Layout layout = Layout.Standard) |
| 61 | { |
| 62 | if (version < 1 || version > MaxVersion) return 0; |
| 63 | Native.EnsureAbi(); |
| 64 | return (int)NativeMethods.ckey_scalable_capacity((uint)version, (int)level, (int)layout); |
| 65 | } |
| 66 | |
| 67 | /// <summary>Modules across the field for a version (29..232); 0 if invalid.</summary> |
| 68 | public static int ModulesAcrossFor(int version) |
| 69 | { |
| 70 | if (version < 1 || version > MaxVersion) return 0; |
| 71 | Native.EnsureAbi(); |
| 72 | return (int)NativeMethods.ckey_scalable_modules_across((uint)version); |
| 73 | } |
| 74 | |
| 75 | /// <summary>Largest logo radius (canonical units) for a version and layout.</summary> |
| 76 | public static double LogoMaxRadius(int version, Layout layout = Layout.Standard) |
| 77 | { |
| 78 | Native.EnsureAbi(); |
| 79 | return NativeMethods.ckey_scalable_logo_max_radius((uint)version, (int)layout); |
| 80 | } |
| 81 | |
| 82 | /// <summary>Version 1..31.</summary> |
| 83 | public int Version => (int)_symbol.Version; |
| 84 | /// <summary>Error-correction level.</summary> |
| 85 | public EccLevel EccLevel => (EccLevel)_symbol.EccLevel; |
| 86 | /// <summary>Layout family.</summary> |
| 87 | public Layout Layout => (Layout)_symbol.Layout; |
| 88 | /// <summary>Payload length in bytes.</summary> |
| 89 | public int PayloadLength => (int)_symbol.PayloadLength; |
| 90 | /// <summary>Codeword bytes in use.</summary> |
| 91 | public int CodewordLength => (int)_symbol.CodewordLength; |
| 92 | /// <summary>Modules across the field (29..232).</summary> |
| 93 | public int ModulesAcross => ModulesAcrossFor(Version); |
| 94 | |
| 95 | /// <summary>A copy of the codeword bytes in use.</summary> |
| 96 | public byte[] GetCodeword() |
| 97 | { |
| 98 | int n = Math.Min(CodewordLength, NativeConstants.ScalableMaxCodewordBytes); |
| 99 | var bytes = new byte[n]; |
| 100 | fixed (NativeScalableSymbol* s = &_symbol) |
| 101 | { |
| 102 | for (int i = 0; i < n; i++) bytes[i] = s->Codeword[i]; |
| 103 | } |
| 104 | return bytes; |
| 105 | } |
| 106 | |
| 107 | /// <summary>Share of the worst block's correction capacity hidden under a logo of this radius.</summary> |
| 108 | public double LogoLoad(double logoRadius) |
| 109 | { |
| 110 | fixed (NativeScalableSymbol* s = &_symbol) return NativeMethods.ckey_scalable_logo_load(s, logoRadius); |
| 111 | } |
| 112 | |
| 113 | // ---- Style check and certification --------------------------------- |
| 114 | |
| 115 | /// <summary> |
| 116 | /// Checks a style against this symbol (contrast, effect limits, logo |
| 117 | /// error budget). Returns true when renderers would accept it; otherwise |
| 118 | /// <paramref name="message"/> explains the refusal. |
| 119 | /// </summary> |
| 120 | public bool CheckStyle(CKeyStyle style, out string message) |
| 121 | { |
| 122 | if (style == null) throw new ArgumentNullException(nameof(style)); |
| 123 | NativeDiagnostics diagnostics = default; |
| 124 | byte[]? href = style.LogoHrefUtf8Z(); |
| 125 | int status; |
| 126 | fixed (byte* h = href) |
| 127 | fixed (NativeScalableSymbol* s = &_symbol) |
| 128 | { |
| 129 | NativeStyle ns = style.ToNative((IntPtr)h); |
| 130 | status = NativeMethods.ckey_check_scalable_style(s, &ns, &diagnostics); |
| 131 | } |
| 132 | message = Native.Message(ref diagnostics); |
| 133 | return status == 0; |
| 134 | } |
| 135 | |
| 136 | /// <summary> |
| 137 | /// Certifies a style by rendering it, compositing it over the backdrops, |
| 138 | /// degrading it with the scan model and decoding it with the real decoder |
| 139 | /// (<c>ckey_certify_scalable_style</c>). A transparent symbol needs a |
| 140 | /// backdrop (colours and/or the host's pixels behind it); without one it |
| 141 | /// is never certified. |
| 142 | /// </summary> |
| 143 | /// <param name="style">The style, or null for the default; its <see cref="CKeyStyle.Policy"/> decides strict/adaptive/override.</param> |
| 144 | /// <param name="transparent">Certify transparent output.</param> |
| 145 | /// <param name="backdropColours">Up to 16 solid colours the symbol may sit on.</param> |
| 146 | /// <param name="backdropImage">Host pixels behind the symbol, stretched over its full canvas.</param> |
| 147 | /// <param name="model">The acquisition model, or null for the default phone-camera model.</param> |
| 148 | public CKeyCertification Certify(CKeyStyle? style = null, bool transparent = false, |
| 149 | IReadOnlyList<CKeyColor>? backdropColours = null, CKeyImage? backdropImage = null, |
| 150 | CKeyScanModel? model = null) |
| 151 | { |
| 152 | if (backdropColours != null && backdropColours.Count > 16) |
| 153 | throw new ArgumentOutOfRangeException(nameof(backdropColours), "At most 16 backdrop colours."); |
| 154 | Native.EnsureAbi(); |
| 155 | NativeRgb[]? colours = null; |
| 156 | if (backdropColours != null && backdropColours.Count > 0) |
| 157 | { |
| 158 | colours = new NativeRgb[backdropColours.Count]; |
| 159 | for (int i = 0; i < colours.Length; i++) colours[i] = backdropColours[i].ToNative(); |
| 160 | } |
| 161 | NativeScanModel nm; |
| 162 | NativeMethods.ckey_scan_model_default(&nm); |
| 163 | if (model != null) model.Apply(ref nm); |
| 164 | |
| 165 | byte[]? href = style?.LogoHrefUtf8Z(); |
| 166 | NativeCertification cert = default; |
| 167 | NativeStyle adjusted = default; |
| 168 | int status; |
| 169 | CKeyStyle? adjustedStyle = null; |
| 170 | fixed (byte* h = href) |
| 171 | fixed (NativeRgb* c = colours) |
| 172 | fixed (byte* bg = backdropImage?.Rgba) |
| 173 | fixed (NativeScalableSymbol* s = &_symbol) |
| 174 | { |
| 175 | NativeStyle ns = style != null ? style.ToNative((IntPtr)h) : default; |
| 176 | NativeBackdrop backdrop = new NativeBackdrop |
| 177 | { |
| 178 | Colours = (IntPtr)c, |
| 179 | ColourCount = colours != null ? (uint)colours.Length : 0u, |
| 180 | Rgba = (IntPtr)bg, |
| 181 | Width = backdropImage != null ? (uint)backdropImage.Width : 0u, |
| 182 | Height = backdropImage != null ? (uint)backdropImage.Height : 0u, |
| 183 | Stride = 0, |
| 184 | }; |
| 185 | bool hasBackdrop = colours != null || backdropImage != null; |
| 186 | status = NativeMethods.ckey_certify_scalable_style(s, style != null ? &ns : null, transparent ? 1u : 0u, |
| 187 | hasBackdrop ? &backdrop : null, &nm, &adjusted, &cert); |
| 188 | // Read the adjusted style while the logo href it may point to is still pinned. |
| 189 | if (status == 0 && style != null && style.Policy == StylePolicy.Adaptive) |
| 190 | { |
| 191 | adjustedStyle = new CKeyStyle(); |
| 192 | adjustedStyle.Load(in adjusted); |
| 193 | } |
| 194 | } |
| 195 | string message = Native.FromUtf8Z(cert.Message, NativeConstants.CertificationMessageBytes) ?? string.Empty; |
| 196 | return new CKeyCertification((CKeyStatus)status, cert.Certified != 0, (StylePolicy)cert.Policy, |
| 197 | (StyleAdjustments)cert.Adjustments, (int)cert.Trials, (int)cert.TrialsPassed, cert.MinContrast, |
| 198 | cert.FeatureSeparation, cert.LogoLoad, message, adjustedStyle); |
| 199 | } |
| 200 | |
| 201 | // ---- Rendering -------------------------------------------------------- |
| 202 | |
| 203 | /// <summary> |
| 204 | /// Renders native RGBA8 pixels (size x size, covering the full 31-unit |
| 205 | /// canvas including the quiet margin). No SVG, file system or network. |
| 206 | /// </summary> |
| 207 | /// <param name="size">16..16384 pixels.</param> |
| 208 | /// <param name="policy">Raster policy.</param> |
| 209 | /// <param name="transparent">Paint no background: unpainted pixels have alpha 0.</param> |
| 210 | /// <param name="style">Style, or null for the default.</param> |
| 211 | /// <param name="logo">Optional RGBA logo drawn into the style's logo circle.</param> |
| 212 | /// <param name="materials">false draws flat ink (no sheen, depth or grain).</param> |
| 213 | public CKeyImage RenderRgba(int size = 512, RasterPolicy policy = RasterPolicy.Coverage, bool transparent = false, |
| 214 | CKeyStyle? style = null, CKeyImage? logo = null, bool materials = true) |
| 215 | { |
| 216 | byte[] px = Raster(size, policy, NativeConstants.PixelsRgba8, transparent, style, logo, materials); |
| 217 | return new CKeyImage(size, size, px); |
| 218 | } |
| 219 | |
| 220 | /// <summary>Renders opaque 8-bit gray pixels (Rec. 709 luma), size x size.</summary> |
| 221 | public CKeyGrayImage RenderGray(int size = 512, RasterPolicy policy = RasterPolicy.Coverage, CKeyStyle? style = null, |
| 222 | bool materials = true) |
| 223 | { |
| 224 | byte[] px = Raster(size, policy, NativeConstants.PixelsGray8, false, style, null, materials); |
| 225 | return new CKeyGrayImage(size, size, px); |
| 226 | } |
| 227 | |
| 228 | /// <summary> |
| 229 | /// Print rendering: black ink on paper on the printer's dot grid, |
| 230 | /// <paramref name="widthInches"/> wide at <paramref name="dpi"/>. Each |
| 231 | /// pixel is 0 (ink) or 255 (paper). Colours, materials and logos are not printed. |
| 232 | /// </summary> |
| 233 | public CKeyGrayImage RenderPrint(double widthInches, double dpi, CKeyStyle? style = null) |
| 234 | { |
| 235 | Native.EnsureAbi(); |
| 236 | byte[]? href = style?.LogoHrefUtf8Z(); |
| 237 | fixed (byte* h = href) |
| 238 | fixed (NativeScalableSymbol* s = &_symbol) |
| 239 | { |
| 240 | NativeStyle ns = style != null ? style.ToNative((IntPtr)h) : default; |
| 241 | NativeStyle* sp = style != null ? &ns : null; |
| 242 | nuint side = 0; |
| 243 | int status = NativeMethods.ckey_render_scalable_print(s, sp, widthInches, dpi, null, 0, &side); |
| 244 | if (status != (int)CKeyStatus.InsufficientBuffer) Native.Check(status, "ckey_render_scalable_print"); |
| 245 | int n = Native.ToInt32Length(side, "Print raster"); |
| 246 | if (n <= 0 || (long)n * n > int.MaxValue) throw new CKeyException(CKeyStatus.InvalidArgument, "Print raster size out of range."); |
| 247 | var gray = new byte[n * n]; |
| 248 | fixed (byte* o = gray) |
| 249 | { |
| 250 | Native.Check(NativeMethods.ckey_render_scalable_print(s, sp, widthInches, dpi, o, (nuint)gray.Length, &side), |
| 251 | "ckey_render_scalable_print"); |
| 252 | } |
| 253 | return new CKeyGrayImage(n, n, gray); |
| 254 | } |
| 255 | } |
| 256 | |
| 257 | /// <summary>Renders RGBA pixels and encodes them as PNG.</summary> |
| 258 | public byte[] ToPng(int size = 512, RasterPolicy policy = RasterPolicy.Coverage, bool transparent = false, |
| 259 | CKeyStyle? style = null, double dpi = 0) => |
| 260 | RenderRgba(size, policy, transparent, style).ToPng(dpi); |
| 261 | |
| 262 | /// <summary>Styled SVG export (vector), <paramref name="scale"/> pixels per canonical unit.</summary> |
| 263 | public string ToSvg(int scale = 8, CKeyStyle? style = null) |
| 264 | { |
| 265 | if (scale < 1 || scale > 256) throw new ArgumentOutOfRangeException(nameof(scale), "scale must be 1..256."); |
| 266 | Native.EnsureAbi(); |
| 267 | style ??= new CKeyStyle(); |
| 268 | byte[]? href = style.LogoHrefUtf8Z(); |
| 269 | fixed (byte* h = href) |
| 270 | fixed (NativeScalableSymbol* s = &_symbol) |
| 271 | { |
| 272 | NativeStyle ns = style.ToNative((IntPtr)h); |
| 273 | nuint required = 0; |
| 274 | int status = NativeMethods.ckey_render_scalable_styled_svg(s, &ns, (uint)scale, null, 0, &required); |
| 275 | if (status != (int)CKeyStatus.InsufficientBuffer) Native.Check(status, "ckey_render_scalable_styled_svg"); |
| 276 | var utf8 = new byte[Native.ToInt32Length(required, "SVG")]; |
| 277 | fixed (byte* o = utf8) |
| 278 | { |
| 279 | Native.Check(NativeMethods.ckey_render_scalable_styled_svg(s, &ns, (uint)scale, o, (nuint)utf8.Length, &required), |
| 280 | "ckey_render_scalable_styled_svg"); |
| 281 | return Native.FromUtf8Z(o, utf8.Length) ?? string.Empty; // required includes the NUL |
| 282 | } |
| 283 | } |
| 284 | } |
| 285 | |
| 286 | /// <summary>One-page PDF 1.4 vector export, <paramref name="widthPt"/> points wide (72 per inch).</summary> |
| 287 | public byte[] ToPdf(double widthPt = 288, bool transparent = false, CKeyStyle? style = null) |
| 288 | { |
| 289 | if (!(widthPt > 0)) throw new ArgumentOutOfRangeException(nameof(widthPt)); |
| 290 | Native.EnsureAbi(); |
| 291 | byte[]? href = style?.LogoHrefUtf8Z(); |
| 292 | fixed (byte* h = href) |
| 293 | fixed (NativeScalableSymbol* s = &_symbol) |
| 294 | { |
| 295 | NativeStyle ns = style != null ? style.ToNative((IntPtr)h) : default; |
| 296 | NativeStyle* sp = style != null ? &ns : null; |
| 297 | nuint required = 0; |
| 298 | uint t = transparent ? 1u : 0u; |
| 299 | int status = NativeMethods.ckey_render_scalable_pdf(s, sp, widthPt, t, null, 0, &required); |
| 300 | if (status != (int)CKeyStatus.InsufficientBuffer) Native.Check(status, "ckey_render_scalable_pdf"); |
| 301 | var pdf = new byte[Native.ToInt32Length(required, "PDF")]; |
| 302 | fixed (byte* o = pdf) |
| 303 | { |
| 304 | Native.Check(NativeMethods.ckey_render_scalable_pdf(s, sp, widthPt, t, o, (nuint)pdf.Length, &required), |
| 305 | "ckey_render_scalable_pdf"); |
| 306 | } |
| 307 | if ((int)required < pdf.Length) Array.Resize(ref pdf, (int)required); |
| 308 | return pdf; |
| 309 | } |
| 310 | } |
| 311 | |
| 312 | private byte[] Raster(int size, RasterPolicy policy, int format, bool transparent, CKeyStyle? style, CKeyImage? logo, bool materials) |
| 313 | { |
| 314 | if (size < 16 || size > 16384) throw new ArgumentOutOfRangeException(nameof(size), "size must be 16..16384 pixels."); |
| 315 | if (policy < RasterPolicy.Coverage || policy > RasterPolicy.Hybrid) throw new ArgumentOutOfRangeException(nameof(policy)); |
| 316 | Native.EnsureAbi(); |
| 317 | NativeRasterOptions options; |
| 318 | NativeMethods.ckey_raster_options_default(&options); |
| 319 | options.SizePx = (uint)size; |
| 320 | options.Policy = (uint)policy; |
| 321 | options.Format = (uint)format; |
| 322 | options.Transparent = transparent ? 1u : 0u; |
| 323 | options.Materials = materials ? 1u : 0u; |
| 324 | byte[]? href = style?.LogoHrefUtf8Z(); |
| 325 | fixed (byte* h = href) |
| 326 | fixed (byte* lp = logo?.Rgba) |
| 327 | fixed (NativeScalableSymbol* s = &_symbol) |
| 328 | { |
| 329 | NativeLogoImage li = default; |
| 330 | if (logo != null) |
| 331 | { |
| 332 | li.Rgba = (IntPtr)lp; |
| 333 | li.Width = (uint)logo.Width; |
| 334 | li.Height = (uint)logo.Height; |
| 335 | li.Stride = (nuint)(logo.Width * 4); |
| 336 | options.Logo = (IntPtr)(&li); |
| 337 | } |
| 338 | NativeStyle ns = style != null ? style.ToNative((IntPtr)h) : default; |
| 339 | NativeStyle* sp = style != null ? &ns : null; |
| 340 | nuint required = 0; |
| 341 | int status = NativeMethods.ckey_render_scalable_raster(s, sp, &options, null, 0, &required); |
| 342 | if (status != (int)CKeyStatus.InsufficientBuffer) Native.Check(status, "ckey_render_scalable_raster"); |
| 343 | var px = new byte[Native.ToInt32Length(required, "Raster")]; |
| 344 | fixed (byte* o = px) |
| 345 | { |
| 346 | Native.Check(NativeMethods.ckey_render_scalable_raster(s, sp, &options, o, (nuint)px.Length, &required), |
| 347 | "ckey_render_scalable_raster"); |
| 348 | } |
| 349 | return px; |
| 350 | } |
| 351 | } |
| 352 | } |
| 353 | } |