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 +4 -4
- data/example.rb +96 -0
- data/example2.rb +139 -0
- data/example3.rb +207 -0
- data/lib/skrift/font.rb +6 -8
- data/lib/skrift/outline.rb +8 -5
- data/lib/skrift/raster.rb +10 -3
- data/lib/skrift/sft.rb +7 -6
- data/lib/skrift/version.rb +1 -1
- data/lib/skrift.rb +0 -1
- data/test.rb +1 -1
- metadata +6 -4
- data/skrift.gemspec +0 -39
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: af1c22718a6dca7c32b99fbcafc078c3805fed9fc79be1ad9dc85ebd825de0da
|
|
4
|
+
data.tar.gz: e870dc3eb39b4e783497a67c7eb2869d44f989c463d73da628d7d324c0ba0959
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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)
|
|
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
|
|
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 =
|
|
286
|
+
local = [[1.0, 0.0, geti16(offset)], [0.0,1.0, geti16(offset+2)]]
|
|
289
287
|
offset += 4
|
|
290
288
|
else
|
|
291
|
-
local =
|
|
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 =
|
|
324
|
+
offset = tables["kern"]
|
|
327
325
|
return nil if offset.nil? || getu16(offset) != 0
|
|
328
326
|
offset += 4
|
|
329
327
|
@kerning = {}
|
data/lib/skrift/outline.rb
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
def midpoint(a, b)
|
|
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
|
|
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
|
|
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
|
|
47
|
-
tesselate_curve(Segment
|
|
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
|
|
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
|
|
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] :
|
|
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
|
|
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 =
|
|
73
|
+
transform = [xr, [0.0, -ys, bbox[3] - @y_offset]]
|
|
73
74
|
else
|
|
74
|
-
transform =
|
|
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
|
data/lib/skrift/version.rb
CHANGED
data/lib/skrift.rb
CHANGED
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
|
|
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:
|
|
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.
|
|
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
|