skrift 0.1.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: fd0c67b57871e673fb91b56bd67172fc1f588b69d37dd777cd89ac65fc04a056
4
- data.tar.gz: b31c46b7d93b1794e72bb894deb73ca54d63bfc641a7c0e3b9f72d390f97a30b
3
+ metadata.gz: 9194573a4c3814c4c48eb0f026274a10f43856fc4c480d7d0631967d7b971fcd
4
+ data.tar.gz: 42646ed388052afa1095fdf4ae4fc700630d5e44a5174f576ace7abed88d0fa5
5
5
  SHA512:
6
- metadata.gz: 87c484791a46b5e82819d12e6c2633bdd12704f92f78e282f1b97cd89931e52827ec4e97e2b2bed52518cda038adad0a77a49105976f6154fa9e85801e04b529
7
- data.tar.gz: a21dd2bd6a20d90639da4264b186d8bae76675e2d9ef4d44a1438d40a4df912396dba0bb3c03aadd58c8518f408e99e3d27bb3a5dae4ff1afc8ad38f238ec308
6
+ metadata.gz: 506d51fdfca628d4ce5ed43cd1d84bbf6b3d828ab12673e94ac68c540155af8f7cf17f23558e603d07bf18b448a2cb86e1f3b6517f2fb5922e4159d35fdc8676
7
+ data.tar.gz: cb719b47e60d4c9335bfb218c66a9900f9833375b20f17687fdb466390af21e76fe435d8ea90470a04d3ef88c57c637816e29d346a8d56227bf2b368c05978bb
data/Rakefile CHANGED
@@ -5,4 +5,9 @@ require "rspec/core/rake_task"
5
5
 
6
6
  RSpec::Core::RakeTask.new(:spec)
7
7
 
8
+ desc "Regenerate the committed golden render buffers (review PGM diffs before committing)"
9
+ task :regen_goldens do
10
+ ruby "spec/support/regen_goldens.rb"
11
+ end
12
+
8
13
  task default: :spec
@@ -0,0 +1,96 @@
1
+ # Bypass bundler
2
+ ENV['BUNDLE_GEMFILE'] = nil
3
+
4
+ require 'pp'
5
+ require_relative '../lib/skrift'
6
+
7
+ # Function to render a string as a text banner
8
+ def render_text_banner(text, font_path = "resources/Ubuntu-Regular.ttf", scale = 20)
9
+ # Load the font
10
+ f = Font.load(font_path)
11
+
12
+ # Create a new SFT instance
13
+ sft = SFT.new(f)
14
+ sft.x_scale = scale
15
+ sft.y_scale = scale
16
+ sft.x_offset = 0
17
+ sft.y_offset = 0
18
+
19
+ # Get font metrics
20
+ metrics = sft.lmetrics
21
+
22
+ # Calculate the maximum height needed for any character
23
+ max_height = metrics.ascender - metrics.descender + metrics.line_gap
24
+
25
+ # Array to store each row of pixels for the banner
26
+ banner_rows = []
27
+
28
+ # Initialize the banner with empty rows
29
+ max_height.to_i.times do
30
+ banner_rows << []
31
+ end
32
+
33
+ # Process each character in the text
34
+ text.each_char do |char|
35
+ # Get the glyph ID for this character
36
+ gid = sft.lookup(char.ord)
37
+
38
+ # Skip if glyph not found
39
+ next if gid.nil?
40
+
41
+ # Get metrics for this glyph
42
+ gmetrics = sft.gmetrics(gid)
43
+
44
+ # Create an image for this glyph
45
+ img = Image.new(gmetrics.min_width, gmetrics.min_height)
46
+
47
+ # Render the glyph
48
+ if sft.render(gid, img)
49
+ # Calculate vertical offset based on metrics
50
+ y_offset = metrics.ascender - gmetrics.y_offset
51
+
52
+ # Calculate character width including spacing
53
+ char_width = img.width + 2 # Width plus spacing
54
+
55
+ # First, add empty space for all rows in the banner for this character
56
+ banner_rows.each do |row|
57
+ row.concat([0] * char_width)
58
+ end
59
+
60
+ # Then, overwrite with actual character data where it exists
61
+ img.height.times do |row|
62
+ row_in_banner = row + y_offset.to_i
63
+
64
+ # Skip rows outside the banner bounds
65
+ next if row_in_banner < 0 || row_in_banner >= banner_rows.length
66
+
67
+ # Get pixel values for this row of the character
68
+ row_pixels = img.pixels[row * img.width...(row + 1) * img.width]
69
+
70
+ # Calculate the position to start writing pixels
71
+ start_pos = banner_rows[row_in_banner].length - char_width
72
+
73
+ # Overwrite the empty space with actual character data
74
+ row_pixels.each_with_index do |pixel, i|
75
+ banner_rows[row_in_banner][start_pos + i] = pixel
76
+ end
77
+ end
78
+ end
79
+ end
80
+
81
+ # Print the banner
82
+ banner_rows.each do |row|
83
+ row.each do |pixel|
84
+ # Use ANSI color codes to display grayscale values
85
+ print "\33[32;48;2;#{pixel};#{pixel};#{pixel}m "
86
+ end
87
+ puts "\33[39;49m"
88
+ end
89
+ end
90
+
91
+ # Get text from command line or use default
92
+ text = ARGV[0] || "Hello!"
93
+ scale = ARGV[1] ? ARGV[1].to_i : 20
94
+
95
+ puts "Rendering: '#{text}' with scale #{scale}"
96
+ render_text_banner(text, "resources/Ubuntu-Regular.ttf", scale)
@@ -0,0 +1,139 @@
1
+ # Bypass bundler
2
+ ENV['BUNDLE_GEMFILE'] = nil
3
+
4
+ require 'pp'
5
+ require_relative '../lib/skrift'
6
+
7
+ # Function to render a string as a text banner
8
+ def render_text_banner(text, font_path = "resources/FiraGO-Regular.ttf", scale = 16, threshold = 30)
9
+ # Load the font
10
+ f = Font.load(font_path)
11
+
12
+ # Create a new SFT instance
13
+ sft = SFT.new(f)
14
+ sft.x_scale = scale
15
+ sft.y_scale = scale
16
+ sft.x_offset = 0
17
+ sft.y_offset = 0
18
+
19
+ # Get font metrics
20
+ metrics = sft.lmetrics
21
+
22
+ # Calculate the maximum height needed for any character
23
+ max_height = metrics.ascender - metrics.descender + metrics.line_gap
24
+
25
+ # Array to store each row of pixels for the banner
26
+ banner_rows = []
27
+
28
+ # Initialize the banner with empty rows
29
+ max_height.to_i.times do
30
+ banner_rows << []
31
+ end
32
+
33
+ # Process each character in the text
34
+ text.each_char do |char|
35
+ # Get the glyph ID for this character
36
+ gid = sft.lookup(char.ord)
37
+
38
+ # Skip if glyph not found
39
+ next if gid.nil?
40
+
41
+ # Get metrics for this glyph
42
+ gmetrics = sft.gmetrics(gid)
43
+
44
+ # Create an image for this glyph
45
+ img = Image.new(gmetrics.min_width, gmetrics.min_height)
46
+
47
+ # Render the glyph
48
+ if sft.render(gid, img)
49
+ # Calculate vertical offset based on metrics
50
+ y_offset = metrics.ascender - gmetrics.y_offset
51
+
52
+ # Calculate character width including spacing
53
+ char_width = img.width + 2 # Width plus spacing
54
+
55
+ # First, add empty space for all rows in the banner for this character
56
+ banner_rows.each do |row|
57
+ row.concat([0] * char_width)
58
+ end
59
+
60
+ # Then, overwrite with actual character data where it exists
61
+ img.height.times do |row|
62
+ row_in_banner = row + y_offset.to_i
63
+
64
+ # Skip rows outside the banner bounds
65
+ next if row_in_banner < 0 || row_in_banner >= banner_rows.length
66
+
67
+ # Get pixel values for this row of the character
68
+ row_pixels = img.pixels[row * img.width...(row + 1) * img.width]
69
+
70
+ # Calculate the position to start writing pixels
71
+ start_pos = banner_rows[row_in_banner].length - char_width
72
+
73
+ # Overwrite the empty space with actual character data
74
+ row_pixels.each_with_index do |pixel, i|
75
+ banner_rows[row_in_banner][start_pos + i] = pixel
76
+ end
77
+ end
78
+ end
79
+ end
80
+
81
+ # Print the banner
82
+ # Process banner for box drawing characters (look at 2x2 blocks)
83
+ (0...banner_rows.length-1).step(2) do |y|
84
+ row_str = ""
85
+ (0...banner_rows[y].length-1).step(2) do |x|
86
+ # Get the 2x2 grid of pixels (with bounds checking)
87
+ top_left = banner_rows[y][x]
88
+ top_right = x+1 < banner_rows[y].length ? banner_rows[y][x+1] : 0
89
+ bottom_left = y+1 < banner_rows.length ? banner_rows[y+1][x] : 0
90
+ bottom_right = (y+1 < banner_rows.length && x+1 < banner_rows[y+1].length) ? banner_rows[y+1][x+1] : 0
91
+
92
+ # Check if any pixel is above threshold
93
+ if [top_left, top_right, bottom_left, bottom_right].max >= threshold
94
+ # Determine which quadrants are filled (1=filled, 0=empty)
95
+ tl = top_left >= threshold ? 1 : 0
96
+ tr = top_right >= threshold ? 1 : 0
97
+ bl = bottom_left >= threshold ? 1 : 0
98
+ br = bottom_right >= threshold ? 1 : 0
99
+
100
+ # Calculate average color for foreground
101
+ fg_pixel = [top_left, top_right, bottom_left, bottom_right].max
102
+ fg_color = "\e[38;2;#{fg_pixel};#{fg_pixel};#{fg_pixel}m"
103
+
104
+ # Select appropriate Unicode box drawing character based on pattern
105
+ char = case [tl, tr, bl, br]
106
+ when [1, 1, 1, 1] then "\u2588" # Full block
107
+ when [1, 1, 1, 0] then "\u259B" # Quadrant upper left and upper right and lower right
108
+ when [1, 1, 0, 1] then "\u259C" # Quadrant upper left and lower left and lower right
109
+ when [1, 0, 1, 1] then "\u2599" # Quadrant upper left and upper right and lower left
110
+ when [0, 1, 1, 1] then "\u259F" # Quadrant upper right and lower left and lower right
111
+ when [1, 1, 0, 0] then "\u2580" # Upper half block
112
+ when [0, 0, 1, 1] then "\u2584" # Lower half block
113
+ when [1, 0, 1, 0] then "\u258C" # Left half block
114
+ when [0, 1, 0, 1] then "\u2590" # Right half block
115
+ when [1, 0, 0, 0] then "\u2598" # Quadrant upper left
116
+ when [0, 1, 0, 0] then "\u259D" # Quadrant upper right
117
+ when [0, 0, 1, 0] then "\u2596" # Quadrant lower left
118
+ when [0, 0, 0, 1] then "\u2597" # Quadrant lower right
119
+ when [1, 0, 0, 1] then "\u259A" # Quadrant upper left and lower right
120
+ when [0, 1, 1, 0] then "\u259E" # Quadrant upper right and lower left
121
+ else " "
122
+ end
123
+ row_str += "#{fg_color}#{char}"
124
+ else
125
+ # Skip rendering pixels below threshold
126
+ row_str += " "
127
+ end
128
+ end
129
+ puts row_str + "\e[0m" # Reset all attributes
130
+ end
131
+ end
132
+
133
+ # Get text from command line or use default
134
+ text = ARGV[0] || "Hello!"
135
+ scale = ARGV[1] ? ARGV[1].to_i : 16
136
+ threshold = ARGV[2] ? ARGV[2].to_i : 30
137
+
138
+ puts "Rendering: '#{text}' with scale #{scale} and threshold #{threshold}"
139
+ render_text_banner(text, "resources/FiraGO-Regular.ttf", scale, threshold)
@@ -0,0 +1,207 @@
1
+ require 'pp'
2
+ require_relative '../lib/skrift'
3
+
4
+ # Helper method to determine the appropriate shadow character
5
+ def determine_shadow_char(banner_rows, x, y, threshold)
6
+ # If current position is above threshold, no shadow needed
7
+ return nil if x < 0 || y < 0
8
+ return nil if y < banner_rows.length && x < banner_rows[y].length && banner_rows[y][x] >= threshold
9
+
10
+ # Check surrounding pixels
11
+ up = (y > 0 && y - 1 < banner_rows.length && x < banner_rows[y - 1].length) ? banner_rows[y - 1][x-1] >= threshold : false
12
+ down = (y + 1 < banner_rows.length && x < banner_rows[y + 1].length) ? banner_rows[y + 1][x-1] >= threshold : false
13
+ left = (x > 0 && y < banner_rows.length && x - 1 < banner_rows[y].length) ? banner_rows[y][x - 2] >= threshold : false
14
+ right = (y < banner_rows.length && x + 1 < banner_rows[y].length) ? banner_rows[y][x] >= threshold : false
15
+
16
+ # For shadow characters, we need to invert our understanding of "up", "down", etc.
17
+ # A shadow character connects TO a character, not FROM it.
18
+ # So we need to swap the directions:
19
+ # - "up" means the shadow is below a character (connects upward)
20
+ # - "down" means the shadow is above a character (connects downward)
21
+ # - "left" means the shadow is to the right of a character (connects leftward)
22
+ # - "right" means the shadow is to the left of a character (connects rightward)
23
+
24
+ # Determine box drawing character based on connections
25
+ if up && down && left && right
26
+ return "┼" # Box drawing: vertical and horizontal
27
+ elsif up && down && left
28
+ return "│" # Box drawing: vertical
29
+ #"Y" #┤" # Box drawing: vertical and left
30
+ elsif up && down && right
31
+ return "├" # Box drawing: vertical and right
32
+ elsif up && left && right
33
+ return "┴" # Box drawing: up and horizontal
34
+ elsif down && left && right
35
+ return "┬" # Box drawing: down and horizontal
36
+ elsif left && right
37
+ return "X" #
38
+ elsif up && down
39
+ return "│" # Box drawing: vertical
40
+ elsif up && right
41
+ return "└" # Box drawing: up and right
42
+ elsif up && left
43
+ return "┌" # Box drawing: down and right
44
+ #"B" #┘" # Box drawing: up and left
45
+ elsif down && right
46
+ return "┌" # Box drawing: down and right
47
+ elsif down && left
48
+ return "┐" # Box drawing: down and left
49
+ elsif up
50
+ return "─" # Box drawing: horizontal
51
+ elsif down
52
+ return "┐" # Box drawing: down and left #"Z"#╷" # Box drawing: down
53
+ elsif left
54
+ return "│" # Box drawing: vertical
55
+ #"A" #╴" # Box drawing: left
56
+ elsif right
57
+ return "╶" # Box drawing: right
58
+ end
59
+
60
+ # If we're below a character but not directly adjacent, use a light shade
61
+ if y > 0 && y - 1 < banner_rows.length && x < banner_rows[y - 1].length && banner_rows[y - 1][x] >= threshold
62
+ return "└" # Box drawing: up and right
63
+ end
64
+
65
+ return nil
66
+ end
67
+
68
+ # Function to render a string as a text banner
69
+ def render_text_banner(text, font_path = "resources/FiraGO-Regular.ttf", scale = 16, threshold = 30)
70
+ # Load the font
71
+ f = Font.load(font_path)
72
+
73
+ # Create a new SFT instance
74
+ sft = SFT.new(f)
75
+ sft.x_scale = scale
76
+ sft.y_scale = scale
77
+ sft.x_offset = 0
78
+ sft.y_offset = 0
79
+
80
+ # Get font metrics
81
+ metrics = sft.lmetrics
82
+
83
+ # Calculate the maximum height needed for any character
84
+ max_height = metrics.ascender - metrics.descender + metrics.line_gap
85
+
86
+ # Array to store each row of pixels for the banner
87
+ banner_rows = []
88
+
89
+ # Initialize the banner with empty rows
90
+ max_height.to_i.times do
91
+ banner_rows << []
92
+ end
93
+
94
+ # Process each character in the text
95
+ text.each_char do |char|
96
+ # Get the glyph ID for this character
97
+ gid = sft.lookup(char.ord)
98
+
99
+ # Skip if glyph not found
100
+ next if gid.nil?
101
+
102
+ # Get metrics for this glyph
103
+ gmetrics = sft.gmetrics(gid)
104
+
105
+ # Create an image for this glyph
106
+ img = Image.new(gmetrics.min_width, gmetrics.min_height)
107
+
108
+ # Render the glyph
109
+ if sft.render(gid, img)
110
+ # Calculate vertical offset based on metrics
111
+ y_offset = metrics.ascender - gmetrics.y_offset
112
+
113
+ # Calculate character width including spacing
114
+ char_width = img.width + 2 # Width plus spacing
115
+
116
+ # First, add empty space for all rows in the banner for this character
117
+ banner_rows.each do |row|
118
+ row.concat([0] * char_width)
119
+ end
120
+
121
+ # Then, overwrite with actual character data where it exists
122
+ img.height.times do |row|
123
+ row_in_banner = row + y_offset.to_i
124
+
125
+ # Skip rows outside the banner bounds
126
+ next if row_in_banner < 0 || row_in_banner >= banner_rows.length
127
+
128
+ # Get pixel values for this row of the character
129
+ row_pixels = img.pixels[row * img.width...(row + 1) * img.width]
130
+
131
+ # Calculate the position to start writing pixels
132
+ start_pos = banner_rows[row_in_banner].length - char_width
133
+
134
+ # Overwrite the empty space with actual character data
135
+ row_pixels.each_with_index do |pixel, i|
136
+ banner_rows[row_in_banner][start_pos + i] = pixel
137
+ end
138
+ end
139
+ end
140
+ end
141
+
142
+ # Process banner for box drawing characters (look at 2x2 blocks)
143
+ (0...banner_rows.length).step(2) do |y|
144
+ row_str = ""
145
+ (0...banner_rows[y].length - 1).step(2) do |x|
146
+ # Get the 2x2 grid of pixels (with bounds checking)
147
+ top_left = banner_rows[y][x]
148
+ top_right = x + 1 < banner_rows[y].length ? banner_rows[y][x + 1] : 0
149
+ bottom_left = y + 1 < banner_rows.length ? banner_rows[y + 1][x] : 0
150
+ bottom_right = (y + 1 < banner_rows.length && x + 1 < banner_rows[y + 1].length) ? banner_rows[y + 1][x + 1] : 0
151
+
152
+ # Check if any pixel is above threshold
153
+ if [top_left, top_right, bottom_left, bottom_right].max >= threshold
154
+ # Determine which quadrants are filled (1=filled, 0=empty)
155
+ tl = top_left >= threshold ? 1 : 0
156
+ tr = top_right >= threshold ? 1 : 0
157
+ bl = bottom_left >= threshold ? 1 : 0
158
+ br = bottom_right >= threshold ? 1 : 0
159
+
160
+ # Calculate average color for foreground
161
+ fg_pixel = [top_left, top_right, bottom_left, bottom_right].max
162
+ fg_color = "\e[38;2;#{fg_pixel};#{fg_pixel/4};#{fg_pixel/4}m"
163
+
164
+ # Select appropriate Unicode box drawing character based on pattern
165
+ char = case [tl, tr, bl, br]
166
+ when [1, 1, 1, 1] then "\u2588" # Full block
167
+ when [1, 1, 1, 0] then "\u259B" # Quadrant upper left and upper right and lower right
168
+ when [1, 1, 0, 1] then "\u259C" # Quadrant upper left and lower left and lower right
169
+ when [1, 0, 1, 1] then "\u2599" # Quadrant upper left and upper right and lower left
170
+ when [0, 1, 1, 1] then "\u259F" # Quadrant upper right and lower left and lower right
171
+ when [1, 1, 0, 0] then "\u2580" # Upper half block
172
+ when [0, 0, 1, 1] then "\u2584" # Lower half block
173
+ when [1, 0, 1, 0] then "\u258C" # Left half block
174
+ when [0, 1, 0, 1] then "\u2590" # Right half block
175
+ when [1, 0, 0, 0] then "\u2598" # Quadrant upper left
176
+ when [0, 1, 0, 0] then "\u259D" # Quadrant upper right
177
+ when [0, 0, 1, 0] then "\u2596" # Quadrant lower left
178
+ when [0, 0, 0, 1] then "\u2597" # Quadrant lower right
179
+ when [1, 0, 0, 1] then "\u259A" # Quadrant upper left and lower right
180
+ when [0, 1, 1, 0] then "\u259E" # Quadrant upper right and lower left
181
+ else " "
182
+ end
183
+ row_str += "#{fg_color}#{char}"
184
+ else
185
+ # Check for shadow character
186
+ shadow_char = determine_shadow_char(banner_rows, x, y, threshold)
187
+ if shadow_char.nil?
188
+ row_str += " "
189
+ else
190
+ # Use a grey color for shadow
191
+ shadow_color = "\e[38;2;64;64;64m"
192
+ row_str += "#{shadow_color}#{shadow_char}"
193
+ end
194
+ end
195
+ end
196
+ puts row_str + "\e[0m" # Reset all attributes
197
+ end
198
+
199
+ end
200
+
201
+ # Get text from command line or use default
202
+ text = ARGV[0] || "Hello!"
203
+ scale = ARGV[1] ? ARGV[1].to_i : 16
204
+ threshold = ARGV[2] ? ARGV[2].to_i : 30
205
+
206
+ puts "Rendering: '#{text}' with scale #{scale} and threshold #{threshold}"
207
+ render_text_banner(text, "resources/FiraGO-Regular.ttf", scale, threshold)
@@ -1,6 +1,6 @@
1
1
 
2
2
  require 'pp'
3
- require_relative './lib/skrift'
3
+ require_relative '../lib/skrift'
4
4
 
5
5
  f = Font.load("resources/Ubuntu-Regular.ttf")
6
6
  #f = Font.load("resources/FiraGO-Regular_extended_with_NotoSansEgyptianHieroglyphs-Regular.ttf")
@@ -59,7 +59,7 @@ if sft.render(gid, img)
59
59
  #img.pixels = r.post_process
60
60
 
61
61
  img.height.times do |row|
62
- img.pixels[row*img.width .. (row+1)*img.width-1].map do |s|
62
+ img.pixels[row * img.width .. (row + 1) * img.width - 1].map do |s|
63
63
  print "\033[32;48;2;#{s};#{s};#{s}m%02x " % s
64
64
  end
65
65
  puts "\033[39;49m"