338 lines
16 KiB
Python
338 lines
16 KiB
Python
import base64
|
|
import pyperclip
|
|
import win32clipboard
|
|
from io import BytesIO
|
|
from PIL import Image, ImageGrab
|
|
import requests
|
|
import numpy as np
|
|
import cv2
|
|
import time
|
|
import os
|
|
from datetime import datetime
|
|
import random
|
|
import asyncio
|
|
import pyperclip # 클립보드 데이터를 확인하기 위한 라이브러리
|
|
|
|
class ClipboardImageManager:
|
|
def __init__(self, app, logger, browser_controller, debug=False):
|
|
self.app = app
|
|
self.logger = logger
|
|
self.browser_controller = browser_controller # BrowserController 인스턴스를 전달받음
|
|
self.debug = debug # 디버그 플래그를 클래스 변수로 사용
|
|
|
|
self.debug = False
|
|
|
|
def get_clipboard_data(self):
|
|
"""클립보드의 텍스트 또는 이미지 데이터를 가져옵니다."""
|
|
self.logger.debug("클립보드의 텍스트 또는 이미지 데이터를 가져옵니다")
|
|
try:
|
|
# 1. 텍스트 데이터 우선 시도
|
|
clipboard_text = pyperclip.paste()
|
|
if clipboard_text:
|
|
return clipboard_text
|
|
|
|
# 2. 텍스트가 없으면 이미지 확인
|
|
self.logger.debug("텍스트 데이터가 없어 이미지 데이터 확인 시도")
|
|
image = ImageGrab.grabclipboard()
|
|
if isinstance(image, Image.Image): # 이미지 데이터가 있는 경우
|
|
self.logger.debug("클립보드에 이미지 데이터가 확인되었습니다.")
|
|
return image # PIL 이미지 객체 반환
|
|
else:
|
|
self.logger.debug("클립보드에 텍스트 또는 이미지 데이터가 없습니다.")
|
|
return None
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"클립보드 데이터를 가져오는 중 오류 발생: {e}", exc_info=True)
|
|
return None
|
|
|
|
def set_image_to_clipboard(self, image):
|
|
"""이미지를 클립보드에 넣는 함수 (Windows 전용)"""
|
|
output = BytesIO()
|
|
image.save(output, "BMP")
|
|
self.logger.debug(f"이미지 데이터 BMP 변환")
|
|
|
|
data = output.getvalue()[14:] # BMP 헤더 제거
|
|
output.close()
|
|
self.logger.debug(f"이미지 BMP 헤더 제거")
|
|
|
|
# 클립보드에 이미지 데이터 넣기
|
|
win32clipboard.OpenClipboard()
|
|
win32clipboard.EmptyClipboard()
|
|
win32clipboard.SetClipboardData(win32clipboard.CF_DIB, data)
|
|
win32clipboard.CloseClipboard()
|
|
|
|
# 클립보드가 제대로 설정되었는지 확인하는 로그
|
|
time.sleep(0.1) # 아주 짧은 대기 시간
|
|
win32clipboard.OpenClipboard()
|
|
if win32clipboard.IsClipboardFormatAvailable(win32clipboard.CF_DIB):
|
|
self.logger.debug("클립보드 데이터 저장 성공")
|
|
else:
|
|
self.logger.error("클립보드 데이터 저장 실패")
|
|
win32clipboard.CloseClipboard()
|
|
|
|
def save_image_to_path(self, image, path):
|
|
try:
|
|
if image:
|
|
# 이미지를 저장 경로에 저장
|
|
self.logger.info(f"이미지 저장 완료 : {path}")
|
|
image.save(path)
|
|
return path
|
|
|
|
except Exception as e:
|
|
raise RuntimeError(f"이미지 저장 중 오류 발생: {e}")
|
|
|
|
# async def set_image_to_clipboard(self, image, crop_percentage=0.03, debug=False):
|
|
# """
|
|
# 이미지를 클립보드에 넣는 함수 (Windows 전용, 크롭 후)
|
|
|
|
# :param image: PIL 이미지 객체
|
|
# :param crop_percentage: 크롭할 비율 (0.05는 5% 크롭을 의미)
|
|
# :param debug: True일 경우 크롭 전후 다양한 비율(3%, 5%, 7%)의 이미지를 디버그 용도로 저장
|
|
# """
|
|
# # 이미지의 크기 계산 (크롭 비율 적용)
|
|
# width, height = image.size
|
|
# left = width * crop_percentage
|
|
# top = height * crop_percentage
|
|
# right = width * (1 - crop_percentage)
|
|
# bottom = height * (1 - crop_percentage)
|
|
|
|
# # 크롭 적용
|
|
# cropped_image = image.crop((left, top, right, bottom))
|
|
|
|
# if debug:
|
|
# # 디버그 모드일 경우 크롭 전후 다양한 비율로 이미지 저장
|
|
# timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
# original_image_path = os.path.join(os.getcwd(), f"original_image_{timestamp}.png")
|
|
# image.save(original_image_path) # 크롭 전 이미지 저장
|
|
# self.logger.debug(f"크롭 전 이미지 저장됨: {original_image_path}")
|
|
|
|
# # 3%, 5%, 7% 크롭 이미지 저장
|
|
# crop_alternatives = [0.03, 0.05, 0.07]
|
|
# for crop in crop_alternatives:
|
|
# left_alt = width * crop
|
|
# top_alt = height * crop
|
|
# right_alt = width * (1 - crop)
|
|
# bottom_alt = height * (1 - crop)
|
|
|
|
# cropped_alt_image = image.crop((left_alt, top_alt, right_alt, bottom_alt))
|
|
# cropped_image_path = os.path.join(os.getcwd(), f"cropped_image_{int(crop*100)}_{timestamp}.png")
|
|
# cropped_alt_image.save(cropped_image_path)
|
|
# self.logger.debug(f"{int(crop*100)}% 크롭된 이미지 저장됨: {cropped_image_path}")
|
|
|
|
# # 크롭된 이미지를 BMP 형식으로 변환하여 클립보드에 넣기
|
|
# output = BytesIO()
|
|
# cropped_image.save(output, "BMP")
|
|
# data = output.getvalue()[14:] # BMP 헤더 제거
|
|
# output.close()
|
|
|
|
# # 클립보드에 이미지 데이터 넣기
|
|
# win32clipboard.OpenClipboard()
|
|
# win32clipboard.EmptyClipboard()
|
|
# win32clipboard.SetClipboardData(win32clipboard.CF_DIB, data)
|
|
# win32clipboard.CloseClipboard()
|
|
|
|
# self.logger.debug(f"{crop_percentage*100}% 크롭된 이미지가 클립보드에 저장되었습니다.")
|
|
|
|
def base64_to_image(self, base64_data):
|
|
"""Base64 데이터를 이미지로 변환하는 함수"""
|
|
if base64_data.startswith('data:image'):
|
|
header, encoded = base64_data.split(',', 1)
|
|
img_data = base64.b64decode(encoded)
|
|
image = Image.open(BytesIO(img_data))
|
|
return image
|
|
else:
|
|
self.logger.debug("유효하지 않은 Base64 이미지 데이터입니다.")
|
|
return None
|
|
|
|
async def download_image_from_url(self, url, max_retries=3):
|
|
"""URL에서 이미지를 다운로드하고 PIL 이미지 객체로 반환"""
|
|
headers = {
|
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36",
|
|
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
|
|
"Accept-Language": "en-US,en;q=0.9",
|
|
"Accept-Encoding": "gzip, deflate, br",
|
|
"DNT": "1", # Do Not Track 요청 헤더
|
|
"Connection": "keep-alive",
|
|
"Upgrade-Insecure-Requests": "1",
|
|
"Cache-Control": "max-age=0"
|
|
}
|
|
|
|
retries = 0
|
|
while retries < max_retries:
|
|
try:
|
|
self.logger.debug(f"이미지 URL 다운로드 중: {url}")
|
|
response = requests.get(url, headers=headers, stream=True)
|
|
|
|
# 상태 코드가 200이 아니면 재시도
|
|
if response.status_code == 200:
|
|
# OpenCV로 이미지를 로드하여 변환
|
|
image = np.asarray(bytearray(response.content), dtype="uint8")
|
|
image = cv2.imdecode(image, cv2.IMREAD_COLOR)
|
|
|
|
# OpenCV에서 이미지를 PIL로 변환
|
|
if image is not None:
|
|
pil_image = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
|
|
return pil_image
|
|
else:
|
|
self.logger.debug(f"이미지 파일 형식이 올바르지 않습니다. 대상 URL: {url}")
|
|
return None
|
|
else:
|
|
self.logger.debug(f"이미지 로딩 실패, HTTP 상태 코드: {response.status_code}. 재시도 {retries + 1}/{max_retries}")
|
|
retries += 1
|
|
# await asyncio.sleep(random.randint(2, 5)) # 2~5초 대기 후 재시도
|
|
time.sleep(random.randint(2, 5))
|
|
|
|
except Exception as e:
|
|
self.logger.debug(f"이미지 로딩 중 오류 발생: {e}. 재시도 {retries + 1}/{max_retries}")
|
|
retries += 1
|
|
# await asyncio.sleep(random.randint(2, 5)) # 예외 발생 시 대기 후 재시도
|
|
time.sleep(random.randint(2, 5))
|
|
|
|
self.logger.debug("이미지 다운로드 최대 재시도 횟수를 초과했습니다.")
|
|
return None
|
|
|
|
def process_clipboard(self, original_url, path=None):
|
|
"""클립보드의 내용을 처리하고, 필요한 경우 이미지 변환, 크롭 또는 클립보드 비우기"""
|
|
|
|
try:
|
|
clipboard_data = self.get_clipboard_data()
|
|
|
|
self.logger.debug("clipboard_data")
|
|
self.logger.debug(f"{clipboard_data}")
|
|
self.logger.debug(f"============================")
|
|
|
|
# 1. 클립보드의 데이터가 Base64 이미지일 경우
|
|
if isinstance(clipboard_data, str) and clipboard_data.startswith('data:image'):
|
|
self.logger.info("[process_clipboard] data:image 감지 : 이미지 데이터로 변환")
|
|
image = self.base64_to_image(clipboard_data)
|
|
if image:
|
|
width, _ = image.size
|
|
self.logger.debug(f"Base64 이미지 크기: {width}px")
|
|
|
|
# 가로 크기가 200픽셀 이상이면 크롭
|
|
if width >= 200:
|
|
self.logger.debug("이미지 가로 크기 200픽셀 이상: 크롭 진행 중...")
|
|
cropped_image = self.crop_image(image) # 크롭 메서드 사용
|
|
self.set_image_to_clipboard(cropped_image) # 클립보드에 저장
|
|
if path:
|
|
self.logger.debug("이미지 저장 시도...")
|
|
self.save_image_to_path(path)
|
|
else:
|
|
self.logger.debug("이미지 가로 크기 200픽셀 이하: 클립보드 비움.")
|
|
self.clear_clipboard()
|
|
else:
|
|
self.logger.debug("Base64 이미지 변환 실패.")
|
|
|
|
# 2. 클립보드에 이미지가 있을 경우
|
|
elif isinstance(clipboard_data, Image.Image):
|
|
self.logger.info("[process_clipboard] 클립보드 이미지 확인")
|
|
|
|
image = clipboard_data
|
|
width, _ = image.size
|
|
self.logger.debug(f"클립보드에 있는 이미지 크기: {width}px")
|
|
|
|
if width >= 200:
|
|
self.logger.debug("이미지 가로 크기 200픽셀 이상: 크롭 진행 중...")
|
|
cropped_image = self.crop_image(image) # 크롭 메서드 사용
|
|
self.set_image_to_clipboard(cropped_image) # 클립보드에 저장
|
|
if path:
|
|
self.logger.debug("이미지 저장 시도...")
|
|
self.save_image_to_path(path)
|
|
|
|
else:
|
|
self.logger.debug("이미지 가로 크기 200픽셀 이하: 클립보드 비움.")
|
|
self.clear_clipboard()
|
|
|
|
# 3. 클립보드에 데이터가 없거나 html > whale-ocr 처리
|
|
elif clipboard_data == "html > whale-ocr" or clipboard_data is None:
|
|
if clipboard_data == "html > whale-ocr":
|
|
self.logger.info("[process_clipboard] html > whale-ocr 감지 : 이미지 번역 실패 확인")
|
|
elif clipboard_data is None:
|
|
self.logger.info("[process_clipboard] 클립보드에 이미지 없음")
|
|
|
|
if original_url:
|
|
image = self.download_image_from_url(original_url)
|
|
if image:
|
|
self.logger.debug("원본 이미지 다운로드 성공, 클립보드에 저장 중...")
|
|
self.set_image_to_clipboard(image) # 크롭 없이 저장
|
|
if path:
|
|
self.logger.debug("이미지 저장 시도...")
|
|
self.save_image_to_path(path)
|
|
else:
|
|
self.logger.debug("원본 이미지 다운로드 실패.")
|
|
else:
|
|
self.logger.debug("원본 이미지 URL을 찾을 수 없습니다.")
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"클립보드에서 이미지를 처리하는 중 오류 발생: {e}", exc_info=True)
|
|
|
|
|
|
def is_clipboard_image(self):
|
|
"""클립보드에 이미지가 있는지 확인하는 함수"""
|
|
is_clipboard_image_flag = win32clipboard.IsClipboardFormatAvailable(win32clipboard.CF_DIB)
|
|
if is_clipboard_image_flag:
|
|
self.logger.debug("클립보드에 이미지가 존재합니다.")
|
|
else:
|
|
self.logger.debug("클립보드에 이미지가 없습니다.")
|
|
|
|
return is_clipboard_image_flag
|
|
|
|
def get_image_from_clipboard(self):
|
|
"""클립보드에서 이미지를 가져오는 함수"""
|
|
try:
|
|
win32clipboard.OpenClipboard()
|
|
if self.is_clipboard_image():
|
|
dib_data = win32clipboard.GetClipboardData(win32clipboard.CF_DIB)
|
|
image = Image.open(BytesIO(dib_data))
|
|
return image
|
|
else:
|
|
self.logger.debug("클립보드에 이미지가 없습니다.")
|
|
except Exception as e:
|
|
self.logger.error(f"클립보드에서 이미지를 가져오는 중 오류 발생: {e}", exc_info=True)
|
|
finally:
|
|
win32clipboard.CloseClipboard()
|
|
|
|
return None
|
|
|
|
def clear_clipboard(self):
|
|
"""클립보드를 비우는 함수"""
|
|
try:
|
|
win32clipboard.OpenClipboard()
|
|
win32clipboard.EmptyClipboard()
|
|
self.logger.debug("클립보드가 비워졌습니다.")
|
|
except Exception as e:
|
|
self.logger.error(f"클립보드를 비우는 중 오류 발생: {e}", exc_info=True)
|
|
finally:
|
|
win32clipboard.CloseClipboard()
|
|
|
|
def crop_image(self, image, crop_percentage=0.01):
|
|
"""이미지를 주어진 퍼센트만큼 크롭하는 함수"""
|
|
width, height = image.size
|
|
left = width * crop_percentage
|
|
top = height * crop_percentage
|
|
right = width * (1 - crop_percentage)
|
|
bottom = height * (1 - crop_percentage)
|
|
|
|
cropped_image = image.crop((left, top, right, bottom))
|
|
|
|
if self.debug:
|
|
# 디버그 모드일 경우 크롭 전후 다양한 비율로 이미지 저장
|
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
original_image_path = os.path.join(os.getcwd(), f"original_image_{timestamp}.png")
|
|
image.save(original_image_path) # 크롭 전 이미지 저장
|
|
self.logger.debug(f"크롭 전 이미지 저장됨: {original_image_path}")
|
|
|
|
# 3%, 5%, 7% 크롭 이미지 저장
|
|
crop_alternatives = [0.01, 0.02, 0.03]
|
|
for crop in crop_alternatives:
|
|
left_alt = width * crop
|
|
top_alt = height * crop
|
|
right_alt = width * (1 - crop)
|
|
bottom_alt = height * (1 - crop)
|
|
|
|
cropped_alt_image = image.crop((left_alt, top_alt, right_alt, bottom_alt))
|
|
cropped_image_path = os.path.join(os.getcwd(), f"cropped_image_{int(crop*100)}_{timestamp}.png")
|
|
cropped_alt_image.save(cropped_image_path)
|
|
self.logger.debug(f"{int(crop*100)}% 크롭된 이미지 저장됨: {cropped_image_path}")
|
|
|
|
return cropped_image |