간혹 파일 포맷을 먼저 확인하고, 그에 맞게 처리해야 할 때가 있다. 예를 들어, 이미지 포맷들 중 JPEG와 GIF를 구분하여 다르게 처리해야하는 경우, JPEG는 정적인 단일 이미지로 렌더링 하고 GIF는 이미지 스트림을 지원하므로 소위 말하는 움짤(Animated GIF)로 구현해야 하는 경우가 있다.

그렇다면 파일 포맷이 JPEG 또는 GIF인지 어떻게 확인하면 좋을까?

가장 간단한 방법은 파일 이름의 확장자(extension)로 구분하는 것이다. 가장 간단한 방법이며, 일반적인 경우 문제가 없다. 하지만 사용자가 임의로 파일의 확장자를 변경하는 경우, 의도하지 않은 결과를 낳게 된다. 왜냐하면 확장자를 변경한다고 해서 파일 포맷이 변경되는 것은 아니기 때문이다. JPEG 포맷인 파일인 image.jpg를 확장자만 gif로 한 image.gif로 바꿔도 여전히 파일은 JPEG 포맷이다.

파일 시그니처(File Signature)

파일 시그니처란 파일의 내용을 식별하거나 원본데이터의 손상 여부를 확인하기 위해 사용하는 데이터다. 일반적으로 파일 시그니처는 파일의 앞부분에 몇개의 바이트들로 시작한다. 이를 헤더 시그니처(Header Signature) 또는 매직 넘버(Magic Number)라고 한다.

JPEG와 GIF 헤더 시그니처는 다음과 같다. 시간이 지남에 따라 확장자가 같은 향상된 버전이 포맷이 추가 되면서, 확장자가 같아도 헤더시그니처가 다른 케이스가 다수 존재 한다.

헤더 시그니처 (Hex)ISO 8859-1확장자Description
47 49 46 38 37 61GIF87agif오리지날 GIF 버전 (87년도 공개)
47 49 46 38 39 61GIF89agif향상된 GIF 버전 (89년도 공개)
FF D8 FF DBÿØÿÛjpg, jpegJPEG 포맷
FF D8 FF E0 00 10 4A 46
49 46 00 01
ÿØÿà␀␐JFIF␀␁jpg, jpegJPEG 포맷
FF D8 FF EEÿØÿîjpg, jpegJPEG 포맷
FF D8 FF E1 ?? ?? 45 78
69 66 00 00
ÿØÿá??Exif␀␀jpg, jpegJPEG 포맷
FF D8 FF E0ÿØÿàjpgJPEG 포맷
출처 : https://en.wikipedia.org/wiki/List_of_file_signatures

다른 파일들의 파일 시그니처도 위키피디아에서 확인할 수 있다.

예제) GIF 파일 판별하기

파일 시그니처를 활용하여 GIF 포맷을 확인해보자.

먼저 파일 시그니처를 다음과 같이 열거형 타입으로 선언한다.

    /**
     * 파일들은 각자의 포맷을 갖고 있으며 특정 바이트들로 표현한다. 이를 파일 시그니처 또는 매직넘버라고 한다.
     * 파일 시그니처는 파일의 처음 또는 끝에 위치하며, 각각 헤더 시그니처 그리고 푸터 시그니처라고 부른다.
     *
     * @see <a href="https://en.wikipedia.org/wiki/List_of_file_signatures">파일 시그니처 목록</a>
     */
    private enum class FileSignature(val headerSignature: ByteArray) {
        GIF87a(byteArrayOf(0x47, 0x49, 0x46, 0x38, 0x37, 0x61)),
        GIF89a(byteArrayOf(0x47, 0x49, 0x46, 0x38, 0x39, 0x61))
    }

이제 파일 시그니처를 확인하고 일치 한다면 true 반환하는 함수를 만든다.

    /**
     * @param target 대상 파일
     * @param fileSignature 파일 시그니처
     *
     * @return 대상 파일과 지정된 파일 시그니처가 일치하는지 확인한다. 일치 한다면 true 반환
     */
    private fun checksHeaderSignature(
        target: File,
        fileSignature: FileSignature
    ): Boolean {
        if (!target.exists() && target.isFile) {
            return false
        }
        val buffer = ByteArray(fileSignature.headerSignature.size)
        target.inputStream().use { it.read(buffer, 0, buffer.size) }

        return fileSignature.headerSignature
            .zip(buffer)
            .all { pair -> pair.first == pair.second }
    }

이제 대상 파일이 GIF 파일인지 판별해보자

checksHeaderSignature(file, FileSignature.GIF87a) // true면 GIF87a 포맷
checksHeaderSignature(file, FileSignature.GIF89a) // true면 GIF89a 포맷

Animated-GIF 인지 확인하기 위해 다음과 같이 코드를 더 보완할 수 있다.

    internal fun isGif89a(file: File): Boolean {
        return checksHeaderSignature(file, FileSignature.GIF89a)
    }

    internal fun isGif87a(file: File): Boolean {
        return checksHeaderSignature(file, FileSignature.GIF87a)
    }

    /**
     * Animated-GIF 인지 판별한다.
     *
     * @param file 대상 파일
     * @return Animated-GIF라면 true 반환
     */
    fun isAnimatedGif(file: File): Boolean {
        return if (isGif87a(file) || isGif89a(file)) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
                ImageDecoder.decodeDrawable(ImageDecoder.createSource(file)) is AnimatedImageDrawable
            } else {
                val bytes = file.readBytes()
                Movie.decodeByteArray(bytes, 0, bytes.size) is Movie
            }
        } else {
            false
        }
    }

이 방법은 Android SDK에서 제공하는 기본적인 이미지 디코더를 활용하여 AnimatedImageDrawable 또는 Movie 객체로 변환이 되는지 확인하는 방법이다.

마치며…

파일 시그니처중 시작하는 몇개의 바이트만으로 간략하게 파일 포맷을 확인하는 방법에 대해 알아보았다. 온전한 파일인지 검증까지 해야 하지만, 포맷별로 검증하는 로직이 상이하기 때문에 이 부분은 생략했다. 확인하고 싶은 파일 포맷이 있다면 해당 포맷의 스펙문서를 찾아서 검증하는 로직을 작성해보도록 하자.

미립자 팁! macOS App Store에는 ‘Any File Info‘라는 앱을 무료로 배포하고 있다. 이 앱을 활용 하면 다음과 같이 파일 전체 내용을 확인할 수 있다.

카테고리: etc

0개의 댓글

답글 남기기

Avatar placeholder

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다.