이미지 경로 변경 및 텍스트 렌더링 모듈 개선. ROI 영역 설정 추가 및 WCAG 대비 비율 계산 기능 추가. 기존 코드 정리 및 주석 보강.
This commit is contained in:
parent
29a32c4ef6
commit
1b72426779
Binary file not shown.
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.0 MiB |
|
|
@ -16,7 +16,7 @@ import requests
|
|||
|
||||
|
||||
API_ROOT = "http://localhost:7890" # 메인 서버 주소
|
||||
IMAGE_PATH = pathlib.Path("5.jpg")
|
||||
IMAGE_PATH = pathlib.Path("6.jpg")
|
||||
TIMEOUT = 120 # 초
|
||||
|
||||
unwanted_texts = {
|
||||
|
|
@ -43,6 +43,7 @@ toggle_states.update({
|
|||
"blend_mode": "simple", # 단순 블렌딩
|
||||
"performance_mode": True, # 빠른 경로 사용
|
||||
"max_image_size": 1280, # 더 작은 크기 제한
|
||||
"roi_area_high": 0.0 # 기본값: 0.60 → 0.0으로 변경 # 풀프레임 인페인팅 강제
|
||||
|
||||
})
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,496 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
텍스트 렌더링 모듈 - 인페인팅된 이미지에 번역된 텍스트를 자연스럽게 렌더링 (라이브러리화)
|
||||
- /app/worker/fonts/ 내 폰트를 font_map으로 관리
|
||||
- 외부에서 render_text 호출 시 font_number로 폰트 선택
|
||||
- 기본 폰트는 3번(NanumSquareRoundR.ttf)로 설정
|
||||
"""
|
||||
|
||||
import os
|
||||
import math
|
||||
import logging
|
||||
from typing import List, Dict, Any, Tuple, Optional
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
|
||||
class TextRenderingModule:
|
||||
def __init__(self, logger, font_path: Optional[str] = None):
|
||||
"""
|
||||
Args:
|
||||
logger: logger.log(msg, level=logging.INFO) 형태를 지원하는 로거
|
||||
font_path (Optional[str]): 외부에서 기본 폰트 경로를 강제 지정할 때 사용 (보통 None)
|
||||
"""
|
||||
self.logger = logger
|
||||
self.default_font_size = 20
|
||||
self.font_cache: Dict[str, ImageFont.FreeTypeFont] = {}
|
||||
|
||||
# 기본 폰트 번호 (요청사항: 3번)
|
||||
self.default_font_number = 3
|
||||
|
||||
# /app/worker/fonts/ 내 폰트 맵 구성 및 유효성 검사
|
||||
default_path = self._setup_default_fonts()
|
||||
|
||||
# 외부에서 font_path가 들어오면 우선 사용, 없으면 디폴트 선택
|
||||
self.font_path = font_path or default_path
|
||||
|
||||
self.logger.log("텍스트 렌더링 모듈 초기화 완료", level=logging.INFO)
|
||||
self.logger.log(f"기본 폰트 경로: {self.font_path}", level=logging.INFO)
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# 내부 설정: /app/worker/fonts/ 내 폰트들을 번호로 매핑
|
||||
# ---------------------------------------------------------------------
|
||||
def _setup_default_fonts(self) -> Optional[str]:
|
||||
"""
|
||||
/app/worker/fonts/ 경로의 폰트 파일을 번호로 매핑한다.
|
||||
- 1: HakgyoansimDunggeunmisoTTFB.ttf
|
||||
- 2: NanumBarunGothic.ttf
|
||||
- 3: NanumSquareRoundR.ttf (기본)
|
||||
- 4: gamtanload.ttf
|
||||
- 5: Cafe24Ohsquare-v2.0.ttf
|
||||
|
||||
Returns:
|
||||
기본으로 사용할 폰트 경로 (가능하면 3번, 없으면 첫 번째 유효한 폰트, 모두 없으면 None)
|
||||
"""
|
||||
base_path = "/app/worker/fonts/"
|
||||
|
||||
self.font_map: Dict[int, str] = {
|
||||
1: os.path.join(base_path, "HakgyoansimDunggeunmisoTTFB.ttf"),
|
||||
2: os.path.join(base_path, "NanumBarunGothic.ttf"),
|
||||
3: os.path.join(base_path, "NanumSquareRoundR.ttf"),
|
||||
4: os.path.join(base_path, "gamtanload.ttf"),
|
||||
5: os.path.join(base_path, "Cafe24Ohsquare-v2.0.ttf"),
|
||||
}
|
||||
|
||||
# 실제 존재하는 폰트만 남김
|
||||
for key in list(self.font_map.keys()):
|
||||
path = self.font_map[key]
|
||||
if not os.path.exists(path):
|
||||
self.logger.log(f"[경고] 폰트 파일 없음: {path} -> font_map에서 제외", level=logging.WARNING)
|
||||
del self.font_map[key]
|
||||
|
||||
# 기본(3번) 우선, 없으면 첫 번째 유효 폰트, 전무하면 None
|
||||
if self.default_font_number in self.font_map:
|
||||
return self.font_map[self.default_font_number]
|
||||
elif len(self.font_map) > 0:
|
||||
first_path = next(iter(self.font_map.values()))
|
||||
self.logger.log(
|
||||
f"[주의] 기본 3번 폰트가 없어 {first_path}로 대체 사용", level=logging.WARNING
|
||||
)
|
||||
return first_path
|
||||
else:
|
||||
self.logger.log(
|
||||
"[오류] 사용 가능한 폰트가 없습니다. PIL 기본 폰트를 사용합니다.",
|
||||
level=logging.ERROR,
|
||||
)
|
||||
return None
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# 헬퍼: 번호로 폰트 경로 가져오기
|
||||
# ---------------------------------------------------------------------
|
||||
def _get_font_path_by_number(self, font_number: Optional[int]) -> Optional[str]:
|
||||
"""
|
||||
font_number로 self.font_map에서 경로를 선택.
|
||||
- 유효하지 않으면 self.font_path(기본 경로) 반환
|
||||
- 둘 다 없으면 None
|
||||
"""
|
||||
if font_number is not None:
|
||||
if font_number in self.font_map:
|
||||
return self.font_map[font_number]
|
||||
else:
|
||||
self.logger.log(
|
||||
f"[경고] 알 수 없는 font_number={font_number}. 기본 폰트를 사용합니다.",
|
||||
level=logging.WARNING,
|
||||
)
|
||||
return self.font_path
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# 폰트 로딩 / 사이즈 측정
|
||||
# ---------------------------------------------------------------------
|
||||
def get_font(self, size: int, font_path: Optional[str] = None) -> ImageFont.FreeTypeFont:
|
||||
"""
|
||||
폰트를 캐시하여 로딩 비용을 줄임
|
||||
- font_path가 None이면 PIL 기본 폰트 사용
|
||||
"""
|
||||
if font_path is None:
|
||||
cache_key = f"__PIL_DEFAULT__{size}"
|
||||
if cache_key not in self.font_cache:
|
||||
try:
|
||||
font = ImageFont.load_default()
|
||||
self.font_cache[cache_key] = font
|
||||
except Exception as e:
|
||||
self.logger.log(f"폰트 로드 오류(PIL 기본 폰트): {e}", level=logging.ERROR)
|
||||
font = ImageFont.load_default()
|
||||
self.font_cache[cache_key] = font
|
||||
return self.font_cache[cache_key]
|
||||
|
||||
cache_key = f"{font_path}_{size}"
|
||||
if cache_key not in self.font_cache:
|
||||
try:
|
||||
if os.path.exists(font_path):
|
||||
font = ImageFont.truetype(font_path, size)
|
||||
else:
|
||||
self.logger.log(
|
||||
f"[경고] 지정 경로의 폰트가 존재하지 않음: {font_path}. PIL 기본 폰트 사용",
|
||||
level=logging.WARNING,
|
||||
)
|
||||
font = ImageFont.load_default()
|
||||
self.font_cache[cache_key] = font
|
||||
except Exception as e:
|
||||
self.logger.log(f"폰트 로드 오류: {e}. PIL 기본 폰트 사용", level=logging.ERROR)
|
||||
font = ImageFont.load_default()
|
||||
self.font_cache[cache_key] = font
|
||||
return self.font_cache[cache_key]
|
||||
|
||||
def estimate_text_size(self, text: str, font_size: int, font_path: Optional[str] = None) -> Tuple[int, int]:
|
||||
"""
|
||||
주어진 폰트로 텍스트의 렌더링 크기를 추정
|
||||
"""
|
||||
font = self.get_font(font_size, font_path)
|
||||
try:
|
||||
bbox = font.getbbox(text)
|
||||
width = bbox[2] - bbox[0]
|
||||
height = bbox[3] - bbox[1]
|
||||
except AttributeError:
|
||||
# Pillow 버전에 따라 getbbox가 없을 수 있음
|
||||
width, height = font.getsize(text)
|
||||
return width, height
|
||||
|
||||
def calculate_optimal_font_size(
|
||||
self,
|
||||
text: str,
|
||||
target_width: int,
|
||||
target_height: int,
|
||||
min_size: int = 8,
|
||||
max_size: int = 100,
|
||||
font_path: Optional[str] = None,
|
||||
) -> int:
|
||||
"""
|
||||
주어진 영역(target_width, target_height)에 들어가는 최대 폰트 크기 탐색
|
||||
"""
|
||||
best_size = min_size
|
||||
for size in range(min_size, max_size + 1):
|
||||
width, height = self.estimate_text_size(text, size, font_path)
|
||||
if width <= target_width and height <= target_height:
|
||||
best_size = size
|
||||
else:
|
||||
break
|
||||
return best_size
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# 색상 관련
|
||||
# ---------------------------------------------------------------------
|
||||
def _estimate_background_color(self, image: np.ndarray, x1: int, y1: int, x2: int, y2: int) -> Tuple[int, int, int]:
|
||||
"""
|
||||
텍스트가 들어갈 영역 주변의 평균 색상(BGR)을 구한 뒤 RGB로 반환
|
||||
"""
|
||||
margin = 5
|
||||
y1_exp = max(0, y1 - margin)
|
||||
y2_exp = min(image.shape[0], y2 + margin)
|
||||
x1_exp = max(0, x1 - margin)
|
||||
x2_exp = min(image.shape[1], x2 + margin)
|
||||
region = image[y1_exp:y2_exp, x1_exp:x2_exp]
|
||||
mean_color = np.mean(region, axis=(0, 1)) # BGR 평균
|
||||
# RGB 튜플로 변환
|
||||
return (int(mean_color[2]), int(mean_color[1]), int(mean_color[0]))
|
||||
|
||||
def _get_contrasting_color(self, bg_color: Tuple[int, int, int]) -> Tuple[int, int, int]:
|
||||
"""
|
||||
배경색과 대비되는 텍스트 색상(RGB) 선택 (단순 라이트/다크 기준)
|
||||
"""
|
||||
brightness = (bg_color[0] * 0.299 + bg_color[1] * 0.587 + bg_color[2] * 0.114)
|
||||
if brightness > 128:
|
||||
return (0, 0, 0) # 밝으면 검정
|
||||
else:
|
||||
return (255, 255, 255) # 어두우면 흰색
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# 메인 렌더링
|
||||
# ---------------------------------------------------------------------
|
||||
def render_text(
|
||||
self,
|
||||
image: np.ndarray,
|
||||
ocr_results: List[Dict],
|
||||
translated_texts: List[str],
|
||||
font_number: Optional[int] = None,
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
OCR 폴리곤과 번역 텍스트 리스트를 받아 지정된 폰트로 중심 정렬 렌더링
|
||||
|
||||
Args:
|
||||
image: 원본 BGR 이미지 (numpy.ndarray)
|
||||
ocr_results: [{'polygon': [(x,y), ...]}, ...]
|
||||
translated_texts: 각 영역에 대응하는 번역 텍스트 리스트
|
||||
font_number: 사용할 폰트 번호 (None이면 기본 폰트)
|
||||
"""
|
||||
print(f"render_text in translated_texts: {translated_texts}")
|
||||
print(f"render_text in ocr_results : {ocr_results}")
|
||||
print(f"render_text in font_number: {font_number}")
|
||||
result_image = image.copy()
|
||||
|
||||
# 폰트 선택
|
||||
selected_font_path = self._get_font_path_by_number(font_number)
|
||||
|
||||
for i, (ocr_result, translated_text) in enumerate(zip(ocr_results, translated_texts)):
|
||||
polygon = ocr_result['polygon']
|
||||
polygon_array = np.array(polygon)
|
||||
x_coords = polygon_array[:, 0]
|
||||
y_coords = polygon_array[:, 1]
|
||||
|
||||
x_min, x_max = int(np.min(x_coords)), int(np.max(x_coords))
|
||||
y_min, y_max = int(np.min(y_coords)), int(np.max(y_coords))
|
||||
width = max(1, x_max - x_min)
|
||||
height = max(1, y_max - y_min)
|
||||
|
||||
optimal_font_size = self.calculate_optimal_font_size(
|
||||
translated_text, width, height, font_path=selected_font_path
|
||||
)
|
||||
|
||||
text_width, text_height = self.estimate_text_size(
|
||||
translated_text, optimal_font_size, selected_font_path
|
||||
)
|
||||
|
||||
center_x = (x_min + x_max) // 2
|
||||
center_y = (y_min + y_max) // 2
|
||||
text_x = center_x - text_width // 2
|
||||
text_y = center_y - text_height // 2
|
||||
|
||||
angle = 0.0
|
||||
if len(polygon_array) >= 2:
|
||||
dx = float(polygon_array[1][0] - polygon_array[0][0])
|
||||
dy = float(polygon_array[1][1] - polygon_array[0][1])
|
||||
angle = math.degrees(math.atan2(dy, dx))
|
||||
|
||||
bg_color = self._estimate_background_color(image, x_min, y_min, x_max, y_max)
|
||||
text_color = self._get_contrasting_color(bg_color)
|
||||
|
||||
result_image = self.render_text_on_image(
|
||||
result_image,
|
||||
translated_text,
|
||||
(int(text_x), int(text_y)),
|
||||
font_size=optimal_font_size,
|
||||
font_path=selected_font_path,
|
||||
text_color=text_color,
|
||||
background_color=None,
|
||||
angle=angle,
|
||||
)
|
||||
|
||||
return result_image
|
||||
|
||||
def render_text_on_image(
|
||||
self,
|
||||
image: np.ndarray,
|
||||
text: str,
|
||||
position: Tuple[int, int],
|
||||
font_size: Optional[int] = None,
|
||||
font_path: Optional[str] = None,
|
||||
text_color: Tuple[int, int, int] = (0, 0, 0),
|
||||
background_color: Optional[Tuple[int, int, int]] = None,
|
||||
angle: float = 0.0,
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
단일 텍스트를 지정 좌표에 그린다. (RGB 색상 인자 사용)
|
||||
🔥 최적화: 메모리 효율성 개선
|
||||
"""
|
||||
if font_size is None:
|
||||
font_size = self.default_font_size
|
||||
|
||||
# 🔥 최적화: PIL 변환 최소화 - 필요한 영역만 처리
|
||||
h, w = image.shape[:2]
|
||||
text_width, text_height = self.estimate_text_size(text, font_size, font_path)
|
||||
|
||||
# 텍스트 영역만 PIL로 처리하여 메모리 절약
|
||||
padding = 20
|
||||
x1 = max(0, position[0] - padding)
|
||||
y1 = max(0, position[1] - padding)
|
||||
x2 = min(w, position[0] + text_width + padding)
|
||||
y2 = min(h, position[1] + text_height + padding)
|
||||
|
||||
# 작은 영역만 PIL로 변환
|
||||
roi = image[y1:y2, x1:x2]
|
||||
pil_roi = Image.fromarray(cv2.cvtColor(roi, cv2.COLOR_BGR2RGB))
|
||||
draw = ImageDraw.Draw(pil_roi)
|
||||
font = self.get_font(font_size, font_path)
|
||||
|
||||
# 상대 좌표로 조정
|
||||
rel_pos = (position[0] - x1, position[1] - y1)
|
||||
|
||||
# 배경 박스(옵션)
|
||||
if background_color is not None:
|
||||
bg_x1 = rel_pos[0] - 2
|
||||
bg_y1 = rel_pos[1] - 2
|
||||
bg_x2 = rel_pos[0] + text_width + 2
|
||||
bg_y2 = rel_pos[1] + text_height + 2
|
||||
draw.rectangle([bg_x1, bg_y1, bg_x2, bg_y2], fill=background_color)
|
||||
|
||||
# 회전 처리
|
||||
if angle != 0:
|
||||
text_image = Image.new('RGBA', (text_width + 10, text_height + 10), (255, 255, 255, 0))
|
||||
text_draw = ImageDraw.Draw(text_image)
|
||||
text_draw.text((5, 5), text, font=font, fill=text_color + (255,))
|
||||
rotated_text = text_image.rotate(angle, expand=True)
|
||||
pil_roi.paste(rotated_text, rel_pos, rotated_text)
|
||||
else:
|
||||
draw.text(rel_pos, text, font=font, fill=text_color)
|
||||
|
||||
# ROI만 다시 BGR로 변환하여 원본에 적용
|
||||
result_roi = cv2.cvtColor(np.array(pil_roi), cv2.COLOR_RGB2BGR)
|
||||
result_image = image.copy()
|
||||
result_image[y1:y2, x1:x2] = result_roi
|
||||
|
||||
return result_image
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# 스타일 관련 (선택 사용)
|
||||
# ---------------------------------------------------------------------
|
||||
def create_text_styles(self) -> Dict[str, Dict[str, Any]]:
|
||||
"""다양한 텍스트 스타일 정의"""
|
||||
styles = {
|
||||
'default': {
|
||||
'color': (0, 0, 0),
|
||||
'bg_color': None,
|
||||
'outline': True,
|
||||
'outline_color': (255, 255, 255),
|
||||
'outline_width': 1,
|
||||
},
|
||||
'bold': {
|
||||
'color': (0, 0, 0),
|
||||
'bg_color': (255, 255, 255),
|
||||
'outline': True,
|
||||
'outline_color': (128, 128, 128),
|
||||
'outline_width': 2,
|
||||
},
|
||||
'highlight': {
|
||||
'color': (255, 255, 255),
|
||||
'bg_color': (255, 0, 0),
|
||||
'outline': False,
|
||||
'outline_color': None,
|
||||
'outline_width': 0,
|
||||
},
|
||||
'subtle': {
|
||||
'color': (128, 128, 128),
|
||||
'bg_color': None,
|
||||
'outline': True,
|
||||
'outline_color': (255, 255, 255),
|
||||
'outline_width': 1,
|
||||
},
|
||||
}
|
||||
return styles
|
||||
|
||||
def render_with_style(
|
||||
self,
|
||||
image: np.ndarray,
|
||||
ocr_results: List[Dict],
|
||||
translated_texts: List[str],
|
||||
style_name: str = 'default',
|
||||
font_number: Optional[int] = None,
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
스타일 사전의 색/배경/외곽선 옵션을 참고해서 렌더링.
|
||||
현재 코드는 기본 렌더링 결과를 반환하며, 추가 효과(그림자/글로우 등)는 필요 시 확장.
|
||||
"""
|
||||
styles = self.create_text_styles()
|
||||
|
||||
if style_name not in styles:
|
||||
self.logger.log(f"[경고] 알 수 없는 스타일: {style_name}. 'default'로 대체", level=logging.WARNING)
|
||||
style_name = 'default'
|
||||
|
||||
# NOTE: 현재 스타일 색을 직접 반영하려면 render_text 내부에서 색 적용 로직을 확장하면 됨.
|
||||
# 여기서는 기본 렌더링만 수행.
|
||||
result = self.render_text(
|
||||
image=image,
|
||||
ocr_results=ocr_results,
|
||||
translated_texts=translated_texts,
|
||||
font_number=font_number,
|
||||
)
|
||||
|
||||
# (추가 스타일 후처리 자리)
|
||||
return result
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# 텍스트 길이/크기 보정 유틸
|
||||
# ---------------------------------------------------------------------
|
||||
def adjust_text_for_space(
|
||||
self,
|
||||
text: str,
|
||||
max_width: int,
|
||||
max_height: int,
|
||||
font_size: int,
|
||||
) -> Tuple[str, int]:
|
||||
"""
|
||||
공간에 맞게 텍스트 조정 (간단 샘플 로직)
|
||||
- 길면 줄바꿈/생략
|
||||
- 필요 시 폰트 크기 감소
|
||||
"""
|
||||
if len(text) > 20:
|
||||
words = text.split(' ')
|
||||
if len(words) > 1:
|
||||
mid = len(words) // 2
|
||||
text = ' '.join(words[:mid]) + '\n' + ' '.join(words[mid:])
|
||||
else:
|
||||
text = text[:15] + '...'
|
||||
|
||||
adjusted_font_size = font_size
|
||||
while adjusted_font_size > 8:
|
||||
estimated_width = int(len(text) * adjusted_font_size * 0.6)
|
||||
if estimated_width <= max_width:
|
||||
break
|
||||
adjusted_font_size -= 2
|
||||
|
||||
return text, adjusted_font_size
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# 테스트/디버그용 비교 이미지 저장
|
||||
# ---------------------------------------------------------------------
|
||||
def _create_style_comparison(self, images: List[np.ndarray], style_names: List[str]):
|
||||
"""
|
||||
스타일 비교 이미지 생성 (디버그 용도)
|
||||
"""
|
||||
if not images:
|
||||
return
|
||||
|
||||
# 이미지 크기 조정
|
||||
target_width = 200
|
||||
target_height = int(images[0].shape[0] * target_width / images[0].shape[1])
|
||||
|
||||
resized_images = [cv2.resize(img, (target_width, target_height)) for img in images]
|
||||
|
||||
# 비교 캔버스
|
||||
num_images = len(resized_images)
|
||||
comparison_width = target_width * num_images
|
||||
comparison_height = target_height + 30
|
||||
|
||||
comparison = np.ones((comparison_height, comparison_width, 3), dtype=np.uint8) * 255
|
||||
|
||||
# 원본
|
||||
comparison[30:30 + target_height, 0:target_width] = resized_images[0]
|
||||
cv2.putText(comparison, "Original", (10, 20),
|
||||
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1)
|
||||
|
||||
# 스타일들
|
||||
for i, (img, style_name) in enumerate(zip(resized_images[1:], style_names)):
|
||||
x_offset = target_width * (i + 1)
|
||||
comparison[30:30 + target_height, x_offset:x_offset + target_width] = img
|
||||
cv2.putText(comparison, style_name, (x_offset + 10, 20),
|
||||
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1)
|
||||
|
||||
os.makedirs("test_output", exist_ok=True)
|
||||
out_path = "test_output/text_style_comparison.jpg"
|
||||
cv2.imwrite(out_path, comparison)
|
||||
self.logger.log(f"스타일 비교 이미지 저장 완료: {out_path}", level=logging.INFO)
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# 사용 예시 (참고용)
|
||||
# -------------------------------------------------------------------------
|
||||
# logger = your_logger
|
||||
# tr = TextRenderingModule(logger)
|
||||
# result = tr.render_text(
|
||||
# image=origin_img,
|
||||
# ocr_results=[{'polygon': [(10,10),(110,10),(110,40),(10,40)]}],
|
||||
# translated_texts=["예시 텍스트"],
|
||||
# font_number=3 # 기본값 3번, 명시적으로 지정 가능
|
||||
# )
|
||||
# cv2.imwrite("result.jpg", result)
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue