상세 컨텐츠

본문 제목

[Python] jpg, jpeg Exif 정보에서 촬영 날짜 추출하기. (Big Endian, Little Endian 이미지)

Flutter/대용량 파일 전송(Android)

by FDG 2023. 6. 23. 12:57

본문

PIL이나 기타 모듈을 사용하지 않고, jpg Exif에서 직접 촬영 날짜를 추출
Big Endian인지 Little Endian인지에 따라 tag의 byte 순서가 바뀐다! 주의 필요!
두 가지 Endian 형식 이미지 첨부

intel.jpg
0.04MB
motorola.jpg
0.15MB

코드 문제점:

  • 0390h을 찾는데 처음 tag offset에서 12 배수 위치가 아님
  • IFD0 이후와 IFD1 사이에 뭐가 추가로 있는 거 같음

프로그램 개요:

  • 촬영 날짜를 Exif에서 추출
  • 파일명이랑 비교해서 같으면 PASS
  • 다르면 이름 수정
  • 이름 수정할 때 겹치는 이름이 있으면 _1 추가
  • 촬영 날짜를 추출하지 못하면 파일 이름 유지
import os

# byte를 십진수로 변경
def bytes_to_decimal(ifd_offset_bytes,big_endian):
    if big_endian:
        ifd_offset = int.from_bytes(ifd_offset_bytes, byteorder='big') # motorola
    else:
        ifd_offset = int.from_bytes(ifd_offset_bytes, byteorder='little') # intel
    return ifd_offset

# 문자가 숫자인지 검사
def is_numeric_string(text):
    for char in text:
        if not (48 <= ord(char) <= 57):  # ASCII 코드에서 숫자 범위는 48부터 57까지입니다.
            return False
    return True

# jpg에서 exif header에서 촬영 날짜 추출
def get_date_from_jpg(file_path):
    with open(file_path, 'rb') as f:
        data = f.read()
    
    # Find SOI marker
    soi_marker = data.find(b'\xFF\xD8')
    
    if soi_marker>=0:
        
        # JIFF에서 APP0 마커 영역을 사용함. 그래서 Exif Format에서 APP1(마커 번호 0xFFE1)을 사용함
        # https://nightohl.tistory.com/entry/EXIF-Format
        # https://exiftool.org/TagNames/EXIF.html
        
        # Find APP1 marker
        app1_marker = data.find(b'\xFF\xE1', soi_marker)
        # exif_marker = data.find(b'\xFF\xE1')
        if app1_marker ==-1:
            return None
        # Check for EXIF identifier code
        if data[app1_marker + 4:app1_marker + 10] == b'Exif\x00\x00':
            
            tiff_offset = app1_marker + 10
            
            # Check the byte order
            # tiff_offset is starting point of offset !
            byte_allign=data[tiff_offset:tiff_offset + 2]
            if  byte_allign == b'\x4D\x4D':
                big_endian = True
            elif byte_allign == b'\x49\x49':
                big_endian = False
            else:
                return None
        
        # 썸네일까지 포함인가?
        app1_size = bytes_to_decimal(data[app1_marker+2:app1_marker+4],big_endian)
        # print(app1_size)
        # data.find(b'\xFF\xE1', soi_marker)

        app1_number_entry=bytes_to_decimal(data[tiff_offset+8:tiff_offset+10],big_endian)
        
        # # ifd0 출력
        # ifd0_size=app1_number_entry*12
        # ifd0_start=tiff_offset+10
        
        # for i in range(ifd0_start,ifd0_start+app1_number_entry*12,12):
        #     print(i,data[i:i+12])
        #     print(bytes_to_decimal(data[i:i+2],big_endian),
        #     bytes_to_decimal(data[i+2:i+4],big_endian),
        #     bytes_to_decimal(data[i+4:i+8],big_endian),
        #     bytes_to_decimal(data[i+8:i+12],big_endian),
        #     end='')
        #     print('\n')
        #     # hex_data = ''.join([hex(byte)[2:].zfill(2) for byte in data[i:i+12]])
        #     # print(hex_data[0:4],hex_data[4:8],hex_data[8:16],hex_data[16:24])
        
        # Find the DateTimeOriginal tag (Tag ID: 0x9003)
        # component is 1byte from format:02
                
        if big_endian: 
            tag_id_bytes = b'\x90\x03' # motorola
        else:
            tag_id_bytes = b'\x03\x90' # intel
    
        # app1_size내에서 tag 찾는다.
        tag_offset=data.find(tag_id_bytes,tiff_offset,app1_size-4)
        # print('tag offset:',tag_offset)
        
        if tag_offset==-1:
            return None
        
        # component
        DateTimeOriginal_componet_byte=data[tag_offset+4:tag_offset+8]
        DateTimeOriginal_componet=bytes_to_decimal(DateTimeOriginal_componet_byte,big_endian)
        DateTimeOriginal_componet_size=DateTimeOriginal_componet*1 # 1byte
        
        # offset
        DateTimeOriginal_offset_byte=data[tag_offset+8:tag_offset+12]
        DateTimeOriginal_offset=bytes_to_decimal(DateTimeOriginal_offset_byte,big_endian)
        
        # 12칸 뒤에 원하는 값이 있음
        # 끝에서 \x00 제외
        DateTimeOriginal_byte=data[tiff_offset+DateTimeOriginal_offset:tiff_offset+DateTimeOriginal_offset+DateTimeOriginal_componet-1]
        DateTimeOriginal=DateTimeOriginal_byte.decode('utf-8')     
        DateTimeOriginal=DateTimeOriginal.replace(":","")
        DateTimeOriginal_filename=DateTimeOriginal.replace(" ","_")+'.jpg'
        
        picture_date=DateTimeOriginal_filename[:8]
        picture_time=DateTimeOriginal_filename[9:14]
        
        if is_numeric_string(picture_date) and is_numeric_string(picture_time):
            return DateTimeOriginal_filename
        
    return None

# 중복 이름 수정
def duplicate_name_check(filename): 
    # 변경할 파일 이름이 이미 존재하는지 확인
    if os.path.exists(filename):
        # 존재하는 경우에는 "_1"을 추가하여 새로운 이름 생성
        base_name, extension = os.path.splitext(filename)
        new_name = f'{base_name}_1{extension}'
    
        new_name=duplicate_name_check(new_name)
        return new_name
    
    return filename

# jpg 이름 수정
def jpg_rename(file_path):
    filename=get_date_from_jpg(file_path)
    
    if filename == file_path:
        print('SAME:', file_path)
    elif filename != None:
        new_name=duplicate_name_check(filename)
        print("OK:", file_path, new_name)
        os.rename(file_path, new_name)
    else:
        pass
        print("HOLDING(head error):", file_path)
    
# *.jpg
def jpg_filelist():
    # 현재 작업 디렉토리
    directory = os.getcwd()
    
    # 현재 디렉토리에서 모든 파일 및 디렉토리 목록 가져오기
    all_files = os.listdir(directory)
    
    # .jpg 파일 필터링 및 출력
    jpg_files = [file for file in all_files if file.endswith(".jpg")]
    return jpg_files

def main():
    for jpg_file in jpg_filelist():
        jpg_rename(jpg_file)

if __name__ == "__main__":
	main()

관련글 더보기

댓글 영역