verifiabl-issuer 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c75b4c20eb27ae4634245aadb04a4fcc0afa04f71fbc63590eaca8cf9b28e7c3
4
- data.tar.gz: d1ed18401e4f81dae4674b649cf70287c12df90041c7c246c6caddd56bf000c7
3
+ metadata.gz: d049c2b4efee0761f9172022bee306456bc6e77d8223081196c027d289f88c48
4
+ data.tar.gz: b43694a8fa98216ddb558f3f63fc9c47f7a6c5740e6f47fd9c187a78f60fe437
5
5
  SHA512:
6
- metadata.gz: b39a7f7c02a05766ec5154cb795832622dd977242b73eeb232b27b220344835cdc67038413570c52a800fb009b4ab3d083a8b22f60559563e741614c986c9441
7
- data.tar.gz: ce6869825c07b3d3db6b34f5c307f93125451c737b04c7b4291f53f3209b246b23e3f3db3c88411d7cdeca4c9e0de091d028e8f80b45dad38f49d2e0846b41a9
6
+ metadata.gz: 961b1387f5420386fa996309dfdf8c3b237879a6c350d31072a34a42d6a54bcc289f205fd81cae3a12da225a1f060b7fdebee1aa43ccc36ed1662b58c2a24682
7
+ data.tar.gz: 9f72f1055910e0725e340510d888fe8e271613c762cf72d39061fb9703b93d7b647af63d2558f45cbadf1832214736c0a2574d31caa338901d0cdb588f58a2a3
data/CHANGELOG.md CHANGED
@@ -4,6 +4,15 @@ All notable changes to the Verifiabl Ruby SDK will be documented in this file.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## [0.4.0] - 2026-10-07
8
+
9
+ - Add `layout: :horizontal` to local SVG and PNG rendering, with the QR on the
10
+ left, a 7-unit white gap and an opaque light-tinted brand panel on the right.
11
+ SVG minimum/default width is 940; PNG supports 940, 1410, 1880 and 2820 pixels
12
+ (default 1410). Vertical stays the default with unchanged output.
13
+ - Qualify horizontal SVG bytes, PNG pixels and QR metadata against Node and .NET,
14
+ including independent digital decoding.
15
+
7
16
  ## [0.3.0] - 2026-10-04
8
17
 
9
18
  - Accept an `other` earnings line in AU2 and NZ2 payslips, for a pay code that
data/README.md CHANGED
@@ -9,7 +9,7 @@ client, RBS signatures, and packed-gem consumer qualification.
9
9
  Add the stable release to your bundle:
10
10
 
11
11
  ```ruby
12
- gem "verifiabl-issuer", "0.3.0"
12
+ gem "verifiabl-issuer", "0.4.0"
13
13
  ```
14
14
 
15
15
  Then run `bundle install`.
@@ -466,9 +466,31 @@ File.binwrite("barcode.png", png.png)
466
466
  ```
467
467
 
468
468
  The renderers use the same official vector frame, rounded finder geometry, and QR error-correction
469
- ladder as the Node and .NET SDKs. SVG widths are continuously scalable from 480 upward. PNG frames
470
- are pre-rasterised for deterministic cross-SDK output and therefore support only `480`, `720`,
471
- `960`, and `1440` pixels. Use `max_error_correction: :q` for greater damage recovery; the default is
469
+ ladder as the Node and .NET SDKs. Vertical is the default; pass `layout: :horizontal` to put the
470
+ QR on the left at full badge height, followed by a 7-unit white gap and the light-tinted
471
+ "Secured by Verifiabl" panel on the right. Horizontal rendering is included in `0.4.0`.
472
+
473
+ | Layout | SVG minimum/default width | PNG widths | PNG default width |
474
+ | --- | ---: | --- | ---: |
475
+ | `:vertical` | 480 | 480, 720, 960, 1440 | 720 |
476
+ | `:horizontal` | 940 | 940, 1410, 1880, 2820 | 1410 |
477
+
478
+ SVG widths scale continuously above the minimum. PNG frames are pre-rasterised for deterministic
479
+ cross-SDK output and support only the listed widths. Both layouts render the QR at the same sizes
480
+ at corresponding widths; the horizontal badge's total width includes its brand panel and gap.
481
+ For example, `build_barcode_svg(**parts, layout: :horizontal)` returns a 940×480 SVG and
482
+ `build_barcode_png(**parts, layout: :horizontal)` returns a 1410×720 PNG.
483
+
484
+ ### Badge placement
485
+
486
+ Place the badge on a light background with a clear margin of at least four QR modules
487
+ (`result.module_px` at the rendered size) on every open QR side: **left, right and bottom** for
488
+ vertical; **left, top and bottom** for horizontal. A tenth of the QR side covers realistic records.
489
+ The host document supplies these quiet zones; the badge supplies only the gap facing the brand
490
+ frame. Size the QR, not the whole horizontal badge: a 22 mm QR means a horizontal badge about
491
+ 43.08 mm wide and 22 mm high.
492
+
493
+ Use `max_error_correction: :q` for greater damage recovery; the default is
472
494
  `:m`, and the renderer steps down only when necessary to preserve the three-pixel module floor.
473
495
  Each result's `degraded` field (`svg.degraded` or `png.degraded` above) is true when the renderer
474
496
  steps down from the requested ceiling or the modules fall below the ideal four-pixel size.
@@ -12,17 +12,28 @@ module Verifiabl
12
12
 
13
13
  module_function
14
14
 
15
- def raster(width)
16
- frame = load(width)
15
+ def raster(width, layout: :vertical)
16
+ frame = load(width, layout:)
17
17
  rgba = String.new(capacity: frame.width * frame.height * 4, encoding: Encoding::BINARY)
18
18
  entries = frame.palette.bytes.each_slice(4).map { |bytes| bytes.pack("C4") }
19
19
  frame.indices.each_byte { |index| rgba << entries.fetch(index) }
20
20
  [rgba, frame.width, frame.height]
21
21
  end
22
22
 
23
- def load(width)
23
+ def supported_widths(layout)
24
+ return SUPPORTED_WIDTHS if layout == :vertical
25
+ return SUPPORTED_HORIZONTAL_WIDTHS if layout == :horizontal
26
+
27
+ raise ArgumentError, "layout must be :vertical or :horizontal"
28
+ end
29
+
30
+ def load(width, layout: :vertical)
31
+ unless supported_widths(layout).include?(width)
32
+ raise ArgumentError, "unsupported frame width"
33
+ end
24
34
  @cache ||= {}
25
- @cache[width] ||= parse(File.binread(File.join(__dir__, "assets", "frame-#{width}.vfr1")), expected_width: width)
35
+ name = (layout == :horizontal) ? "frame-horizontal-#{width}.vfr1" : "frame-#{width}.vfr1"
36
+ @cache[[layout, width]] ||= parse(File.binread(File.join(__dir__, "assets", name)), expected_width: width)
26
37
  end
27
38
 
28
39
  def parse(container, expected_width: nil)
@@ -5,6 +5,7 @@ module Verifiabl
5
5
  module Issuer
6
6
  module FrameAssets
7
7
  SUPPORTED_WIDTHS = [480, 720, 960, 1440].freeze
8
+ SUPPORTED_HORIZONTAL_WIDTHS = [940, 1410, 1880, 2820].freeze
8
9
  end
9
10
  end
10
11
  end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Generated cross-SDK QR artifact. Do not edit by hand.
4
+ module Verifiabl
5
+ module Issuer
6
+ module RenderingAssets
7
+ HORIZONTAL_FRAME = "<rect x=\"0\" y=\"0\" width=\"103\" height=\"96\" fill=\"#FFFFFF\"/><g transform=\"translate(103 0)\"><path d=\"M0 0H75.4C80.70196 0 85 4.29806 85 9.6V86.4C85 91.70194 80.70196 96 75.4 96H0V0Z\" fill=\"#EDEFFF\"/><g transform=\"translate(0.5 0) scale(1.2)\" fill=\"#010A4F\"><path d=\"M9.76 32.63C9.76 32.87 9.80333 33.0733 9.89 33.24C9.97667 33.4067 10.09 33.5433 10.23 33.65C10.3767 33.75 10.5467 33.8267 10.74 33.88C10.9333 33.9267 11.1333 33.95 11.34 33.95C11.48 33.95 11.63 33.94 11.79 33.92C11.95 33.8933 12.1 33.8467 12.24 33.78C12.38 33.7133 12.4967 33.6233 12.59 33.51C12.6833 33.39 12.73 33.24 12.73 33.06C12.73 32.8667 12.6667 32.71 12.54 32.59C12.42 32.47 12.26 32.37 12.06 32.29C11.86 32.21 11.6333 32.14 11.38 32.08C11.1267 32.02 10.87 31.9533 10.61 31.88C10.3433 31.8133 10.0833 31.7333 9.83 31.64C9.57667 31.54 9.35 31.4133 9.15 31.26C8.95 31.1067 8.78667 30.9167 8.66 30.69C8.54 30.4567 8.48 30.1767 8.48 29.85C8.48 29.4833 8.55667 29.1667 8.71 28.9C8.87 28.6267 9.07667 28.4 9.33 28.22C9.58333 28.04 9.87 27.9067 10.19 27.82C10.51 27.7333 10.83 27.69 11.15 27.69C11.5233 27.69 11.88 27.7333 12.22 27.82C12.5667 27.9 12.8733 28.0333 13.14 28.22C13.4067 28.4067 13.6167 28.6467 13.77 28.94C13.93 29.2267 14.01 29.5767 14.01 29.99H12.49C12.4767 29.7767 12.43 29.6 12.35 29.46C12.2767 29.32 12.1767 29.21 12.05 29.13C11.9233 29.05 11.7767 28.9933 11.61 28.96C11.45 28.9267 11.2733 28.91 11.08 28.91C10.9533 28.91 10.8267 28.9233 10.7 28.95C10.5733 28.9767 10.4567 29.0233 10.35 29.09C10.25 29.1567 10.1667 29.24 10.1 29.34C10.0333 29.44 10 29.5667 10 29.72C10 29.86 10.0267 29.9733 10.08 30.06C10.1333 30.1467 10.2367 30.2267 10.39 30.3C10.55 30.3733 10.7667 30.4467 11.04 30.52C11.32 30.5933 11.6833 30.6867 12.13 30.8C12.2633 30.8267 12.4467 30.8767 12.68 30.95C12.92 31.0167 13.1567 31.1267 13.39 31.28C13.6233 31.4333 13.8233 31.64 13.99 31.9C14.1633 32.1533 14.25 32.48 14.25 32.88C14.25 33.2067 14.1867 33.51 14.06 33.79C13.9333 34.07 13.7433 34.3133 13.49 34.52C13.2433 34.72 12.9333 34.8767 12.56 34.99C12.1933 35.1033 11.7667 35.16 11.28 35.16C10.8867 35.16 10.5033 35.11 10.13 35.01C9.76333 34.9167 9.43667 34.7667 9.15 34.56C8.87 34.3533 8.64667 34.09 8.48 33.77C8.31333 33.45 8.23333 33.07 8.24 32.63H9.76ZM18.5141 31.86C18.4475 31.5 18.3275 31.2267 18.1541 31.04C17.9875 30.8533 17.7308 30.76 17.3841 30.76C17.1575 30.76 16.9675 30.8 16.8141 30.88C16.6675 30.9533 16.5475 31.0467 16.4541 31.16C16.3675 31.2733 16.3041 31.3933 16.2641 31.52C16.2308 31.6467 16.2108 31.76 16.2041 31.86H18.5141ZM16.2041 32.76C16.2241 33.22 16.3408 33.5533 16.5541 33.76C16.7675 33.9667 17.0741 34.07 17.4741 34.07C17.7608 34.07 18.0075 34 18.2141 33.86C18.4208 33.7133 18.5475 33.56 18.5941 33.4H19.8441C19.6441 34.02 19.3375 34.4633 18.9241 34.73C18.5108 34.9967 18.0108 35.13 17.4241 35.13C17.0175 35.13 16.6508 35.0667 16.3241 34.94C15.9975 34.8067 15.7208 34.62 15.4941 34.38C15.2675 34.14 15.0908 33.8533 14.9641 33.52C14.8441 33.1867 14.7841 32.82 14.7841 32.42C14.7841 32.0333 14.8475 31.6733 14.9741 31.34C15.1008 31.0067 15.2808 30.72 15.5141 30.48C15.7475 30.2333 16.0241 30.04 16.3441 29.9C16.6708 29.76 17.0308 29.69 17.4241 29.69C17.8641 29.69 18.2475 29.7767 18.5741 29.95C18.9008 30.1167 19.1675 30.3433 19.3741 30.63C19.5875 30.9167 19.7408 31.2433 19.8341 31.61C19.9275 31.9767 19.9608 32.36 19.9341 32.76H16.2041ZM24.2963 31.65C24.203 31.0567 23.853 30.76 23.2463 30.76C23.0197 30.76 22.8297 30.8133 22.6763 30.92C22.523 31.02 22.3963 31.1533 22.2963 31.32C22.203 31.48 22.1363 31.66 22.0963 31.86C22.0563 32.0533 22.0363 32.2467 22.0363 32.44C22.0363 32.6267 22.0563 32.8167 22.0963 33.01C22.1363 33.2033 22.1997 33.38 22.2863 33.54C22.3797 33.6933 22.503 33.82 22.6563 33.92C22.8097 34.02 22.9963 34.07 23.2163 34.07C23.5563 34.07 23.8163 33.9767 23.9963 33.79C24.183 33.5967 24.2997 33.34 24.3463 33.02H25.7163C25.623 33.7067 25.3563 34.23 24.9163 34.59C24.4763 34.95 23.913 35.13 23.2263 35.13C22.8397 35.13 22.483 35.0667 22.1563 34.94C21.8363 34.8067 21.563 34.6233 21.3363 34.39C21.1097 34.1567 20.933 33.88 20.8063 33.56C20.6797 33.2333 20.6163 32.8767 20.6163 32.49C20.6163 32.09 20.673 31.72 20.7863 31.38C20.9063 31.0333 21.0797 30.7367 21.3063 30.49C21.533 30.2367 21.8097 30.04 22.1363 29.9C22.463 29.76 22.8363 29.69 23.2563 29.69C23.563 29.69 23.8563 29.73 24.1363 29.81C24.423 29.89 24.6763 30.0133 24.8963 30.18C25.123 30.34 25.3063 30.5433 25.4463 30.79C25.5863 31.03 25.6663 31.3167 25.6863 31.65H24.2963ZM31.3685 35H30.0185V34.28H29.9885C29.8085 34.58 29.5752 34.7967 29.2885 34.93C29.0018 35.0633 28.7085 35.13 28.4085 35.13C28.0285 35.13 27.7152 35.08 27.4685 34.98C27.2285 34.88 27.0385 34.74 26.8985 34.56C26.7585 34.3733 26.6585 34.15 26.5985 33.89C26.5452 33.6233 26.5185 33.33 26.5185 33.01V29.83H27.9385V32.75C27.9385 33.1767 28.0052 33.4967 28.1385 33.71C28.2718 33.9167 28.5085 34.02 28.8485 34.02C29.2352 34.02 29.5152 33.9067 29.6885 33.68C29.8618 33.4467 29.9485 33.0667 29.9485 32.54V29.83H31.3685V35ZM32.4463 29.83H33.7963V30.79H33.8163C33.8829 30.63 33.9729 30.4833 34.0863 30.35C34.1996 30.21 34.3296 30.0933 34.4762 30C34.6229 29.9 34.7796 29.8233 34.9463 29.77C35.1129 29.7167 35.2863 29.69 35.4663 29.69C35.5596 29.69 35.6629 29.7067 35.7763 29.74V31.06C35.7096 31.0467 35.6296 31.0367 35.5363 31.03C35.4429 31.0167 35.3529 31.01 35.2663 31.01C35.0063 31.01 34.7863 31.0533 34.6063 31.14C34.4263 31.2267 34.2796 31.3467 34.1663 31.5C34.0596 31.6467 33.9829 31.82 33.9363 32.02C33.8896 32.22 33.8663 32.4367 33.8663 32.67V35H32.4463V29.83ZM39.6372 31.86C39.5705 31.5 39.4505 31.2267 39.2772 31.04C39.1105 30.8533 38.8539 30.76 38.5072 30.76C38.2805 30.76 38.0905 30.8 37.9372 30.88C37.7905 30.9533 37.6705 31.0467 37.5772 31.16C37.4905 31.2733 37.4272 31.3933 37.3872 31.52C37.3539 31.6467 37.3339 31.76 37.3272 31.86H39.6372ZM37.3272 32.76C37.3472 33.22 37.4639 33.5533 37.6772 33.76C37.8905 33.9667 38.1972 34.07 38.5972 34.07C38.8839 34.07 39.1305 34 39.3372 33.86C39.5439 33.7133 39.6705 33.56 39.7172 33.4H40.9672C40.7672 34.02 40.4605 34.4633 40.0472 34.73C39.6339 34.9967 39.1339 35.13 38.5472 35.13C38.1405 35.13 37.7739 35.0667 37.4472 34.94C37.1205 34.8067 36.8439 34.62 36.6172 34.38C36.3905 34.14 36.2139 33.8533 36.0872 33.52C35.9672 33.1867 35.9072 32.82 35.9072 32.42C35.9072 32.0333 35.9705 31.6733 36.0972 31.34C36.2239 31.0067 36.4039 30.72 36.6372 30.48C36.8705 30.2333 37.1472 30.04 37.4672 29.9C37.7939 29.76 38.1539 29.69 38.5472 29.69C38.9872 29.69 39.3705 29.7767 39.6972 29.95C40.0239 30.1167 40.2905 30.3433 40.4972 30.63C40.7105 30.9167 40.8639 31.2433 40.9572 31.61C41.0505 31.9767 41.0839 32.36 41.0572 32.76H37.3272ZM45.5694 32.4C45.5694 32.1867 45.5494 31.9833 45.5094 31.79C45.4694 31.59 45.3994 31.4167 45.2994 31.27C45.206 31.1167 45.0827 30.9933 44.9294 30.9C44.776 30.8067 44.5827 30.76 44.3494 30.76C44.116 30.76 43.9194 30.8067 43.7594 30.9C43.5994 30.9933 43.4694 31.1167 43.3694 31.27C43.276 31.4233 43.206 31.6 43.1594 31.8C43.1194 31.9933 43.0994 32.1967 43.0994 32.41C43.0994 32.61 43.1227 32.81 43.1694 33.01C43.216 33.21 43.2894 33.39 43.3894 33.55C43.496 33.7033 43.626 33.83 43.7794 33.93C43.9394 34.0233 44.1294 34.07 44.3494 34.07C44.5827 34.07 44.776 34.0233 44.9294 33.93C45.0894 33.8367 45.216 33.7133 45.3094 33.56C45.4027 33.4 45.4694 33.22 45.5094 33.02C45.5494 32.82 45.5694 32.6133 45.5694 32.4ZM45.5894 34.34H45.5694C45.4027 34.62 45.1827 34.8233 44.9094 34.95C44.6427 35.07 44.3394 35.13 43.9994 35.13C43.6127 35.13 43.2727 35.0567 42.9794 34.91C42.686 34.7567 42.4427 34.5533 42.2494 34.3C42.0627 34.04 41.9194 33.7433 41.8194 33.41C41.726 33.0767 41.6794 32.73 41.6794 32.37C41.6794 32.0233 41.726 31.69 41.8194 31.37C41.9194 31.0433 42.0627 30.7567 42.2494 30.51C42.4427 30.2633 42.6827 30.0667 42.9694 29.92C43.256 29.7667 43.5894 29.69 43.9694 29.69C44.276 29.69 44.566 29.7567 44.8394 29.89C45.1194 30.0167 45.3394 30.2067 45.4994 30.46H45.5194V27.86H46.9394V35H45.5894V34.34ZM54.5859 32.42C54.5859 32.2 54.5625 31.99 54.5159 31.79C54.4692 31.59 54.3959 31.4133 54.2959 31.26C54.1959 31.1067 54.0692 30.9867 53.9159 30.9C53.7692 30.8067 53.5892 30.76 53.3759 30.76C53.1692 30.76 52.9892 30.8067 52.8359 30.9C52.6825 30.9867 52.5559 31.1067 52.4559 31.26C52.3559 31.4133 52.2825 31.59 52.2359 31.79C52.1892 31.99 52.1659 32.2 52.1659 32.42C52.1659 32.6333 52.1892 32.84 52.2359 33.04C52.2825 33.24 52.3559 33.4167 52.4559 33.57C52.5559 33.7233 52.6825 33.8467 52.8359 33.94C52.9892 34.0267 53.1692 34.07 53.3759 34.07C53.5892 34.07 53.7692 34.0267 53.9159 33.94C54.0692 33.8467 54.1959 33.7233 54.2959 33.57C54.3959 33.4167 54.4692 33.24 54.5159 33.04C54.5625 32.84 54.5859 32.6333 54.5859 32.42ZM50.7959 27.86H52.2159V30.46H52.2359C52.4092 30.1933 52.6425 30 52.9359 29.88C53.2359 29.7533 53.5459 29.69 53.8659 29.69C54.1259 29.69 54.3825 29.7433 54.6359 29.85C54.8892 29.9567 55.1159 30.12 55.3159 30.34C55.5225 30.56 55.6892 30.8433 55.8159 31.19C55.9425 31.53 56.0059 31.9367 56.0059 32.41C56.0059 32.8833 55.9425 33.2933 55.8159 33.64C55.6892 33.98 55.5225 34.26 55.3159 34.48C55.1159 34.7 54.8892 34.8633 54.6359 34.97C54.3825 35.0767 54.1259 35.13 53.8659 35.13C53.4859 35.13 53.1459 35.07 52.8459 34.95C52.5459 34.83 52.3192 34.6267 52.1659 34.34H52.1459V35H50.7959V27.86ZM59.4591 35.64C59.3058 36.06 59.0925 36.36 58.8191 36.54C58.5458 36.72 58.1658 36.81 57.6791 36.81C57.5325 36.81 57.3858 36.8033 57.2391 36.79C57.0991 36.7833 56.9558 36.7733 56.8091 36.76V35.59C56.9425 35.6033 57.0791 35.6167 57.2191 35.63C57.3591 35.6433 57.4991 35.6467 57.6391 35.64C57.8258 35.62 57.9625 35.5467 58.0491 35.42C58.1425 35.2933 58.1891 35.1533 58.1891 35C58.1891 34.8867 58.1691 34.78 58.1291 34.68L56.3191 29.83H57.8291L58.9991 33.37H59.0191L60.1491 29.83H61.6191L59.4591 35.64Z\"/><path d=\"M18.5312 47.6154C18.5312 45.6882 20.0523 44.2301 22.0626 44.2301C24.073 44.2301 25.5905 45.6882 25.5905 47.6401C25.593 47.8232 25.5757 48.006 25.5388 48.1855H20.3224C20.5416 49.0227 21.225 49.5673 22.1268 49.5673C22.8095 49.5673 23.3763 49.2631 23.673 48.8448H25.4879C24.9853 50.125 23.6835 51 22.0857 51C20.0523 51 18.5312 49.5419 18.5312 47.6154ZM20.3357 46.9808H23.8021C23.7088 46.5987 23.4853 46.2595 23.1688 46.0197C22.8523 45.7799 22.462 45.654 22.0626 45.6628C21.2117 45.6628 20.5646 46.182 20.3357 46.9808Z\"/><path d=\"M26.4435 44.3819H28.1188V45.3585C28.6346 44.6861 29.3431 44.2802 30.2066 44.2802H30.4509V45.9794H30.2066C28.8664 45.9794 28.1188 46.6765 28.1188 47.7651V50.8461H26.4435V44.3819Z\"/><path d=\"M30.9152 42.4423C30.9152 42.1665 31.0265 41.902 31.2247 41.707C31.4229 41.512 31.6917 41.4025 31.972 41.4025C32.2523 41.4025 32.5211 41.512 32.7193 41.707C32.9175 41.902 33.0288 42.1665 33.0288 42.4423C33.0288 42.7181 32.9175 42.9826 32.7193 43.1776C32.5211 43.3726 32.2523 43.4821 31.972 43.4821C31.6917 43.4821 31.4229 43.3726 31.2247 43.1776C31.0265 42.9826 30.9152 42.7181 30.9152 42.4423ZM31.1343 44.3819H32.8096V50.8482H31.1343V44.3819Z\"/><path d=\"M34.5736 45.8908H33.5426V44.3798H34.5736V43.862C34.5736 42.4197 35.4371 41.6436 37.0474 41.6436H37.8593V43.1545H37.215C36.6098 43.1545 36.2489 43.4842 36.2489 44.0543V44.3839H37.7692V45.8949H36.2489V50.8523H34.5736V45.8908Z\"/><path d=\"M38.5244 42.4423C38.5315 42.1712 38.6459 41.9136 38.8433 41.7244C39.0406 41.5351 39.3053 41.4292 39.5809 41.4292C39.8565 41.4292 40.1212 41.5351 40.3185 41.7244C40.5159 41.9136 40.6303 42.1712 40.6374 42.4423C40.6303 42.7134 40.5159 42.971 40.3185 43.1603C40.1212 43.3495 39.8565 43.4554 39.5809 43.4554C39.3053 43.4554 39.0406 43.3495 38.8433 43.1603C38.6459 42.971 38.5315 42.7134 38.5244 42.4423Z\"/><path d=\"M40.4182 44.3819H38.7429V50.8482H40.4182V44.3819Z\"/><path d=\"M41.3467 47.6154C41.3467 45.6882 42.7253 44.2301 44.5576 44.2301C44.9597 44.2237 45.3586 44.3019 45.7275 44.4593C46.0965 44.6167 46.427 44.8497 46.6971 45.1429V44.3819H48.3724V50.8482H46.6971V50.0872C46.427 50.3804 46.0965 50.6134 45.7275 50.7709C45.3586 50.9283 44.9597 51.0064 44.5576 51C42.7253 51 41.3467 49.5419 41.3467 47.6154ZM46.6943 47.6154C46.6943 46.601 45.9209 45.8297 44.8794 45.8297C43.8484 45.8297 43.0757 46.603 43.0757 47.6154C43.0757 48.6278 43.8484 49.4011 44.8794 49.4011C45.9209 49.4025 46.6943 48.6291 46.6943 47.6154Z\"/><path d=\"M49.4634 42.0989H51.1387V45.1428C51.4083 44.8496 51.7385 44.6163 52.1071 44.4585C52.4756 44.3008 52.8742 44.2222 53.2761 44.228C55.1056 44.228 56.4717 45.6861 56.4717 47.6133C56.4717 49.5405 55.1056 50.9979 53.2761 50.9979C52.8745 51.0043 52.4761 50.9263 52.1076 50.7693C51.739 50.6122 51.4087 50.3797 51.1387 50.0872V50.8482H49.4634V42.0989ZM54.7461 47.6147C54.7461 46.6003 53.986 45.829 52.9424 45.829C51.9114 45.829 51.138 46.6023 51.138 47.6147C51.138 48.627 51.9114 49.4004 52.9424 49.4004C53.9846 49.4024 54.7447 48.6291 54.7447 47.6154L54.7461 47.6147Z\"/><path d=\"M57.3247 42.0989H59V50.8482H57.3247V42.0989Z\"/><path d=\"M19.702 41.2644L16.498 49.0082L16.1287 49.1676C15.8062 49.3049 15.4488 49.1346 15.2966 48.7665L14.422 46.6573L16.4463 41.7637C16.6411 41.2953 17.0403 41 17.4759 41H19.5665C19.6796 41 19.7522 41.1429 19.702 41.2644Z\"/><path d=\"M10.7859 47.2644H8.29596C8.07678 47.2644 7.93368 46.9897 8.03141 46.7548L9.30393 43.6786L10.7859 47.2644Z\"/><path d=\"M16.257 49.592L16.0162 50.1745C15.9403 50.3624 15.8165 50.528 15.6567 50.6552C15.5012 50.7791 15.3075 50.8474 15.1074 50.8489H13.3176C12.9323 50.8489 12.5812 50.5879 12.4102 50.1751L11.2109 47.2644L11.0399 46.8523L9.51331 43.171H11.8482C12.2845 43.171 12.6824 43.467 12.8771 43.9348L14.2125 47.1642L14.9594 48.967C15.1953 49.5398 15.7552 49.8084 16.257 49.592Z\"/></g></g>"
8
+ end
9
+ end
10
+ end
@@ -3,6 +3,7 @@
3
3
  require_relative "frame_assets"
4
4
  require_relative "png_encoder"
5
5
  require_relative "rendering_assets"
6
+ require_relative "generated/horizontal_frame"
6
7
 
7
8
  module Verifiabl
8
9
  module Issuer
@@ -24,52 +25,66 @@ module Verifiabl
24
25
  SvgResult = Data.define(:svg, :width, :height, :content, :error_correction_level, :qr_version, :module_px, :degraded)
25
26
  PngResult = Data.define(:png, :width, :height, :content, :error_correction_level, :qr_version, :module_px, :degraded)
26
27
  Raster = Data.define(:rgba, :width, :height, :content, :error_correction_level, :qr_version, :module_px, :degraded)
27
- RasterContext = Data.define(:rgba, :width, :denominator, :num_x, :num_y, :pixel_width)
28
- SvgRequest = Data.define(:verifiabl_reference, :encrypted_pii, :width, :environment, :scan_base_url, :max_error_correction, :error_correction_level)
28
+ Geometry = Data.define(:viewbox_width, :height_units, :qr_x, :qr_y, :qr_units, :min_width, :png_width)
29
+ VERTICAL_GEOMETRY = Geometry.new(VIEWBOX_WIDTH, HEIGHT_UNITS, 0, QR_Y, QR_UNITS, MIN_WIDTH, 720)
30
+ # 96-unit QR, the same 7-unit gap as vertical, then an 85-unit panel.
31
+ # Corresponding widths give the same QR size in both layouts.
32
+ HORIZONTAL_GEOMETRY = Geometry.new(188, 96, 0, 0, 96, 940, 1410)
33
+ RasterContext = Data.define(:rgba, :width, :denominator, :num_x, :num_y, :pixel_width, :geometry)
34
+ SvgRequest = Data.define(:verifiabl_reference, :encrypted_pii, :width, :layout, :environment, :scan_base_url, :max_error_correction, :error_correction_level)
29
35
  PngRequest = Data.define(*SvgRequest.members, :compression_level)
30
- SVG_DEFAULTS = {width: MIN_WIDTH, environment: :production, scan_base_url: nil, max_error_correction: nil, error_correction_level: nil}.freeze
36
+ SVG_DEFAULTS = {width: MIN_WIDTH, layout: :vertical, environment: :production, scan_base_url: nil, max_error_correction: nil, error_correction_level: nil}.freeze
31
37
  PNG_DEFAULTS = SVG_DEFAULTS.merge(width: 720, compression_level: 6).freeze
32
- private_constant :Raster, :RasterContext, :SvgRequest, :PngRequest, :SVG_DEFAULTS, :PNG_DEFAULTS
38
+ private_constant :Raster, :RasterContext, :SvgRequest, :PngRequest, :SVG_DEFAULTS, :PNG_DEFAULTS, :Geometry, :VERTICAL_GEOMETRY, :HORIZONTAL_GEOMETRY
33
39
 
34
40
  module_function
35
41
 
36
42
  def svg(**options)
37
- request = SvgRequest.new(**SVG_DEFAULTS.merge(options))
38
- width = validate_svg_width(request.width)
43
+ geometry = geometry_for(options.fetch(:layout, :vertical))
44
+ request = SvgRequest.new(**SVG_DEFAULTS.merge(width: geometry.min_width).merge(options))
45
+ width = validate_svg_width(request.width, geometry.min_width)
39
46
  ladder = error_correction_ladder(request.max_error_correction, request.error_correction_level)
40
- qr = select_qr(request, width, ladder)
41
- module_size = QR_UNITS.to_f / qr.modules.length
42
- module_px = module_size * width / VIEWBOX_WIDTH
43
- build_svg_result(qr, ladder, width, module_size, module_px)
47
+ qr = select_qr(request, width, ladder, geometry)
48
+ build_svg_result(qr, ladder, width, geometry)
44
49
  end
45
50
 
46
- def build_svg_result(qr, ladder, width, module_size, module_px)
47
- height = round(width * HEIGHT_UNITS / VIEWBOX_WIDTH.to_f)
51
+ def build_svg_result(qr, ladder, width, geometry)
52
+ module_size = geometry.qr_units.to_f / qr.modules.length
53
+ module_px = module_size * width / geometry.viewbox_width
54
+ height = round(width * geometry.height_units / geometry.viewbox_width.to_f)
48
55
  SvgResult.new(
49
- svg: svg_document(qr.modules, width, height, module_size).freeze,
56
+ svg: svg_document(qr.modules, [width, height], module_size, geometry).freeze,
50
57
  width:,
51
58
  height:,
59
+ **qr_metadata(qr, ladder, module_px)
60
+ )
61
+ end
62
+ private_class_method :build_svg_result
63
+
64
+ def qr_metadata(qr, ladder, module_px)
65
+ {
52
66
  content: qr.content,
53
67
  error_correction_level: qr.error_correction_level,
54
68
  qr_version: qr.version,
55
69
  module_px: round(module_px),
56
70
  degraded: degraded?(qr.error_correction_level, ladder, module_px)
57
- )
71
+ }
58
72
  end
59
- private_class_method :build_svg_result
73
+ private_class_method :qr_metadata
60
74
 
61
- def svg_document(modules, width, height, module_size)
62
- header = %(<path d="M0 8C0 3.58172 3.58172 0 8 0H88C92.4183 0 96 3.58172 96 8V47H0V8Z" fill="#{NAVY}"/>) + render_header
63
- %(<svg xmlns="http://www.w3.org/2000/svg" width="#{number(width)}" height="#{number(height)}" viewBox="0 0 96 150" role="img" aria-label="Secured by Verifiabl verification barcode">) +
64
- %(<rect x="0" y="39" width="96" height="111" fill="#FFFFFF"/>) + header +
65
- %(<g transform="translate(0 54)"><g shape-rendering="crispEdges">) + render_modules(modules, module_size) +
75
+ def svg_document(modules, dimensions, module_size, geometry)
76
+ width, height = dimensions
77
+ frame = (geometry == HORIZONTAL_GEOMETRY) ? RenderingAssets::HORIZONTAL_FRAME : render_vertical_frame
78
+ %(<svg xmlns="http://www.w3.org/2000/svg" width="#{number(width)}" height="#{number(height)}" viewBox="0 0 #{geometry.viewbox_width} #{geometry.height_units}" role="img" aria-label="Secured by Verifiabl verification barcode">) + frame +
79
+ %(<g transform="translate(#{geometry.qr_x} #{geometry.qr_y})"><g shape-rendering="crispEdges">) + render_modules(modules, module_size) +
66
80
  %(</g>) + render_finders(modules.length, module_size) + %(</g></svg>)
67
81
  end
68
82
  private_class_method :svg_document
69
83
 
70
84
  def png(**options)
71
- request = PngRequest.new(**PNG_DEFAULTS.merge(options))
72
- validate_png_width(request.width)
85
+ geometry = geometry_for(options.fetch(:layout, :vertical))
86
+ request = PngRequest.new(**PNG_DEFAULTS.merge(width: geometry.png_width).merge(options))
87
+ validate_png_width(request.width, request.layout)
73
88
  ladder = error_correction_ladder(request.max_error_correction, request.error_correction_level)
74
89
  build_png_result(compose(request:, ladder:), request.compression_level)
75
90
  end
@@ -91,28 +106,30 @@ module Verifiabl
91
106
  def compose(**options)
92
107
  ladder = options.delete(:ladder)
93
108
  request = options.delete(:request) || PngRequest.new(**PNG_DEFAULTS.merge(options))
94
- qr = select_qr(request, request.width, ladder)
95
- rgba, raster_width, height = FrameAssets.raster(request.width)
96
- blit_qr(rgba, raster_width, qr.modules, request.width)
97
- build_raster(rgba, [raster_width, height], qr, ladder, request.width.to_f / qr.modules.length)
109
+ compose_qr(request, ladder, geometry_for(request.layout))
98
110
  end
99
111
  private_class_method :compose
100
112
 
113
+ def compose_qr(request, ladder, geometry)
114
+ qr = select_qr(request, request.width, ladder, geometry)
115
+ rgba, raster_width, height = FrameAssets.raster(request.width, layout: request.layout)
116
+ blit_qr(rgba, raster_width, qr.modules, request.width, geometry)
117
+ module_px = geometry.qr_units.to_f * request.width / (geometry.viewbox_width * qr.modules.length)
118
+ build_raster(rgba, [raster_width, height], qr, ladder, module_px)
119
+ end
120
+ private_class_method :compose_qr
121
+
101
122
  def build_raster(rgba, dimensions, qr, ladder, module_px)
102
123
  Raster.new(
103
124
  rgba: rgba.freeze,
104
125
  width: dimensions.fetch(0),
105
126
  height: dimensions.fetch(1),
106
- content: qr.content,
107
- error_correction_level: qr.error_correction_level,
108
- qr_version: qr.version,
109
- module_px: round(module_px),
110
- degraded: degraded?(qr.error_correction_level, ladder, module_px)
127
+ **qr_metadata(qr, ladder, module_px)
111
128
  )
112
129
  end
113
130
  private_class_method :build_raster
114
131
 
115
- def select_qr(request, width, ladder)
132
+ def select_qr(request, width, ladder, geometry)
116
133
  ladder.each do |level|
117
134
  begin
118
135
  qr = Qr.encode_scan_url(
@@ -125,7 +142,7 @@ module Verifiabl
125
142
  rescue Qr::CapacityError
126
143
  next
127
144
  end
128
- return qr if width.to_f / qr.modules.length >= MIN_MODULE_PX
145
+ return qr if geometry.qr_units.to_f * width / (geometry.viewbox_width * qr.modules.length) >= MIN_MODULE_PX
129
146
  end
130
147
  raise Qr::CapacityError, "The PII is too long to render a scannable barcode in the branded frame at width #{number(width)}, even at the lowest error correction"
131
148
  end
@@ -237,6 +254,12 @@ module Verifiabl
237
254
  end
238
255
  private_class_method :rounded_rect_left
239
256
 
257
+ def render_vertical_frame
258
+ header = %(<path d="M0 8C0 3.58172 3.58172 0 8 0H88C92.4183 0 96 3.58172 96 8V47H0V8Z" fill="#{NAVY}"/>) + render_header
259
+ %(<rect x="0" y="39" width="96" height="111" fill="#FFFFFF"/>) + header
260
+ end
261
+ private_class_method :render_vertical_frame
262
+
240
263
  def render_header
241
264
  secured_by = RenderingAssets::SECURED_BY_PATH.sub("{color}", "#FFFFFF")
242
265
  wordmark = %(<g transform="translate(8 23) scale(1)" fill="#FFFFFF">#{RenderingAssets::WORDMARK_PATHS}</g>)
@@ -244,8 +267,8 @@ module Verifiabl
244
267
  end
245
268
  private_class_method :render_header
246
269
 
247
- def blit_qr(rgba, raster_width, modules, pixel_width)
248
- context = raster_context(rgba, raster_width, modules.length, pixel_width)
270
+ def blit_qr(rgba, raster_width, modules, pixel_width, geometry)
271
+ context = raster_context(rgba, raster_width, modules.length, pixel_width, geometry)
249
272
  edges_x, edges_y = raster_edges(context, modules.length)
250
273
  draw_raster_modules(context, modules, edges_x, edges_y)
251
274
 
@@ -256,11 +279,13 @@ module Verifiabl
256
279
  end
257
280
  private_class_method :blit_qr
258
281
 
259
- def raster_context(rgba, width, size, pixel_width)
260
- denominator = VIEWBOX_WIDTH * size
261
- num_x = ->(position) { pixel_width * QR_UNITS * position }
262
- num_y = ->(position) { pixel_width * (QR_Y * size + QR_UNITS * position) }
263
- RasterContext.new(rgba:, width:, denominator:, num_x:, num_y:, pixel_width:)
282
+ # Keep module edges and finder coverage in exact rational/integer units.
283
+ # Floating-point raster geometry would break cross-SDK pixel parity.
284
+ def raster_context(rgba, width, size, pixel_width, geometry)
285
+ denominator = geometry.viewbox_width * size
286
+ num_x = ->(position) { pixel_width * (geometry.qr_x * size + geometry.qr_units * position) }
287
+ num_y = ->(position) { pixel_width * (geometry.qr_y * size + geometry.qr_units * position) }
288
+ RasterContext.new(rgba:, width:, denominator:, num_x:, num_y:, pixel_width:, geometry:)
264
289
  end
265
290
  private_class_method :raster_context
266
291
 
@@ -295,8 +320,8 @@ module Verifiabl
295
320
  private_class_method :render_raster_finder
296
321
 
297
322
  def raster_finder_shapes(context, module_x, module_y)
298
- module_q = QR_UNITS * context.pixel_width * 80
299
- radius_unit = QR_UNITS * context.pixel_width
323
+ module_q = context.geometry.qr_units * context.pixel_width * 80
324
+ radius_unit = context.geometry.qr_units * context.pixel_width
300
325
  outer = raster_finder_outer(context, module_x, module_y, module_q, radius_unit)
301
326
  {outer:, inner: inset_finder_shape(outer, module_q, 1, 80 * radius_unit), dot: inset_finder_shape(outer, module_q, 2, 52 * radius_unit)}
302
327
  end
@@ -385,18 +410,27 @@ module Verifiabl
385
410
  end
386
411
  private_class_method :fill_black
387
412
 
388
- def validate_svg_width(value)
389
- unless value.is_a?(Numeric) && value.finite? && value >= MIN_WIDTH
390
- raise ArgumentError, "width must be at least #{MIN_WIDTH}"
413
+ def geometry_for(layout)
414
+ return VERTICAL_GEOMETRY if layout == :vertical
415
+ return HORIZONTAL_GEOMETRY if layout == :horizontal
416
+
417
+ raise ArgumentError, "layout must be :vertical or :horizontal"
418
+ end
419
+ private_class_method :geometry_for
420
+
421
+ def validate_svg_width(value, minimum)
422
+ unless value.is_a?(Numeric) && value.finite? && value >= minimum
423
+ raise ArgumentError, "width must be at least #{minimum}"
391
424
  end
392
425
  value
393
426
  end
394
427
  private_class_method :validate_svg_width
395
428
 
396
- def validate_png_width(value)
397
- return value if value.is_a?(Integer) && FrameAssets::SUPPORTED_WIDTHS.include?(value)
429
+ def validate_png_width(value, layout)
430
+ widths = FrameAssets.supported_widths(layout)
431
+ return value if value.is_a?(Integer) && widths.include?(value)
398
432
 
399
- raise ArgumentError, "width must be one of #{FrameAssets::SUPPORTED_WIDTHS.join(", ")}"
433
+ raise ArgumentError, "width must be one of #{widths.join(", ")}"
400
434
  end
401
435
  private_class_method :validate_png_width
402
436
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Verifiabl
4
4
  module Issuer
5
- VERSION = "0.3.0"
5
+ VERSION = "0.4.0"
6
6
  end
7
7
  end
@@ -59,10 +59,22 @@ module Verifiabl
59
59
  Payload.scan_url(**options)
60
60
  end
61
61
 
62
+ # Render a branded SVG. Vertical is the default; horizontal puts the QR
63
+ # on the left and the light-tinted brand panel on the right.
64
+ # @param options [Hash] rendering and scan-URL options
65
+ # @option options [Symbol] :layout (:vertical) :vertical or :horizontal
66
+ # @option options [Numeric] :width minimum/default 480 vertical, 940 horizontal
67
+ # @return [Rendering::SvgResult] deterministic SVG and QR metadata
62
68
  def build_barcode_svg(**options)
63
69
  Rendering.svg(**options)
64
70
  end
65
71
 
72
+ # Render a deterministic branded PNG from a pre-rasterised frame.
73
+ # @param options [Hash] rendering and scan-URL options
74
+ # @option options [Symbol] :layout (:vertical) :vertical or :horizontal
75
+ # @option options [Integer] :width default 720 vertical, 1410 horizontal;
76
+ # supported widths are 480/720/960/1440 and 940/1410/1880/2820 respectively
77
+ # @return [Rendering::PngResult] deterministic PNG and QR metadata
66
78
  def build_barcode_png(**options)
67
79
  Rendering.png(**options)
68
80
  end
@@ -66,8 +66,8 @@ module Verifiabl
66
66
  def self.generate_verifiabl_reference: () -> String
67
67
  def self.build_barcode_payload: (verifiabl_reference: String, encrypted_pii: binary) -> String
68
68
  def self.build_scan_url: (verifiabl_reference: String, encrypted_pii: binary, ?environment: Symbol | String, ?scan_base_url: String?) -> String
69
- def self.build_barcode_svg: (verifiabl_reference: String, encrypted_pii: binary, ?width: Numeric, ?environment: Symbol | String, ?scan_base_url: String?, ?max_error_correction: Symbol | String, ?error_correction_level: Symbol | String) -> Rendering::SvgResult
70
- def self.build_barcode_png: (verifiabl_reference: String, encrypted_pii: binary, ?width: Integer, ?environment: Symbol | String, ?scan_base_url: String?, ?max_error_correction: Symbol | String, ?error_correction_level: Symbol | String, ?compression_level: Integer) -> Rendering::PngResult
69
+ def self.build_barcode_svg: (verifiabl_reference: String, encrypted_pii: binary, ?width: Numeric, ?layout: :vertical | :horizontal, ?environment: Symbol | String, ?scan_base_url: String?, ?max_error_correction: Symbol | String, ?error_correction_level: Symbol | String) -> Rendering::SvgResult
70
+ def self.build_barcode_png: (verifiabl_reference: String, encrypted_pii: binary, ?width: Integer, ?layout: :vertical | :horizontal, ?environment: Symbol | String, ?scan_base_url: String?, ?max_error_correction: Symbol | String, ?error_correction_level: Symbol | String, ?compression_level: Integer) -> Rendering::PngResult
71
71
 
72
72
  class PreparedV2Payslip
73
73
  attr_reader verifiabl_reference: String
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: verifiabl-issuer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Verifiabl
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-10-04 00:00:00.000000000 Z
11
+ date: 2026-10-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base64
@@ -167,6 +167,10 @@ files:
167
167
  - lib/verifiabl/issuer/assets/frame-480.vfr1
168
168
  - lib/verifiabl/issuer/assets/frame-720.vfr1
169
169
  - lib/verifiabl/issuer/assets/frame-960.vfr1
170
+ - lib/verifiabl/issuer/assets/frame-horizontal-1410.vfr1
171
+ - lib/verifiabl/issuer/assets/frame-horizontal-1880.vfr1
172
+ - lib/verifiabl/issuer/assets/frame-horizontal-2820.vfr1
173
+ - lib/verifiabl/issuer/assets/frame-horizontal-940.vfr1
170
174
  - lib/verifiabl/issuer/base32.rb
171
175
  - lib/verifiabl/issuer/client.rb
172
176
  - lib/verifiabl/issuer/configuration.rb
@@ -174,6 +178,7 @@ files:
174
178
  - lib/verifiabl/issuer/errors.rb
175
179
  - lib/verifiabl/issuer/frame_assets.rb
176
180
  - lib/verifiabl/issuer/generated/frame_widths.rb
181
+ - lib/verifiabl/issuer/generated/horizontal_frame.rb
177
182
  - lib/verifiabl/issuer/generated/jurisdiction_pii_profiles.rb
178
183
  - lib/verifiabl/issuer/generated/non-pii-v2-fields.json
179
184
  - lib/verifiabl/issuer/generated/pii_text_profile.rb