qr_forge 1.0.0 → 1.1.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: 379423936d868bc8e4589d3483cc412c1bd6ec85bd6b9ae82e9ad58dfeb03903
4
- data.tar.gz: 815cf224b5876c0222e8449b5edcbcc35d4f00c53d800f0a4cddb54d31f42735
3
+ metadata.gz: 39cc8a8482cee2898925eab861d71d2a96e778d61d34e3e3e6d2774f8ec3fa95
4
+ data.tar.gz: d217725f654c5b08ee9e7addb7d04f04a8b7e9495b2809210c5ead98e933deb2
5
5
  SHA512:
6
- metadata.gz: 75c947677801f0d3d4469d3544206f9f3d0755ef90444b2eec312cf6373a0a2e10661d86a7f97d574ff69d05accb797299699d9bfd6f517b74130ecfe5393765
7
- data.tar.gz: b31403d7730bb7f2bca48f237b8e99fd1a96414c470e24485164dd6046990e337a385bae68eb8832cff3cb2504b3fccdf0ffb5553b91a5d8bc70c7037e0bc10a
6
+ metadata.gz: cc85e4a4f63070f81e69d2a7c715b17a9fccb5dd19469a4a72dd37ebfb40245b073b79e2a35357b5b0edfb73ca812f32be6651cf7afb496fc0af419093b87c9e
7
+ data.tar.gz: 48a38d7344bdc09748a0bd546a3466f4e403a90611ef85d40c6f9418cbf569f18b7c797d4dae346e8b192d14ed6ae836020bceddc51a855e1dc700b2dfa70540
data/README.md CHANGED
@@ -1,39 +1,195 @@
1
- # QrForge
1
+ # !! Docs are still being written !! #
2
+ ![output](https://github.com/user-attachments/assets/71edb1aa-2182-4cd3-bb28-ec4f71336029)
2
3
 
3
- TODO: Delete this and the text below, and describe your gem
4
+ An example of the output of QR Forge
4
5
 
5
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/qr_forge`. To experiment with that code, run `bin/console` for an interactive prompt.
6
+ # QR Forge
6
7
 
7
- ## Installation
8
+ This library is a QR Code renderer that lets you customize almost every aspect of a QR Code. From module design to a logo, this library is meant to be design-extensible, which means you can render your own components such as modules, the outer eyes and inner eyes etc. in your own custom SVG shapes.
8
9
 
9
- TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
10
+ # Installation
10
11
 
11
- Install the gem and add to the application's Gemfile by executing:
12
+ ```shell
13
+ gem install qr_forge
14
+ ```
15
+
16
+ or if using bundler
17
+
18
+ ```shell
19
+ bundle add qr_forge
20
+ ```
21
+
22
+ Note: To export in the PNG format you will also want to install vips on your machine
12
23
 
13
- ```bash
14
- bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
24
+ ```shell
25
+ brew install vips
15
26
  ```
16
27
 
17
- If bundler is not being used to manage dependencies, install the gem by executing:
28
+ # Usage
18
29
 
19
- ```bash
20
- gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
30
+ To create a QR Code you can simply do the following:
31
+
32
+ ```ruby
33
+ QrForge::Forge.build(text: "https://yourlinkhere.com")
34
+ ```
35
+
36
+ If you want an (SVG) QRCode fast, that's all there is to it. Everything else is covered by default values, however, you can tweak anything you want, which we will go through in the next section.
37
+
38
+ ## Configuration
39
+
40
+ The library tries to provide sensible defaults, but defaults can be subjective, so the following shows a configuration object that you can pass to the QR Code.
41
+
42
+ ```ruby
43
+ QrForge::Forge.build(
44
+ text: "https://www.google.com",
45
+ config: {
46
+ qr: { version: 10 },
47
+ components: { inner_eye: QrForge::Components::EyeInner::Square },
48
+ design: {
49
+ size: 800,
50
+ colors: { module: 'blue', outer_eye: 'cyan', inner_eye: 'skyblue' },
51
+ image: Base64.strict_encode64(...)
52
+ },
53
+ output: { format: :png }
54
+ }
55
+ )
21
56
  ```
22
57
 
23
- ## Usage
58
+ ##### QR
59
+
60
+ This hash provides details to the underlying QR code generator (see: https://github.com/whomwah/rqrcode_core).
61
+
62
+ _version_ dictates the module count or size of the QR Code and how much data it can hold. It does not necessarily dictate the dimensions, however, it will be unreadable if you choose too small of a size, but a larger QR code version. (see: https://www.qrcode.com/en/about/version.html)
24
63
 
25
- TODO: Write usage instructions here
64
+ ##### Components
26
65
 
27
- ## Development
66
+ This hash can have up to 3 components:
67
+ - outer_eye
68
+ <img width="77" alt="Screenshot 2025-06-17 at 1 23 39 AM" src="https://github.com/user-attachments/assets/ccef3f08-cc4b-43c7-95b4-8d6c707f5f5a" />
28
69
 
29
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
70
+ - inner_eye
71
+ <img width="77" alt="Screenshot 2025-06-17 at 1 24 22 AM" src="https://github.com/user-attachments/assets/c21bb175-00c2-4179-a20a-4e505ad37df1" />
30
72
 
31
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
73
+ - module
74
+ <img width="216" alt="Screenshot 2025-06-17 at 1 25 24 AM" src="https://github.com/user-attachments/assets/f670d5fd-e2dc-42cc-9bff-d77a51d65fa1" />
32
75
 
33
- ## Contributing
76
+ This is what the circle outer_eye component looks like:
77
+
78
+ ```ruby
79
+ # frozen_string_literal: true
80
+
81
+ module QrForge
82
+ module Components
83
+ module EyeOuter
84
+ class Circle < ForgeComponent
85
+ DEFAULT_STROKE_WIDTH = 1.0
86
+
87
+ # @see ForgeComponent#draw
88
+ # Draws a circle that fills the full 'area' box, inset by half the stroke so it
89
+ # never overlaps the modules beneath.
90
+ def draw(y:, x:, quiet_zone:, area:, color: "black", **_)
91
+ stroke_width = DEFAULT_STROKE_WIDTH
92
+
93
+ # Radius = (full width of box – one stroke) / 2
94
+ r = (area - stroke_width) / 2.0
95
+
96
+ # Center of the N×N box (plus quiet_zone offset)
97
+ cx = x + quiet_zone + (area / 2.0)
98
+ cy = y + quiet_zone + (area / 2.0)
99
+
100
+ @xml_builder.circle(
101
+ cx: cx,
102
+ cy: cy,
103
+ r: r,
104
+ 'stroke-width': stroke_width,
105
+ stroke: color,
106
+ fill: "transparent",
107
+ test_id: @test_id
108
+ )
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
114
+ ```
115
+
116
+ And the square for comparison:
117
+
118
+ ```ruby
119
+ # frozen_string_literal: true
120
+
121
+ module QrForge
122
+ module Components
123
+ module EyeOuter
124
+ class Square < ForgeComponent
125
+ # @see ForgeComponent#draw
126
+ def draw(y:, x:, quiet_zone:, area:, color: "black", **_)
127
+ x += quiet_zone
128
+ y += quiet_zone
129
+
130
+ # Draw the outer black square (7x7)
131
+ @xml_builder.rect(
132
+ x:,
133
+ y:,
134
+ width: area,
135
+ height: area,
136
+ fill: color
137
+ )
138
+
139
+ # Draw the inner (cutout) square (5x5) to create the finder pattern
140
+ inset = 1
141
+ inner = area - 2
142
+
143
+ @xml_builder.rect(
144
+ x: x + inset,
145
+ y: y + inset,
146
+ width: inner,
147
+ height: inner,
148
+ fill: color,
149
+ test_id: @test_id
150
+ )
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
156
+ ```
157
+
158
+ All components must inherit from the base ForgeComponent. You can look at that to see what is available to be used (this is on the roadmap to allow more design variations).
159
+
160
+ If you create your own designs, follow the same pattern and pass them into the components config. It will merge them with the default components, so if you only want to change one or two of the components, but not all three, you can!
161
+
162
+ ##### Design
163
+
164
+ ```
165
+ size: 800
166
+ ```
167
+
168
+ This sets the width and height of the svg. I recommend that you set this to a higher value than you plan to use as the SVG will scale well without needing to use any image processing like vips. If you do want to use your own strategies for image processing, I still recommend a higher size and do with the SVG as you wish.
169
+
170
+ ```
171
+ colors: { module: 'blue', outer_eye: 'cyan', inner_eye: 'skyblue' }
172
+ ```
173
+
174
+ This will set the fill or stroke appropriately for the respective component.
175
+
176
+ ```
177
+ image: ...
178
+ ```
179
+
180
+ A base64 encoded image that will be placed in the center of your QR Code. This scales with the version of the QR Code and is strictly set as to make sure that we do not remove more data that can be recovered through the error correcting algorithms.
181
+
182
+ ##### Output
183
+
184
+ ```
185
+ output: { format: :png }})
186
+ ```
34
187
 
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/qr_forge.
188
+ This will use vips to process the svg and return it as a PNG. If you want to do your own image processing, you can leave this off as the default is the raw SVG string.
36
189
 
37
- ## License
38
190
 
39
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
191
+ ## Roadmap
192
+ - Color gradients for components
193
+ - More data types i.e. wifi, passkey etc.
194
+ - Image background and shape improvements
195
+ - More default component selections
@@ -4,21 +4,25 @@ module QrForge
4
4
  #
5
5
  # Entry point for building QRCodes
6
6
  class Forge
7
- def initialize(text:, config:)
7
+ def initialize(data:, type:, config:)
8
8
  version = config.dig(:qr, :version)
9
9
 
10
- @data = QrForge::QrData.new(text:, version:)
10
+ @data = QrForge::QrData.new(text: QrForge::Payload.build(data:, type:).to_s, version:)
11
11
  @renderer = QrForge::Renderer.new(qr_data: @data, config:)
12
12
  @exporter = QrForge::Exporter.new(config:)
13
13
  end
14
14
 
15
15
  #
16
16
  # Builds a QR code with the given parameters.
17
- # @param text [String] The text/data to encode in the QR code
18
- # @param size [Integer] The size of the QR code in modules [1-40]
17
+ # @param data [String, Hash] The data to encode in the QR code. This can be a string or a hash for specific payloads.
18
+ # @param type [Symbol] The type of QR code to build (e.g., :plain, :url)
19
+ # @param config [Hash] Configuration options for the QR code, including design and export settings.
20
+ # - `:qr` - QR code specific settings (e.g., version).
21
+ # - `:design` - Design specific settings (e.g., colors, shapes).
22
+ # - `:output` - Export specific settings (e.g., format).
19
23
  # @return [String, StringIO] The SVG or PNG representation of the QR code
20
- def self.build(text:, config: {})
21
- new(text:, config:).build
24
+ def self.build(data:, type: :url, config: {})
25
+ new(data:, type:, config:).build
22
26
  end
23
27
 
24
28
  def build
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QrForge
4
+ #
5
+ # Payload is a factory class that builds different types of payloads based on the provided type and data.
6
+ # It will validate the payload data (based on the type) and return a string representation of the payload.
7
+ class Payload
8
+ PAYLOAD_TYPES = {
9
+ wifi: ::QrForge::Payloads::Wifi,
10
+ plain: ::QrForge::Payloads::PlainText,
11
+ url: ::QrForge::Payloads::Url,
12
+ geo: ::QrForge::Payloads::Geo,
13
+ phone: ::QrForge::Payloads::Phone
14
+ }.freeze
15
+
16
+ #
17
+ # Builds a payload based on the type and data provided.
18
+ # @param type [Symbol] The type of payload to build (e.g., :url, :wifi).
19
+ # @param data [String, Hash] The data to encode in the payload.
20
+ # @return [String] The string representation of the payload.
21
+ def self.build(type:, data:)
22
+ klass = PAYLOAD_TYPES[type.to_sym]
23
+
24
+ raise ArgumentError "Invalid payload type: #{type}" unless klass
25
+
26
+ payload = case data
27
+ when Hash
28
+ klass.new(**data)
29
+ when String
30
+ klass.new(data)
31
+ else
32
+ raise ArgumentError, "Invalid data type: #{data.class}. Expected Hash or String."
33
+ end
34
+
35
+ payload.validate!
36
+ payload
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ puts "[Geo] file loaded"
4
+
5
+ module QrForge
6
+ module Payloads
7
+ #
8
+ # Represents a geo lat/long payload
9
+ # @example return "geo:40.712776,-74.005974"
10
+ class Geo
11
+ def initialize(latitude:, longitude:)
12
+ @latitude = latitude
13
+ @longitude = longitude
14
+ end
15
+
16
+ def to_s
17
+ "geo:#{@latitude},#{@longitude}"
18
+ end
19
+
20
+ #
21
+ # Validates that the passed latitude and longitude are within valid ranges.
22
+ def validate!
23
+ return if (-90..90).cover?(@latitude.to_f) && (-180..180).cover?(@longitude.to_f)
24
+
25
+ raise PayloadValidationError, "Latitude or longitude out of range"
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,35 @@
1
+ require 'uri'
2
+
3
+ module QrForge
4
+ module Payloads
5
+ #
6
+ # Represents a telephone payload
7
+ # @example return "https://example.com" or "http://example.com"
8
+ class Phone
9
+
10
+ def initialize(phone_number)
11
+ @phone_number = phone_number
12
+ end
13
+
14
+ def to_s
15
+ "tel:#{@phone_number}"
16
+ end
17
+
18
+ #
19
+ # Validates that the passed phone number is in a valid format.
20
+ # @example
21
+ # valid phones:
22
+ # +919367788755
23
+ # 8989829304
24
+ # +16308520397
25
+ # 786-307-3615
26
+ # 555.555.5555
27
+ def validate!
28
+ # credit: https://ihateregex.io/expr/phone/
29
+ return if @phone_number =~ /^[+]?[(]?[0-9]{3}[)]?[-\s.]?[0-9]{3}[-\s.]?[0-9]{4,6}$/
30
+
31
+ raise PayloadValidationError, "Invalid phone number format"
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QrForge
4
+ module Payloads
5
+ #
6
+ # Represents a plain text payload
7
+ # @example return "Hello, World!"
8
+ class PlainText
9
+ def initialize(text)
10
+ @text = text
11
+ end
12
+
13
+ def to_s
14
+ @text
15
+ end
16
+
17
+ #
18
+ # Validates that the passed data is a string.
19
+ def validate!
20
+ raise PayloadValidationError, "Must be a valid string" unless @text.is_a?(String)
21
+ raise PayloadValidationError, "String cannot be empty" if @text.empty?
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,31 @@
1
+ require 'uri'
2
+
3
+ module QrForge
4
+ module Payloads
5
+ #
6
+ # Represents a URL payload
7
+ # @example return "https://example.com" or "http://example.com"
8
+ class Url
9
+
10
+ def initialize(url)
11
+ @url = url
12
+ end
13
+
14
+ def to_s
15
+ @url
16
+ end
17
+
18
+ #
19
+ # Validates that the passed url is a valid HTTP or HTTPS URL.
20
+ def validate!
21
+ uri = URI.parse(@url)
22
+
23
+ unless uri.is_a?(::URI::HTTP) || uri.is_a?(::URI::HTTPS)
24
+ raise PayloadValidationError, "Must be a valid HTTP/HTTPS URL"
25
+ end
26
+ rescue ::URI::InvalidURIError
27
+ raise PayloadValidationError, "Invalid URL syntax"
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QrForge
4
+ module Payloads
5
+ # Represents a Wi-Fi network payload
6
+ # @example returns WIFI:T:<encryption>;S:<ssid>;P:<password>;H:<hidden>;;
7
+ class Wifi
8
+ VALID_ENCRYPTIONS = %w[WEP WPA WPA2 WPA3].freeze
9
+ NO_PASSWORD_ENCRYPTION = "NOPASS"
10
+
11
+ def initialize(encryption:, ssid:, password: nil, hidden: false)
12
+ @encryption = encryption.upcase
13
+ @ssid = ssid
14
+ @password = password
15
+ @hidden = hidden
16
+ end
17
+
18
+ #
19
+ # Encryption type for the Wi-Fi network.
20
+ # @return [String, nil] The encryption type or nil if no password is set.
21
+ def encryption_type
22
+ "T:#{@encryption}" unless @encryption == NO_PASSWORD_ENCRYPTION
23
+ end
24
+
25
+ #
26
+ # SSID of the Wi-Fi network.
27
+ # @return [String] The SSID
28
+ def ssid
29
+ "S:#{escape(@ssid)}"
30
+ end
31
+
32
+ #
33
+ # Password for the Wi-Fi network.
34
+ # @return [String, nil] The password or nil if no password is set.
35
+ def password
36
+ "P:#{escape(@password)}" if @password && @encryption != NO_PASSWORD_ENCRYPTION
37
+ end
38
+
39
+ #
40
+ # Indicates if the Wi-Fi network is hidden
41
+ # @return [String, nil] "H:true" if hidden, nil otherwise
42
+ def hidden
43
+ "H:true" unless @hidden.nil? || @hidden == false
44
+ end
45
+
46
+ #
47
+ # Returns the parts of the Wi-Fi payload as an array.
48
+ # @return [Array<String>] The parts of the Wi-Fi payload
49
+ def parts
50
+ parts = []
51
+ parts << ssid
52
+ parts << encryption_type
53
+ parts << password
54
+ parts << hidden
55
+
56
+ parts.compact
57
+ end
58
+
59
+ def to_s
60
+ "WIFI:#{parts.join(";")};;"
61
+ end
62
+
63
+ #
64
+ # Validates the Wi-Fi payload data.
65
+ # @raise [PayloadValidationError] if the SSID is blank or if the password is required but not provided.
66
+ # @raise [PayloadValidationError] if the encryption type is invalid.
67
+ def validate!
68
+ raise PayloadValidationError, "SSID is required" if blank?(@ssid)
69
+
70
+ if @encryption != NO_PASSWORD_ENCRYPTION && blank?(@password)
71
+ raise PayloadValidationError, "Password is required for #{@encryption} networks"
72
+ end
73
+
74
+ return if VALID_ENCRYPTIONS.include?(@encryption) || @encryption == NO_PASSWORD_ENCRYPTION
75
+
76
+ raise PayloadValidationError, "Invalid encryption: #{@encryption}"
77
+ end
78
+
79
+ private
80
+
81
+ #
82
+ # Escapes special characters in the Wi-Fi payload.
83
+ # @param str [String, NilClass] The string to escape
84
+ # @return [String] The escaped string
85
+ def escape(str)
86
+ return "" if blank?(str)
87
+
88
+ # https://github.com/zxing/zxing/wiki/Barcode-Contents#wi-fi-network-config-android-ios-11
89
+ # Escape characters: \ ; , : " as they are valid in SSID but are used as delimiters in the payload format
90
+ str.to_s.gsub(/([\\;,:"])/) { "\\#{::Regexp.last_match(1)}" }
91
+ end
92
+
93
+ #
94
+ # Checks if a value is blank.
95
+ # @param val [String, nil] The value to check
96
+ # @return [Boolean] true if the value is blank, false otherwise
97
+ def blank?(val)
98
+ val.nil? || val.strip == ""
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,5 @@
1
+ module QrForge
2
+ module Payloads
3
+ class PayloadValidationError < StandardError; end
4
+ end
5
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module QrForge
4
- VERSION = "1.0.0"
4
+ VERSION = "1.1.0"
5
5
  end
data/lib/qr_forge.rb CHANGED
@@ -1,7 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "zeitwerk"
4
- loader = Zeitwerk::Loader.for_gem
5
- loader.setup
6
4
 
7
- module QrForge;end
5
+ # Entry point for the QrForge gem.
6
+ module QrForge
7
+ def self.loader
8
+ @loader ||= Zeitwerk::Loader.for_gem.tap do |loader|
9
+ loader.tag = "qr_forge"
10
+ loader.inflector = Zeitwerk::GemInflector.new(__FILE__)
11
+ loader.setup
12
+ end
13
+ end
14
+
15
+ loader
16
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: qr_forge
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Keegankb93
@@ -116,6 +116,13 @@ files:
116
116
  - lib/qr_forge/exporter.rb
117
117
  - lib/qr_forge/forge.rb
118
118
  - lib/qr_forge/layout.rb
119
+ - lib/qr_forge/payload.rb
120
+ - lib/qr_forge/payloads.rb
121
+ - lib/qr_forge/payloads/geo.rb
122
+ - lib/qr_forge/payloads/phone.rb
123
+ - lib/qr_forge/payloads/plain_text.rb
124
+ - lib/qr_forge/payloads/url.rb
125
+ - lib/qr_forge/payloads/wifi.rb
119
126
  - lib/qr_forge/qr_data.rb
120
127
  - lib/qr_forge/renderer.rb
121
128
  - lib/qr_forge/version.rb