이미지 실루엣 만들기

7과11사이·2024년 4월 21일
1
post-custom-banner

특정 이미지의 테두리를 적용할 필요가 생겼다.
단순히 테두리라기보다 이미지의 실루엣이라고 하는게 더 맞겠다.
스택오버 플로우를 검색해보다 방법을 찾았는데, 해당 방식이 deprecated되어 버리는 바람에 iOS 10버전에 소개된 API로 변경해서 적용해보았는데, 관련해서 내용을 적어본다.


이미지 테두리 만드는 방법

구현했었던 2가지 방법을 적어본다. 방법1같은 경우 deprecated 될 예정이며 메서드가 필수 메서드가 나뉘어 있어 따로 호출하는 것을 잊을 수 있기에 편의상 최대한 UIGraphicsImageRenderer을 생성해서 구현하는 것이 가장 좋을 것 같다.

  • 방법 1. Deprecated - iOS 2.0

    extension UIImage {
        func colorized(with color: UIColor = .white) -> UIImage {
            UIGraphicsBeginImageContextWithOptions(size, false, scale)
    
            defer {
                UIGraphicsEndImageContext()
            }
    
            guard let context = UIGraphicsGetCurrentContext(), let cgImage = cgImage else { return self }
            let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height)
    
            color.setFill()
            context.translateBy(x: 0, y: size.height)
            context.scaleBy(x: 1.0, y: -1.0)
            context.clip(to: rect, mask: cgImage)
            context.fill(rect)
    
            guard let colored = UIGraphicsGetImageFromCurrentImageContext() else { return self }
            return colored
        }
    }
  • 방법 2. iOS 10 update

    extension UIImage {
    
        //MARK: - previous bitmap workaround deprecated - Update to iOS 10
        func createOutline(color: UIColor = .white) -> UIImage {
    
            // create an instance of UIGrpahicImageRender
            let renderer = UIGraphicsImageRenderer(size: size)
    
            let image = renderer.image { context in
                // sets new position for image position.
                let rect = CGRect(origin: .zero, size: size)
    
                if let cgImage = cgImage {
                    // fills the image with given color
                    color.setFill()
                    // Core Graphics origin shift to match UIKit coordinate origin [top left]
                    context.cgContext.translateBy(x: 0, y: size.height)
                    context.cgContext.scaleBy(x: 1, y: -1)
                    // clip the image's mask to rectangle & fill in the color
                    context.cgContext.clip(to: rect, mask: cgImage)
                    context.fill(rect)
                }
            }
            return image
        }
    }

🚨 주의사항

ImageView에 draw를 적용해볼 생각은 하지 말자.

extension으로 굳이 만드는 이유가 무엇일까 생각을 했었는데, UIImageView 공식문서에서 '이미지가 보이지 않는다면' 영역을 읽으면서 답을 찾게 됐다. UIImageView에서 커스텀 드로잉을 하지 말라는 안내였는데, UIImageView가 이미지와 연관된 View이지만 이미지를 출력하기 위해 사용되는 view라고 강조한다. 별도로 이미지를 그리거나 수정해야하는 경우가 있다면 UIView를 상속받아 활용하라고 안내를 한다.

따라서 draw 메서드를 대신하기 위해 위 2가지 방법으로 이미지를 추출, 수정, 저장하는 과정을 거쳤는데, 해당 방법은 공식문서를 위반하지 않으니 안심하고 사용해도 될 듯하다.

그렇다면 이해해보는 시간을 가져보자.


1. Before iOS 10...

공식 문서를 읽었는 때 UIGraphicsBeginImageContextWithOptions 방식은 이미지 렌더링을 비트맵으로 변환해주는 것으로 이해했다. 하지만 단순히 변환이라기 보다 '생성'이 더 맞는 표현인 것 같다.

size, opaque, scale 인자를 받아드리는데,해당 값을 활용하여 새로운 비트맵 이미지가 생성되는 것으로 이해하면 될 듯하다.

과정을 거쳐보면
① 새 비트맵 이미지를 지정한 크기로 생성,
② 해당 비트맵 이미지값을 수정하기 위해 context를 추출,
③ context에서 크기를 맞춰준 이후 clip을 통해 새 비트맵 이미지를 이미지에 맞게 자르는 순서로 이해했다.

한가지 특징은 BeginImageContext로 이미지 context를 가져온 뒤 수정이 다 끝난 이후에는 아래처럼 UIGraphicsEndImageContext()를 무조건 호출해주어야 한다고 한다.

defer {
	UIGraphicsEndImageContext()
}

이유는 새로운 비트맵 이미지를 생성한 만큼, 메모리와 데이터에서 context를 지우기 위함이라고 한다. 위 코드에서는 메서드를 종료 시키기 전에 UIGraphicsGetImageFromCurrentImageContext()로 비트맵 이미지 데이터를 colored 변수에 담아 리턴하고, 이후 defer에 있던 EndImageContext()가 호출되어 종료되는 순이다.

추가 업데이트

UIGraphicsGetCurrentContext()는 현재 GC의 참조값을 가져온다고 한다.
반대로 UIGraphicsBeginImageContext는 새로 생성하는 것이라고 한다.

아울러 UIView 또는 View를 생성해서 이미지를 만드는 과정을 거치지 않기에 퍼포먼스 면에서도 긍정적인 요소라고 하는데, 이유는 저장된 asset이 아니기에 디스크를 읽는 시간이 필요없기 때문이라고 한다.
이외에도...

  • UIGraphicsBegin/EndContext는 색 영역을 sRGB만 지원을 한다고 한다. (P3는 불가)
  • Block이 사용되기 전에 탄생된 코드
    따라서 과거 C 언어 기반으로 생성된 API라는 점이 가장 큰 차이점이다.

[출처], [출처2]



2. UIGraphicImageRenderer

iOS 10 이후 소개된 이미지 렌더러에서는 CG context를 별도로 관리할 필요가 없어졌다. 아울러 UIImage를 접근할 때 png, jpg로 변환할 수 있게 되었으며 Thread-safe라고 한다.

	let renderer = UIGraphicsImageRenderer(size: size)
	let image = renderer.image { context in
	let rect = CGRect(origin: .zero, size: size)

	if let cgImage = cgImage {
        // 상세 수정...
    }

이미지 렌더러는 색상 깊이, 스케일 또는 context 등의 별도의 수정을 할 필요가 없다고 한다. 더불어 하나의 객체를 생성해 두면 이미지에 대한 정보를 캐시로 저장해두기에 반복적으로 만들 필요가 없다고 한다.

간단하게 imageRenderer을 생성한 이후, 클로저 형태로 context를 접근해서 이미지 수정을 이루면 된다.


3. CGContext는 무엇일까

공부하다보니 CGContext가 매우 중요한 포인트로 작용하고 있었는데,
조금 더 디테일하게 들어가 보도록 하자!

Quartz 2d 프로그래밍 가이드에선 Graphic Context를 드로잉 위치를 의미한다고 한다. 해당 Context에서 개발자는 기본적인 드로잉 특성을 지정할 수 있는데, 그림을 드릴 때 사용하는 색상, clip할 위치, 선의 두께 등을 의미한다. 간단하게 보면 CGContext (CG = Core Graphics)는 iOS에서 이미지를 그릴 때 생성하는 객체라 볼 수 있겠다. iOS에서는 이미지를 그리기 위해 UIView에서 제공하는 draw 메서드를 오버라이드 하는 방식이라고 한다.

override func draw(_ rect: CGRect) {
	super.draw(rect)
}

위처럼 기본적으로 draw는 CGRect 값을 받기에 네모난 상자만 그릴 수 있게 되어 있는데, 이는 특정한 영역에 있는 뷰 이미지를 새로 그려 업데이트하는 상황에 사용을 하기 때문인 것으로 보여진다. 하지만 이 모양은 다르게 생성할 수 있는데, 이 때 필요한 정보가 CGContext인 것으로 보인다.

iOS 9까지는 UIGraphicsGetCurrentContext() 메서드를 호출하여 현재 이미지의 상세 데이터를 가져오고 수정할 수 있게 되어 있는데 - 한 가지 특징은 이렇게 가져온 이미지는 반전된다는 점이다.

UIKit는 이미지를 생성할 떄 혹은 위치를 계산할 때 좌측 상단이 중점이 된다. 따라서 이미지 크기를 줄이거나 frame 크기를 변경하게 되면 해당 변경 값은 좌측 상단을 (0,0)을 기준으로 두고 변경된 값을 적용할 것이다.

하지만 CG는 다르다. Coordinate System 값이 전혀 다르다고 하는데,
좌측 상단이 아닌 좌측 하단에서 시작한다고 한다. 따라서 CGContext로 새로 생성한 이미지는 반전되어 있는 상황이기 때문에 UIKit에 맞는 형식으로 바꾸기 위해 context.ScaledBy에서 y축을 반전하게 되는 것이다.

참고

post-custom-banner

0개의 댓글