Skip to content

Commit

Permalink
Implement image rotation, default value for color channels, fix bug w…
Browse files Browse the repository at this point in the history
…ith alpha
  • Loading branch information
Vici37 committed Jul 15, 2023
1 parent 94a7505 commit 6b46e0b
Show file tree
Hide file tree
Showing 8 changed files with 113 additions and 14 deletions.
46 changes: 46 additions & 0 deletions spec/cr-image/operation/rotate_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
require "../../spec_helper"

Spectator.describe CrImage::Operation::Rotate, :focus do
include SpecHelper

specs_for_operator(rotate(45),
gray_hash: "b23e83666e8046bd28396b3c5d7e4c7874671cfd",
rgba_hash: "e21797526425cd7e9473ca479f45bbdb8e049d1e"
)

specs_for_operator(rotate(90),
gray_hash: "124795c3ed7e003750d024f610626c3b22b26206",
rgba_hash: "fd20c71bb52ff5a148dfcea9f4fe49ab92c80804"
)

specs_for_operator(rotate(-30),
gray_hash: "a14c40f3c480330e4ec8aa9d8441d3e08d6e3864",
rgba_hash: "edafe8f2472918dcdad5e72187665fcc32e14851"
)

specs_for_operator(rotate(45, center_x: 100, center_y: 200),
gray_hash: "89f91776edc2c8814d235c29d0a50eaa3f015753",
rgba_hash: "fb4a52471956cd6a48aee2f4470477a4b295587c"
)

specs_for_operator(rotate(45, center_x: 375, center_y: 250, radius: 20),
gray_hash: "171f3aa256a46a3aeec94cddec477ed01fdb658e",
rgba_hash: "ba9d444ed5f4567830d6037990231aae0f3431ca"
)

# specs_for_operator(crop(0, 0, 750, 500),
# gray_hash: "62d6101d60ee8da38d1b9d8e809091099cec5994",
# rgba_hash: "d764459f778b4839972367f78197bf9a96cd11fd"
# )

let(image) { gray_moon_ppm }

it "rotates" do
image.to_rgba
.draw_square(0, 0, image.width - 1, image.height - 1, CrImage::Color.random)
.draw_circle(375, 250, 5, CrImage::Color.of("#ff0000"), fill: true)
.save("original.jpg")
.rotate(-45, center_x: 375, center_y: 250, radius: 20, pad: true)
.save("rotated.jpg")
end
end
4 changes: 4 additions & 0 deletions src/cr-image/channel_type.cr
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,8 @@ enum CrImage::ChannelType
Blue
Gray
Alpha

def default : UInt8
alpha? ? 255u8 : 0u8
end
end
9 changes: 5 additions & 4 deletions src/cr-image/image.cr
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,14 @@ abstract class CrImage::Image
include Operation::Brightness
include Operation::ChannelSwap
include Operation::Contrast
include Operation::GaussianBlur
include Operation::HorizontalBlur
include Operation::VerticalBlur
include Operation::Crop
include Operation::Pad
include Operation::Draw
include Operation::GaussianBlur
include Operation::HistogramEqualize
include Operation::HorizontalBlur
include Operation::Rotate
include Operation::Pad
include Operation::VerticalBlur
include Format::Save
extend Format::Open

Expand Down
10 changes: 5 additions & 5 deletions src/cr-image/map.cr
Original file line number Diff line number Diff line change
Expand Up @@ -265,14 +265,14 @@ module CrImage
@raw.dup
end

def pad(all : Int32 = 0, *, top : Int32 = 0, bottom : Int32 = 0, left : Int32 = 0, right : Int32 = 0, pad_type : EdgePolicy = EdgePolicy::Black) : self
def pad(all : Int32 = 0, *, top : Int32 = 0, bottom : Int32 = 0, left : Int32 = 0, right : Int32 = 0, pad_type : EdgePolicy = EdgePolicy::Black, pad_black_value : T = T.zero) : self
top = top > 0 ? top : all
bottom = bottom > 0 ? bottom : all
left = left > 0 ? left : all
right = right > 0 ? right : all

new_width = left + width + right
new_raw = initial_raw_pad(pad_type, new_width, top, bottom, left, right)
new_raw = initial_raw_pad(pad_type, new_width, top, bottom, left, right, pad_black_value)

# Now copy the original values into the new raw array at the correct locations
height.times do |y|
Expand All @@ -299,12 +299,12 @@ module CrImage
self.class.new(new_width, new_raw)
end

private def initial_raw_pad(pad_type, new_width, top, bottom, left, right) : Array(T)
private def initial_raw_pad(pad_type, new_width, top, bottom, left, right, pad_black_value) : Array(T)
case pad_type
in EdgePolicy::Black then Array(T).new((top + height + bottom) * new_width) { T.zero }
in EdgePolicy::Black then Array(T).new((top + height + bottom) * new_width) { pad_black_value }
in EdgePolicy::Repeat then Array(T).new((top + height + bottom) * new_width) do |i|
current_y = i // new_width
next T.zero if (current_y) < top || current_y >= (top + height)
next pad_black_value if (current_y) < top || current_y >= (top + height)

adjusted_y = (current_y) - top
adjusted_x = i % new_width
Expand Down
2 changes: 1 addition & 1 deletion src/cr-image/operation/pad.cr
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ module CrImage::Operation::Pad
def pad!(all : Int32 = 0, *, top : Int32 = 0, bottom : Int32 = 0, left : Int32 = 0, right : Int32 = 0, pad_type : EdgePolicy = EdgePolicy::Black) : self
orig_width = width
each_channel do |channel, channel_type|
padded = UInt8Map.new(orig_width, channel).pad(all, top: top, bottom: bottom, left: left, right: right, pad_type: pad_type)
padded = UInt8Map.new(orig_width, channel).pad(all, top: top, bottom: bottom, left: left, right: right, pad_type: pad_type, pad_black_value: channel_type.default)
self[channel_type] = padded.raw
@width = padded.width
@height = padded.height
Expand Down
43 changes: 43 additions & 0 deletions src/cr-image/operation/rotate.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
module CrImage::Operation::Rotate
def rotate(degrees : Float64, *, center_x : Int32 = width // 2, center_y : Int32 = height // 2, radius : Int32 = -1, pad : Bool = false) : self
clone.rotate!(degrees, center_x: center_x, center_y: center_y, radius: radius, pad: pad)
end

def rotate!(degrees : Float64, *, center_x : Int32 = width // 2, center_y : Int32 = height // 2, radius : Int32 = -1, pad : Bool = false) : self
# TODO: use pad

# Rotate backwards, so that we can "look back" from the output pixel location into the input pixel location
radians = -Math::PI * degrees / 180
sin = Math.sin(radians)
cos = Math.cos(radians)
radius_sq = radius * radius

new_size = size
out_center_x = center_x
out_center_y = center_y
each_channel do |channel, channel_type|
new_x = -1
new_y = -1
new_channel = Array.new(new_size) do |i|
new_x += 1
new_x %= width
new_y += 1 if new_x == 0

if radius >= 0
next channel[i] if ((new_x - center_x) ** 2 + (new_y - center_y) ** 2) > radius_sq
end

orig_x = (cos * (new_x - center_x) - sin * (new_y - center_y) + out_center_x).round.to_i
orig_y = (sin * (new_x - center_x) + cos * (new_y - center_y) + out_center_y).round.to_i

next channel_type.default if orig_x < 0 || orig_x >= width || orig_y < 0 || orig_y >= height

channel.unsafe_fetch(orig_y * width + orig_x)
end

self[channel_type] = new_channel
end

self
end
end
5 changes: 5 additions & 0 deletions src/cr-image/rgba_image.cr
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@ class CrImage::RGBAImage < CrImage::Image
)
end

# Returns self
def to_rgba : RGBAImage
self
end

# Return the number of pixels in this image
def size : Int32
@width * @height
Expand Down
8 changes: 4 additions & 4 deletions src/lib-formats/jpeg.cr
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@ module CrImage::Format::JPEG
)
check_jpeg handle, LibJPEGTurbo.destroy(handle)

red = Array.new(width * height) { 0u8 }
green = Array.new(width * height) { 0u8 }
blue = Array.new(width * height) { 0u8 }
alpha = Array.new(width * height) { 255u8 }
red = Array.new(width * height) { ChannelType::Red.default }
green = Array.new(width * height) { ChannelType::Green.default }
blue = Array.new(width * height) { ChannelType::Blue.default }
alpha = Array.new(width * height) { ChannelType::Alpha.default }

(width * height).times do |index|
red.unsafe_put(index, buffer[index * 3])
Expand Down

0 comments on commit 6b46e0b

Please sign in to comment.