skrift 0.1.0 → 0.2.1

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: af1c22718a6dca7c32b99fbcafc078c3805fed9fc79be1ad9dc85ebd825de0da
4
+ data.tar.gz: e870dc3eb39b4e783497a67c7eb2869d44f989c463d73da628d7d324c0ba0959
5
5
  SHA512:
6
- metadata.gz: 87c484791a46b5e82819d12e6c2633bdd12704f92f78e282f1b97cd89931e52827ec4e97e2b2bed52518cda038adad0a77a49105976f6154fa9e85801e04b529
7
- data.tar.gz: a21dd2bd6a20d90639da4264b186d8bae76675e2d9ef4d44a1438d40a4df912396dba0bb3c03aadd58c8518f408e99e3d27bb3a5dae4ff1afc8ad38f238ec308
6
+ metadata.gz: 34d0ae2eef70651d92a6775a6117d71fedd67708fe4ee3c400ce7a3a163342f197f66b82921de821b9372f818be4c292499f6db664f70d4d3f86b9a0c228fdbf
7
+ data.tar.gz: e439351bbc5900eba27a0e0b34ae3e44881224fb05f7126e84775a3332997c69cc74199dc6560d204f475b81c928a098f227a699d402e3a8d12fc2a8a38e611e
data/example.rb ADDED
@@ -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)
data/example2.rb ADDED
@@ -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)
data/example3.rb ADDED
@@ -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)
data/lib/skrift/font.rb CHANGED
@@ -36,8 +36,7 @@ class Font
36
36
  ]
37
37
  end
38
38
 
39
- def reqtable(tag); tables[tag] or raise "Unable to get table '#{tag}'"; end
40
- def gettable(tag); tables[tag]; end
39
+ def reqtable(tag) = (tables[tag] or raise "Unable to get table '#{tag}'")
41
40
 
42
41
  def glyph_bbox(outline)
43
42
  box = at(outline+2, 8).unpack("s>*")
@@ -119,7 +118,7 @@ class Font
119
118
  id_range_offsets = id_deltas + seg_count_x2
120
119
 
121
120
  @ecodes ||= at(end_codes,seg_count_x2 -1).unpack("n*")
122
- seg_id_x_x2 = @ecodes.bsearch_index {|i| i > char_code } * 2
121
+ seg_id_x_x2 = @ecodes.bsearch_index {|i| i > char_code }.to_i * 2
123
122
 
124
123
  # Look up segment info from the arrays & short circuit if the spec requires
125
124
  start_code = getu16(start_codes + seg_id_x_x2)
@@ -202,7 +201,7 @@ class Font
202
201
  accum
203
202
  end
204
203
 
205
- num_pts.times {|i| points << Vector[accumulate.call(i,1), 0.0] }
204
+ num_pts.times {|i| points << Raster::Vector.new(accumulate.call(i,1), 0.0) }
206
205
  accum = 0.0
207
206
  num_pts.times {|i| points[base_point+i][1] = accumulate.call(i,2) }
208
207
  end
@@ -279,16 +278,15 @@ class Font
279
278
  flags = THERE_ARE_MORE_COMPONENTS
280
279
  while flags.allbits?(THERE_ARE_MORE_COMPONENTS)
281
280
  flags, glyph = at(offset,4).unpack("S>*")
282
- p [flags,glyph]
283
281
  offset += 4
284
282
  # We don't implement point matching, and neither does stb truetype
285
283
  return nil if (flags & ACTUAL_XY_OFFSETS) == 0
286
284
  # Read additional X and Y offsets (in FUnits) of this component.
287
285
  if (flags & OFFSETS_ARE_LARGE) != 0
288
- local = Matrix[[1.0, 0.0, geti16(offset)], [0.0,1.0, geti16(offset+2)]]
286
+ local = [[1.0, 0.0, geti16(offset)], [0.0,1.0, geti16(offset+2)]]
289
287
  offset += 4
290
288
  else
291
- local = Matrix[[1.0, 0.0, geti8(offset)], [0.0, 1.0, geti8(offset)+1]]
289
+ local = [[1.0, 0.0, geti8(offset)], [0.0, 1.0, geti8(offset)+1]]
292
290
  offset += 2
293
291
  end
294
292
 
@@ -323,7 +321,7 @@ class Font
323
321
 
324
322
  def kerning
325
323
  return @kerning if @kerning
326
- offset = gettable("kern")
324
+ offset = tables["kern"]
327
325
  return nil if offset.nil? || getu16(offset) != 0
328
326
  offset += 4
329
327
  @kerning = {}
@@ -1,8 +1,11 @@
1
- def midpoint(a, b); 0.5*(a+b); end
1
+ def midpoint(a, b) = (a+b)*0.5
2
2
 
3
3
  # Applies an affine linear transformation matrix to a set of points
4
4
  def transform_points(trf, pts)
5
- pts.each {|pt| pt[0],pt[1] = *(trf * Vector[*pt,1]) }
5
+ pts.each do |pt|
6
+ pt[0] = trf[0][0] * pt[0] + trf[0][1] * pt[1] + trf[0][2]
7
+ pt[1] = trf[1][0] * pt[0] + trf[1][1] * pt[1] + trf[1][2]
8
+ end
6
9
  end
7
10
 
8
11
  class Outline
@@ -34,7 +37,7 @@ class Outline
34
37
 
35
38
  def tesselate_curve(curve)
36
39
  if is_flat(curve)
37
- @segments << Segment.new(curve.beg, curve.end)
40
+ @segments << Segment[curve.beg, curve.end]
38
41
  return
39
42
  end
40
43
  ctrl0 = @points.length
@@ -43,8 +46,8 @@ class Outline
43
46
  @points << midpoint(@points[curve.ctrl], @points[curve.end])
44
47
  pivot = @points.length
45
48
  @points << midpoint(@points[ctrl0], @points[ctrl1])
46
- tesselate_curve(Segment.new(curve.beg, pivot, ctrl0))
47
- tesselate_curve(Segment.new(pivot, curve.end, ctrl1))
49
+ tesselate_curve(Segment[curve.beg, pivot, ctrl0])
50
+ tesselate_curve(Segment[pivot, curve.end, ctrl1])
48
51
  end
49
52
 
50
53
  # A heuristic to tell whether a given curve can be approximated closely enough by a line. */
data/lib/skrift/raster.rb CHANGED
@@ -1,10 +1,17 @@
1
1
  class Raster
2
- Cell = Struct.new(:area, :cover)
2
+ Cell = Struct.new(:area, :cover)
3
3
 
4
+ Vector = Struct.new(:x,:y) do
5
+ def*(f) = Vector[x*f,y*f]
6
+ def+(o) = Vector[x+o[0],y+o[1]]
7
+ def-(o) = Vector[x-o[0],y-o[1]]
8
+ def min = (x < y ? x : y)
9
+ end
10
+
4
11
  def initialize width, height
5
12
  @width = width
6
13
  @height = height
7
- @cells = (0..(width*height-1)).map { Cell.new(0.0,0.0) }
14
+ @cells = (0..(width*height-1)).map { Cell[0.0,0.0] }
8
15
  end
9
16
 
10
17
  # Integrate the values in the buffer to arrive at the final grayscale image.
@@ -72,7 +79,7 @@ class Raster
72
79
  prev_distance = next_distance
73
80
  along_x = next_crossing[0] < next_crossing[1]
74
81
  pixel += along_x ? Vector[dir_x,0] : Vector[0,dir_y]
75
- next_crossing += along_x ? Vector[crossing_incr_x, 0.0] : Vector[0.0, crossing_incr_y]
82
+ next_crossing += along_x ? Vector[crossing_incr_x, 0.0] : [0.0, crossing_incr_y]
76
83
  next_distance = next_crossing.min
77
84
  end
78
85
  setcell.call(1.0)
data/lib/skrift/sft.rb CHANGED
@@ -30,6 +30,7 @@ class SFT
30
30
  end
31
31
 
32
32
  def gmetrics(glyph) # 149
33
+ return nil if glyph.nil?
33
34
  raise "out of bounds" if glyph < 0
34
35
  xs = @x_scale.to_f / @font.units_per_em
35
36
  adv, lsb = @font.hor_metrics(glyph)
@@ -42,13 +43,13 @@ class SFT
42
43
  bbox = glyph_bbox(outline)
43
44
  return nil if !bbox
44
45
  metrics.min_width = bbox[2] - bbox[0] + 1
45
- metrics.min_height= bbox[3] - bbox[1] + 1
46
- metrics.y_offset = @flags & SFT::DOWNWARD_Y != 0 ? bbox[3] : bbox[1]
46
+ metrics.min_height = bbox[3] - bbox[1] + 1
47
+ metrics.y_offset = @flags & SFT::DOWNWARD_Y != 0 ? bbox[3] : bbox[1]
47
48
  return metrics
48
49
  end
49
50
 
50
51
  def lmetrics
51
- hhea= font.reqtable("hhea")
52
+ hhea = font.reqtable("hhea")
52
53
  factor = @y_scale.to_f / @font.units_per_em
53
54
  LMetrics.new(
54
55
  font.geti16(hhea + 4) * factor, # ascender
@@ -69,12 +70,12 @@ class SFT
69
70
  xr = [@x_scale.to_f / @font.units_per_em, 0.0, @x_offset - bbox[0]]
70
71
  ys = @y_scale.to_f / @font.units_per_em
71
72
  if @flags.allbits?(SFT::DOWNWARD_Y)
72
- transform = Matrix.rows([xr, [0.0, -ys, bbox[3] - @y_offset]])
73
+ transform = [xr, [0.0, -ys, bbox[3] - @y_offset]]
73
74
  else
74
- transform = Matrix.rows([xr, [0.0, +ys, @y_offset - bbox[1] ]])
75
+ transform = [xr, [0.0, +ys, @y_offset - bbox[1] ]]
75
76
  end
76
77
  outl = @font.decode_outline(outline)
77
- outl.render(transform, image)
78
+ outl.render(transform, image) if outl
78
79
  end
79
80
 
80
81
  def kerning(left_glyph, right_glyph) # 176
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Skrift
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.1"
5
5
  end
data/lib/skrift.rb CHANGED
@@ -8,4 +8,3 @@ require_relative './skrift/sft'
8
8
  require_relative './skrift/outline'
9
9
  require_relative './skrift/raster'
10
10
  require_relative './skrift/font'
11
- require 'matrix'
data/test.rb CHANGED
@@ -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"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: skrift
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vidar Hokstad
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-08-18 00:00:00.000000000 Z
11
+ date: 2026-06-15 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email:
@@ -22,6 +22,9 @@ files:
22
22
  - LICENSE.txt
23
23
  - README.md
24
24
  - Rakefile
25
+ - example.rb
26
+ - example2.rb
27
+ - example3.rb
25
28
  - lib/skrift.rb
26
29
  - lib/skrift/font.rb
27
30
  - lib/skrift/outline.rb
@@ -29,7 +32,6 @@ files:
29
32
  - lib/skrift/sft.rb
30
33
  - lib/skrift/version.rb
31
34
  - resources/Ubuntu-Regular.ttf
32
- - skrift.gemspec
33
35
  - test.rb
34
36
  homepage: https://github.com/vidarh/skrift
35
37
  licenses:
@@ -52,7 +54,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
52
54
  - !ruby/object:Gem::Version
53
55
  version: '0'
54
56
  requirements: []
55
- rubygems_version: 3.1.4
57
+ rubygems_version: 3.4.10
56
58
  signing_key:
57
59
  specification_version: 4
58
60
  summary: A pure Ruby TruteType font renderer
data/skrift.gemspec DELETED
@@ -1,39 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "lib/skrift/version"
4
-
5
- Gem::Specification.new do |spec|
6
- spec.name = "skrift"
7
- spec.version = Skrift::VERSION
8
- spec.authors = ["Vidar Hokstad"]
9
- spec.email = ["vidar@hokstad.com"]
10
-
11
- spec.summary = "A pure Ruby TruteType font renderer"
12
- #spec.description = "TODO: Write a longer description or delete this line."
13
- spec.homepage = "https://github.com/vidarh/skrift"
14
- spec.license = "ISC"
15
- spec.required_ruby_version = ">= 2.7.0"
16
-
17
- #spec.metadata["allowed_push_host"] = "TODO: Set to your gem server 'https://example.com'"
18
-
19
- spec.metadata["homepage_uri"] = spec.homepage
20
- spec.metadata["source_code_uri"] = spec.homepage
21
- #spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
22
-
23
- # Specify which files should be added to the gem when it is released.
24
- # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
25
- spec.files = Dir.chdir(__dir__) do
26
- `git ls-files -z`.split("\x0").reject do |f|
27
- (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|circleci)|appveyor)})
28
- end
29
- end
30
- spec.bindir = "exe"
31
- spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
32
- spec.require_paths = ["lib"]
33
-
34
- # Uncomment to register a new dependency of your gem
35
- # spec.add_dependency "example-gem", "~> 1.0"
36
-
37
- # For more information and examples about making a new gem, check out our
38
- # guide at: https://bundler.io/guides/creating_gem.html
39
- end