Skip to content

Commit 9679aca

Browse files
committed
refactor: optimization + refactor
1 parent b65f67d commit 9679aca

12 files changed

Lines changed: 1505 additions & 345 deletions

File tree

shard.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: crimage
2-
version: 1.0.4
2+
version: 1.0.5
33

44
authors:
55
- Ali Naqvi <syed.alinaqvi@gmail.com>

spec/jpeg/writer_spec.cr

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,15 @@ describe CrImage::JPEG::Writer do
7676
end
7777
end
7878

79+
it "rejects empty image dimensions" do
80+
img = CrImage::Gray.new(CrImage.rect(0, 0, 0, 0))
81+
io = IO::Memory.new
82+
83+
expect_raises(CrImage::JPEG::FormatError, /dimensions must be positive/) do
84+
CrImage::JPEG.write(io, img, 75)
85+
end
86+
end
87+
7988
it "writes to file" do
8089
rect = CrImage.rect(0, 0, 8, 8)
8190
img = CrImage::Gray.new(rect)

spec/png/writer_spec.cr

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,13 +63,29 @@ module CrImage::PNG
6363
File.delete(path)
6464
end
6565

66-
it "handles empty image dimensions" do
66+
it "rejects empty image dimensions" do
6767
img = CrImage::RGBA.new(CrImage.rect(0, 0, 0, 0))
68+
io = IO::Memory.new
69+
expect_raises(FormatError, /Invalid Image size/) do
70+
PNG.write(io, img)
71+
end
72+
end
73+
74+
it "preserves palette transparency in read_config" do
75+
palette = Color::Palette.new([
76+
Color::NRGBA.new(0, 0, 0, 0).as(Color::Color),
77+
Color::NRGBA.new(255, 255, 255, 255).as(Color::Color),
78+
])
79+
img = CrImage::Paletted.new(CrImage.rect(0, 0, 2, 1), palette)
6880

6981
io = IO::Memory.new
70-
# Empty images are written successfully (0x0 PNG)
7182
PNG.write(io, img)
72-
io.size.should be > 0
83+
io.rewind
84+
85+
config = PNG.read_config(io)
86+
config_palette = config.color_model.as(Color::Palette)
87+
_, _, _, alpha = config_palette[0].rgba
88+
alpha.should eq(0_u32)
7389
end
7490

7591
it "writes NRGBA image" do

src/crimage/color/ycbcr.cr

Lines changed: 39 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,27 @@
11
require "./color"
22

33
module CrImage::Color
4+
YCBCR_YY1_TABLE = StaticArray(Int32, 256).new { |i| i.to_i32 * YCBCR_YY1_MULTIPLIER }
5+
YCBCR_CB_B_TABLE = StaticArray(Int32, 256).new { |i| RGB_FROM_CB_COEFF * (i.to_i32 - 128) }
6+
YCBCR_CR_R_TABLE = StaticArray(Int32, 256).new { |i| RGB_FROM_CR_COEFF * (i.to_i32 - 128) }
7+
YCBCR_CB_G_TABLE = StaticArray(Int32, 256).new { |i| RGB_FROM_CB_G_COEFF * (i.to_i32 - 128) }
8+
YCBCR_CR_G_TABLE = StaticArray(Int32, 256).new { |i| RGB_FROM_CR_G_COEFF * (i.to_i32 - 128) }
9+
RGB_TO_Y_R_TABLE = StaticArray(Int32, 256).new { |i| YCBCR_Y_R_COEFF * i.to_i32 }
10+
RGB_TO_Y_G_TABLE = StaticArray(Int32, 256).new { |i| YCBCR_Y_G_COEFF * i.to_i32 }
11+
RGB_TO_Y_B_TABLE = StaticArray(Int32, 256).new { |i| YCBCR_Y_B_COEFF * i.to_i32 }
12+
RGB_TO_CB_R_TABLE = StaticArray(Int32, 256).new { |i| YCBCR_CB_R_COEFF * i.to_i32 }
13+
RGB_TO_CB_G_TABLE = StaticArray(Int32, 256).new { |i| YCBCR_CB_G_COEFF * i.to_i32 }
14+
RGB_TO_CB_B_TABLE = StaticArray(Int32, 256).new { |i| YCBCR_CB_B_COEFF * i.to_i32 }
15+
RGB_TO_CR_R_TABLE = StaticArray(Int32, 256).new { |i| YCBCR_CR_R_COEFF * i.to_i32 }
16+
RGB_TO_CR_G_TABLE = StaticArray(Int32, 256).new { |i| YCBCR_CR_G_COEFF * i.to_i32 }
17+
RGB_TO_CR_B_TABLE = StaticArray(Int32, 256).new { |i| YCBCR_CR_B_COEFF * i.to_i32 }
18+
YCBCR_RANGE_LIMIT = StaticArray(UInt8, 768).new do |i|
19+
value = i.to_i32 - 256
20+
value = 0 if value < 0
21+
value = 255 if value > 255
22+
value.to_u8
23+
end
24+
425
# YCbCr represents a fully opaque 24-bit Y'CbCr color, having 8 bits each for
526
# one luma and two chroma components.
627
#
@@ -137,24 +158,20 @@ module CrImage::Color
137158
# cr = 0.5000*r - 0.4187*g - 0.0813*b + 128
138159
# https:#www.w3.org/Graphics/JPEG/jfif3.pdf says y but means y'.
139160

140-
r1 = r.to_i32
141-
g1 = g.to_i32
142-
b1 = b.to_i32
143-
144161
# yy is in range 0..0xff
145162
# Note that YCBCR_Y_R_COEFF + YCBCR_Y_G_COEFF + YCBCR_Y_B_COEFF equals 65536.
146-
yy = (YCBCR_Y_R_COEFF*r1 + YCBCR_Y_G_COEFF*g1 + YCBCR_Y_B_COEFF*b1 + (1 << 15)) >> 16
163+
yy = (RGB_TO_Y_R_TABLE[r] + RGB_TO_Y_G_TABLE[g] + RGB_TO_Y_B_TABLE[b] + (1 << 15)) >> 16
147164

148165
# Note that YCBCR_CB_R_COEFF + YCBCR_CB_G_COEFF + YCBCR_CB_B_COEFF equals 0.
149-
cb = YCBCR_CB_R_COEFF*r1 + YCBCR_CB_G_COEFF*g1 + YCBCR_CB_B_COEFF*b1 + YCBCR_CHROMA_OFFSET
166+
cb = RGB_TO_CB_R_TABLE[r] + RGB_TO_CB_G_TABLE[g] + RGB_TO_CB_B_TABLE[b] + YCBCR_CHROMA_OFFSET
150167
if cb.to_u32! & 0xff000000 == 0
151168
cb >>= 16
152169
else
153170
cb = ~0 ^ (cb >> 31)
154171
end
155172

156173
# Note that YCBCR_CR_R_COEFF + YCBCR_CR_G_COEFF + YCBCR_CR_B_COEFF equals 0.
157-
cr = YCBCR_CR_R_COEFF*r1 + YCBCR_CR_G_COEFF*g1 + YCBCR_CR_B_COEFF*b1 + YCBCR_CHROMA_OFFSET
174+
cr = RGB_TO_CR_R_TABLE[r] + RGB_TO_CR_G_TABLE[g] + RGB_TO_CR_B_TABLE[b] + YCBCR_CHROMA_OFFSET
158175
if cr.to_u32! & 0xff000000 == 0
159176
cr >>= 16
160177
else
@@ -255,31 +272,26 @@ module CrImage::Color
255272
# This is used internally for fast YCbCr to RGB conversion
256273
@[AlwaysInline]
257274
def self.ycbcr_to_rgb_16bit(y : UInt8, cb : UInt8, cr : UInt8) : {UInt32, UInt32, UInt32}
258-
yy1 = y.to_i32 * YCBCR_YY1_MULTIPLIER
259-
cb1 = cb.to_i32 - 128
260-
cr1 = cr.to_i32 - 128
275+
yy1 = YCBCR_YY1_TABLE[y]
261276

262-
r = yy1 + RGB_FROM_CR_COEFF*cr1
263-
if r.to_u32! & 0xff000000 == 0
264-
r >>= 8
265-
else
266-
r = (~0 ^ (r >> 31)) & 0xffff
267-
end
277+
r = clamp_ycbcr_16bit(yy1 + YCBCR_CR_R_TABLE[cr])
278+
g = clamp_ycbcr_16bit(yy1 - YCBCR_CB_G_TABLE[cb] - YCBCR_CR_G_TABLE[cr])
279+
b = clamp_ycbcr_16bit(yy1 + YCBCR_CB_B_TABLE[cb])
268280

269-
g = yy1 - RGB_FROM_CB_G_COEFF*cb1 - RGB_FROM_CR_G_COEFF*cr1
270-
if g.to_u32! & 0xff000000 == 0
271-
g >>= 8
272-
else
273-
g = (~0 ^ (g >> 31)) & 0xffff
274-
end
281+
{r, g, b}
282+
end
275283

276-
b = yy1 + RGB_FROM_CB_COEFF*cb1
277-
if b.to_u32! & 0xff000000 == 0
278-
b >>= 8
284+
@[AlwaysInline]
285+
def self.clamp_ycbcr_16bit(value : Int32) : UInt32
286+
if value.to_u32! & 0xff000000 == 0
287+
(value >> 8).to_u32
279288
else
280-
b = (~0 ^ (b >> 31)) & 0xffff
289+
((~0 ^ (value >> 31)) & 0xffff).to_u32
281290
end
291+
end
282292

283-
{r.to_u32, g.to_u32, b.to_u32}
293+
@[AlwaysInline]
294+
def self.clamp_ycbcr_8bit(value : Int32) : UInt8
295+
YCBCR_RANGE_LIMIT[(value >> 16) + 256]
284296
end
285297
end

src/crimage/jpeg/dct.cr

Lines changed: 52 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,40 +2,73 @@
22
# The DCT converts spatial image data into frequency domain for compression.
33
module CrImage::JPEG
44
# Forward Discrete Cosine Transform (DCT) implementation
5-
# Uses direct formula for correctness
6-
# Operates on 8x8 blocks
5+
# Uses a separable 8x8 transform with precomputed coefficients.
76
module DCT
7+
SCALE = [
8+
1.0 / ::Math.sqrt(8.0),
9+
::Math.sqrt(2.0 / 8.0),
10+
::Math.sqrt(2.0 / 8.0),
11+
::Math.sqrt(2.0 / 8.0),
12+
::Math.sqrt(2.0 / 8.0),
13+
::Math.sqrt(2.0 / 8.0),
14+
::Math.sqrt(2.0 / 8.0),
15+
::Math.sqrt(2.0 / 8.0),
16+
]
17+
18+
BASIS = [
19+
[0.3535533905932738, 0.3535533905932738, 0.3535533905932738, 0.3535533905932738, 0.3535533905932738, 0.3535533905932738, 0.3535533905932738, 0.3535533905932738],
20+
[0.4903926402016152, 0.4157348061512726, 0.27778511650980114, 0.09754516100806417, -0.0975451610080641, -0.277785116509801, -0.4157348061512727, -0.4903926402016152],
21+
[0.46193976625564337, 0.19134171618254492, -0.19134171618254486, -0.46193976625564337, -0.4619397662556434, -0.19134171618254517, 0.191341716182545, 0.46193976625564326],
22+
[0.4157348061512726, -0.0975451610080641, -0.4903926402016152, -0.2777851165098011, 0.2777851165098009, 0.4903926402016153, 0.0975451610080644, -0.41573480615127256],
23+
[0.35355339059327384, -0.35355339059327373, -0.35355339059327384, 0.3535533905932737, 0.35355339059327384, -0.35355339059327334, -0.3535533905932733, 0.35355339059327323],
24+
[0.27778511650980114, -0.4903926402016152, 0.09754516100806415, 0.41573480615127273, -0.41573480615127256, -0.09754516100806429, 0.49039264020161516, -0.27778511650980076],
25+
[0.19134171618254495, -0.4619397662556434, 0.46193976625564326, -0.19134171618254528, -0.19134171618254495, 0.46193976625564315, -0.4619397662556437, 0.19134171618254314],
26+
[0.09754516100806417, -0.2777851165098011, 0.41573480615127273, -0.4903926402016153, 0.4903926402016152, -0.415734806151272, 0.27778511650980076, -0.09754516100806251],
27+
]
28+
829
# Perform 2D DCT on an 8x8 block
930
# Input: 64-element array of pixel values (0-255 range)
1031
# Output: 64-element array of DCT coefficients
1132
def self.transform(block : Array(Int32)) : Array(Int32)
33+
result = Array(Int32).new(64, 0)
34+
transform_into(block, result)
35+
result
36+
end
37+
38+
def self.transform_into(block : Indexable(Int32), result : Array(Int32)) : Nil
1239
raise ArgumentError.new("Block must have 64 elements") unless block.size == 64
40+
raise ArgumentError.new("Result must have 64 elements") unless result.size == 64
1341

14-
# Create a working copy and shift values from [0, 255] to [-128, 127]
15-
temp = block.map { |val| (val - 128).to_f64 }
42+
shifted = StaticArray(Float64, 64).new(0.0)
43+
temp = StaticArray(Float64, 64).new(0.0)
1644

17-
# Apply 2D DCT
18-
result = Array(Float64).new(64, 0.0)
45+
64.times do |i|
46+
shifted[i] = (block[i] - 128).to_f64
47+
end
1948

20-
8.times do |ver|
21-
8.times do |hor|
49+
8.times do |y|
50+
row_base = y * 8
51+
8.times do |u|
2252
sum = 0.0
23-
8.times do |ypos|
24-
8.times do |xpos|
25-
sum += temp[ypos * 8 + xpos] *
26-
::Math.cos((2 * xpos + 1) * hor * ::Math::PI / 16.0) *
27-
::Math.cos((2 * ypos + 1) * ver * ::Math::PI / 16.0)
28-
end
53+
basis = BASIS[u]
54+
8.times do |x|
55+
sum += shifted[row_base + x] * basis[x]
2956
end
57+
temp[row_base + u] = sum
58+
end
59+
end
3060

31-
# Apply normalization: (2/N) * C(u) * C(v) where N=8
32-
cu = hor == 0 ? 1.0 / ::Math.sqrt(2.0) : 1.0
33-
cv = ver == 0 ? 1.0 / ::Math.sqrt(2.0) : 1.0
34-
result[ver * 8 + hor] = sum * 0.25 * cu * cv # 2/8 = 0.25
61+
8.times do |v|
62+
8.times do |u|
63+
sum = 0.0
64+
8.times do |y|
65+
sum += BASIS[v][y] * temp[y * 8 + u]
66+
end
67+
result[v * 8 + u] = sum.round.to_i32
3568
end
3669
end
3770

38-
result.map(&.round.to_i32)
71+
nil
3972
end
4073
end
4174
end

0 commit comments

Comments
 (0)