본문 바로가기

👩‍💻/개발

flask(python)로 Android/iOS API 구현하기(파일 첨부 추가)

android, iOS 모바일 앱 구현 시 BE API를 구현해야할 일이 생깁니다.

 

그런데 iOS의 경우에는 장비들이 어느 정도 정해져 있지만 android의 경우에는 많이 파편화 되어 있습니다.

 

그래서 파일 첨부 시 다양한 사이즈 형태의 파일을 접하게 됩니다.

 

모든 기기에서 앨범, 카메라 사용 시 처리하게 대응할 수 있지만 모든 장비를 대응할 수는 없기 때문에 

 

BE 서버에서 처리하는게 더 효율적일 수 있습니다.

 

flask로 파일을 처리하는 API 코드 입니다. 

 

import io
from flask import Flask, request, redirect, url_for, render_template
from werkzeug.utils import secure_filename
from PIL import Image, ExifTags
import os
import pyheif

# 프로젝트 설명
# 안드로이드, iOS등 의 경우 파일 업로드 시 다양한 이미지 포맷이 존재하고 파일 사이즈가 다양하기 때문에
# 서버에서 포맷 및 파일 용량을 조정해서 처리하는 로직 입니다.
# / 라우트에서 테스트가 가능하고 smaple_files 폴더에 테스트 파일이 있습니다.

app = Flask(__name__)

# 업로드된 파일을 저장할 디렉토리 설정
UPLOAD_FOLDER = './uploads'
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER

# 업로드 가능한 확장자 목록 설정 (예: 이미지 파일)
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'heic', 'heif', 'webp'}

# 파일 확장자 확인 함수
def allowed_file(filename):
    return '.' in filename and \
        filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

# 파일 업로드 폼 라우트
@app.route('/')
def upload_form():
    return '''
    <!doctype html>
    <title>Upload a File</title>
    <h1>Upload a File</h1>
    <form method="post" enctype="multipart/form-data" action="/upload">
      <input type="file" name="file">
      <input type="submit" value="Upload">
    </form>
    '''

# 파일 업로드 처리 라우트
@app.route('/upload', methods=['POST'])
def upload_file():
    # 파일이 제대로 제출됐는지 확인
    if 'file' not in request.files:
        return 'No file part'

    file = request.files['file']

    # 파일 이름이 비어 있는지 확인
    if file.filename == '':
        return 'No selected file'

    # 파일이 업로드 가능하다면 처리
    if file and allowed_file(file.filename):
        filename = secure_filename(file.filename)

        # 이미지를 처리하여 변환 및 크기 조정
        processed_file, new_filename = process_image(file)
        new_filename = secure_filename(new_filename)  # 파일명 안전하게 처리

        # 변환된 파일 저장
        save_path = os.path.join(app.config['UPLOAD_FOLDER'], new_filename)
        with open(save_path, 'wb') as f:
            f.write(processed_file.read())

        return f'File successfully processed and uploaded: {new_filename}'

    return 'File type not allowed'

# 해당 이미지 파일의 화면 방향을 구하는 함수 (EXIF Orientation)
# 방향을 고려하지 않을 경우 후처리 시 이미지가 회전되어 버림
def get_orientation(image):
    # EXIF 정보 가져오기 (getexif() 사용)
    exif_data = image.getexif()
    orientation = None

    if exif_data is not None:
        # EXIF 태그를 읽어서 Orientation 값 찾기
        for tag, value in exif_data.items():
            tag_name = ExifTags.TAGS.get(tag, tag)
            if tag_name == "Orientation":
                orientation = value
                break

    return orientation

# 파일 사이즈가 1MB가 이상인 경우 리사이즈 함
# 리사이즈 시 jpg로 포멧을 변경
# 업로드 된 파일이 1MB 이하인 경우 ['.jpg', '.jpeg', '.png', '.gif'] 확장자인 경우는 그래도 유지하고 그 외의 확장자는 jpg로 변경한다.

def process_image(uploaded_file):
    # 파일 포인터를 처음으로 이동
    uploaded_file.seek(0)
    content = uploaded_file.read()

    filename = uploaded_file.filename
    ext = os.path.splitext(filename)[1].lower()

    supported_formats = ['.jpg', '.jpeg', '.png', '.gif']

    if ext in ['.heic', '.heif']:
        try:
            heif_file = pyheif.read(content)
            image = Image.frombytes(
                heif_file.mode,
                heif_file.size,
                heif_file.data,
                "raw",
                heif_file.mode,
                heif_file.stride,
            )
            filename = os.path.splitext(filename)[0] + '.jpg'
            ext = '.jpg'
        except Exception as e:
            print(f"Error processing HEIC file: {e}")
            raise

    elif ext in supported_formats:
        try:
            image = Image.open(io.BytesIO(content))
        except Exception as e:
            print(f"Error processing supported format: {e}")
            raise
    else:
        try:
            image = Image.open(io.BytesIO(content))
            filename = os.path.splitext(filename)[0] + '.jpg'
            ext = '.jpg'
        except Exception as e:
            print(f"Error processing unsupported format: {e}")
            raise

    # Orientation 정보 가져오기
    orientation = get_orientation(image)

    # Orientation 값을 기반으로 이미지 회전 적용
    if orientation:
        if orientation == 3:
            image = image.rotate(180, expand=True)
        elif orientation == 6:
            image = image.rotate(270, expand=True)
        elif orientation == 8:
            image = image.rotate(90, expand=True)

    output_io = io.BytesIO()
    quality = 85

    try:
        image.save(output_io, format='JPEG', quality=quality)
    except Exception as e:
        print(f"Error saving image: {e}")
        output_io.seek(0)
        output_io.truncate()
        image.save(output_io, format='JPEG', quality=quality)

    file_size = output_io.tell()
    while file_size > 1 * 1024 * 1024 and quality > 10:
        quality -= 5
        output_io.seek(0)
        output_io.truncate()
        image.save(output_io, format='JPEG', quality=quality)
        file_size = output_io.tell()

    if file_size > 1 * 1024 * 1024:
        width, height = image.size
        scale_factor = (1 * 1024 * 1024 / file_size) ** 0.5
        new_width = int(width * scale_factor)
        new_height = int(height * scale_factor)
        image = image.resize((new_width, new_height), Image.Resampling.LANCZOS)
        output_io.seek(0)
        output_io.truncate()
        image.save(output_io, format='JPEG', quality=quality)

    output_io.seek(0)
    return output_io, filename

if __name__ == '__main__':
    # 업로드 폴더가 없다면 생성
    if not os.path.exists(UPLOAD_FOLDER):
        os.makedirs(UPLOAD_FOLDER)
    app.run(host='0.0.0.0', port=4999)

 

해당 코드 아래의 조건에 대응하게 되어 있습니다.

- 대용량 파일의 경우 1024 px로 리사이징 합니다.

- exif를 이용해서 방향을 보정합니다.

- iOS의 경우 HEIC, HEIF의 경우 jpeg로 변환합니다.