출처 : http://trick14.egloos.com/4459073
내용중 심각하게 잘못된 부분이 몇군데 있습니다. 혹시 이 문서를 참고하시다가 안되시는 부분이 있으시면 저에게 메일 주시면 알려드리겠습니다. 수정된 내용으로의 업데이트는 조만간 하겠습니다.
C++에서는 JPG파일에서 EXIF정보를 뽑아오기 위해 매우 귀찮은 짓을 해야만 했다.
JPG파일을 바이너리로 읽어들여서 1 character씩 검사하고 태그 아이디 검사하여 해당 ID의 value를 변환하여 출력하는 방법이었는데, C#으로 역시 비슷한 삽질을 하다가 하도 답답해서 좀 구글링을 해봤더니 매우 간단하게 EXIF를 뽑아오는 방법이 있다.
분명 C++에 비해 C#이 퍼포먼스 문제가 좀 있지만 어느정도 해결이 가능하다. (Effective C# 참고)
이 내용으로도 언젠가 한번 블로그에 포스팅 해 보려한다.
먼저 필요한 네임스페이스를 추가해준다.
using System.Drawing.Imaging;
using System.Drawing;
코딩이 끝나고 System.Drawing.Imaging을 살펴보니 상당히 유용해 보이는 메소드가 많다. 나중에 한번 쭈~욱 써봐야 할것 같다.
일단 Image 인스턴스 생성.
Image theImage = new Bitmap("F:\\test\\testImage.jpg");
testImage.jpg에 대한 핸들을 생성한뒤 EXIF속성들을 불러온다.
System.Drawing.Imaging.PropertyItem[] propItems = theImage.PropertyItems;
EXIF속성에서 촬영 날짜와 시간에 대한 태그ID는 0x13B가 된다. propItems의 몇번째에 원하는 태그가 저장되는지는 EXIF버전마다 조금씩 다른데, 이 전에는 모두 같은 위치에 저장되는 것으로 생각했다가 오류를 못찾아 한~참 헤멤.
어쨋든 원하는 태그ID를 찾기 위해서는 propItems의 최대크기를 구해서 그 크기만큼 일일히 탐색하여 원하는 ID와 Value가 저장된 배열의 위치를 찾는 방법밖에 없어보인다.
Nikon D60에서 생성되는 JPG의 EXIF에서는 14번 위치에 촬영 날짜와 시간에 대한 정보가 기록되고 propItems는 아래와 같은 멤버 변수들을 가지게 된다.
propItems[14].Id
propItems[14].Len
propItems[14].Value
propItems[14].Type
예를 들어 Nikon D60의 JPG파일에서 멤버 변수의 값들은 아래와 비슷한 값을 가지게 된다.
propItems[14].Id = 0x13B
propItems[14].Len = 2
propItems[14].Value = 2008:05:03 17:45:59
propItems[14].Type = 3
(2였나? 4일수도...-_-;; 확실한 기억이 안나는데 아래서 타입에 대한 설명을 다시 할거니까~)
즉 미친듯이 JPG의 헤더를 캐릭터단위로 읽어와서 확인하지 않아도 저 위에 단 두줄로 이미지 파일의 EXIF정보를 가져올 수 있게된다. 추측하건데 살아있는 모든 EXIF정보를 propItems로 가져오기 때문에 퍼포먼스 문제가 생기는 것으로 보인다.
저 배열에 저장된 정보들은 byte[]타입이고 type 3,4같은경우는 약간 변환을 해야 정상적으로 읽어올 수가 있다. 참고로 각 타입별로 Value를 출력하는 방법이 조금씩 다르니 아래 내용들을 참고해서 작성해보면 되겠다.
< 카메라 모델 정보를 가져오기 위한 부분: 카메라 모델정보가 propItems[1]에 저장되어 있다고 가정>
System.Text.ASCIIEncoding encoding = new System.Text.ASCIIEncoding();
string sModel = encoding.GetString(propItems[1].Value);
sModel = sModel.Substring(0, sModel.Length - 1);
일단 정상적인 텍스트 출력을 위해 ASCII타입으로 인코딩 타입을 지정해준다. 마지막줄은 C++의 strncpy대신 쓸 수 있는 메소드 인데 string의 끝에 '\0'을 제거하기 위해 사용하였다. 이유라면 카메라 모델명을 파일이름으로 쓰려고 보니 끝에 '\0'때문에 정상적으로 파일명이 생성되질 않더라..
<날짜와 시간 정보를 가져오기 위한 부분>
string sDate = encoding.GetString(propItems[14].Value);
sDateFolderName = string.Format("{0}.{1}.{2}",
sDate.Substring(0, 4), //2008
sDate.Substring(5,2), // 09
sDate.Substring(8,2)); // 25
sTimeFileName = sDate.Substring(11, 2) // 17
+ sDate.Substring(14, 2) // 47
+ sDate.Substring(17, 2); // 59
날짜정보(sDateFolderName)와 촬영시간정보(sTimeFileName)에 대한 string을 저장하기 위해 두 가지 방법을 사용해 보았는데 두 경우의 퍼포먼스를 비교해 보기 위함이다.
일단 결론적으로 말하면 위 쪽이 리소스를 덜 잡아먹는다. 절대로 아래처럼 사용하지 말자(T.T)
이 부분에서 특별한 것은 없다. 원래 2008:09:25 17:47:59 식으로 되어있는 배열을 2008.09.25와 174759로 나누기 위한 부분.
<가로해상도와 세로해상도가 propItems[31], [32]에 있다고 가정한 경우>
string xRes = Convert.ToUInt16(propItems[31].Value[1] << 8 | propItems[31].Value[0]).ToString();
string yRes = Convert.ToUInt16(propItems[32].Value[1] << 8 | propItems[32].Value[0]).ToString();
보기에도 범상치 않아보이는 방법인데...
일단 당연히 ulong 또는 ushort타입이라고 해서 간단히 Value 또는 Value.ToString()으로 찍는줄 알고 시도해 보았으나 계속되는 Exception Error. 생각해보니 C++로 구현했을때도 이부분에서 한참 헤멨던 기억이 나서 다시 찾아보니 비트연산을 좀 해줘야 한다. 쉽게 말하면 두개의 byte의 위치를 바꿔주는 연산.
일단 완성된 어플의 목적은 지정된 디렉토리 이하의 모든 JPG를 탐색해서 날짜정보별로 디렉토리를 생성하고, 파일이름을 카메라이름,_촬영시간,_해상도_일련번호로 바꾸고 해당 디렉토리로 이동시키는 것이다.
전체 흐름을 보자.
foreach (string fileEntry in Directory.GetFiles(sDir, "*.jpg", SearchOption.AllDirectories))
{
Image theImage = new Bitmap(fileEntry);
System.Drawing.Imaging.PropertyItem[] propItems = theImage.PropertyItems;
sDate = encoding.GetString(propItems[14].Value);
sDateFolderName = string.Format("{0}.{1}.{2}",
sDate.Substring(0, 4), //2008
sDate.Substring(5,2), // 09
sDate.Substring(8,2)); // 25
sTimeFileName = string.Format("{0}{1}{2}",
sDate.Substring(11, 2), // 17
sDate.Substring(14, 2), // 47
sDate.Substring(17, 2)); // 59
if (!Directory.Exists(dDir + sDateFolderName))
{ // 디렉토리가 없으면 새로 생성
Directory.CreateDirectory(dDir + sDateFolderName);
}
xRes = Convert.ToUInt16(propItems[31].Value[1] <<>
yRes = Convert.ToUInt16(propItems[32].Value[1] <<>
sModel = encoding.GetString(propItems[1].Value);
sModel = sModel.Substring(0, sModel.Length - 1);
// 파일명 설정
fFileName = string.Format("{0}{1}{2}", dDir, sDateFolderName, "\\");
fFileName = string.Format("{0}{1}_", fFileName, sModel); // 카메라명 추가
fFileName = string.Format("{0}{1}_", fFileName, sTimeFileName); // 촬영시간 추가
fFileName = string.Format("{0}{1}x{2}_", fFileName, xRes, yRes); // 해상도 추가(기본)
fFileName = string.Format("{0}{1}.jpg", fFileName, count.ToString());
// 파일 이동
theImage.Dispose(); // 요걸 안해주면 파일핸들이 계속 열려있으므로 파일이동이 안됨
File.Move(fileEntry, fFileName);
}
일단 예전에 올렸던 것에서 내용을 좀 많이 수정했는데 string에 대해 +연산보다 string.Format을 사용하여 퍼포먼스를 향상시켰고, 타입에 대해 .ToString()을 적극적으로 사용했다. 중간에 Exception 핸들링이나 에러 처리, 로그파일부분은 생략했지만 만약 이런류의 프로그램을 작성한다면 사용자들이 변경한 EXIF정보에 대한 에러처리를 철저히 해 주어야 한다.