customer_template_service.py 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. from email.policy import default
  2. from settings import *
  3. from middleware import UnicornException
  4. import copy
  5. import requests
  6. from PIL import Image
  7. from io import BytesIO
  8. import base64,shutil
  9. from logger import logger
  10. '''前端生图接口'''
  11. generate_templace = "/generate"
  12. class CustomerTemplateService:
  13. def __init__(self):
  14. pass
  15. def generateTemplate(self,config_data,template_json,template_name,save_path):
  16. '''
  17. 参数:
  18. config_data: 配置数据
  19. template_json: 模板数据
  20. template_name: 模板名称
  21. save_path: 保存路径
  22. '''
  23. print("开始生成模板")
  24. # print("config_data",config_data)
  25. handler_config_data,model_image,scene_image = self.__handler_config_data(config_data)
  26. goods_no = list(handler_config_data.keys())[0]
  27. headers = {"Content-Type": "application/json"}
  28. json_data = {"goodsList":[handler_config_data],"canvasList":template_json}
  29. json_data = json.dumps(json_data,ensure_ascii=False)
  30. # print("json_data",json_data)
  31. template_result = requests.post(CUSTOMER_TEMPLATE_URL+generate_templace,data=json_data,headers=headers)
  32. resultJson = template_result.json()
  33. code = resultJson.get("code")
  34. msg = resultJson.get("msg")
  35. images = resultJson.get("images",[])
  36. if code != 0:
  37. raise UnicornException(f"详情页生成失败,请检查模板数据是否正确:{msg}")
  38. concat_images_array = []
  39. for image in images:
  40. canvasIndex = image.get("canvasIndex")
  41. dataUrl = image.get("dataUrl")
  42. save_name = f"{save_path}/切片图-{template_name}/{goods_no}({int(canvasIndex)+1}).png"
  43. match dataUrl:
  44. case "model":
  45. # 复制模特图进行拼接
  46. if model_image:
  47. model_copy_res = self.copyImage(model_image,save_name)
  48. if model_copy_res:
  49. model_image_pil = Image.open(model_image)
  50. concat_images_array.append(model_image_pil)
  51. case "scene":
  52. # 复制场景图进行拼接
  53. if scene_image:
  54. scene_copy_res = self.copyImage(scene_image,save_name)
  55. if scene_copy_res:
  56. scene_image_pil = Image.open(scene_image)
  57. concat_images_array.append(scene_image_pil)
  58. case _:
  59. pillowImage = self.save_base64_image(dataUrl,save_name)
  60. concat_images_array.append(pillowImage)
  61. long_image = self.concat_images_vertically(concat_images_array)
  62. save_name = f"{save_path}/详情页-{template_name}.jpg"
  63. long_image.save(save_name,format="JPEG")
  64. print("模板生成成功")
  65. def concat_images_vertically(self,image_array, custom_width=None):
  66. """
  67. 按照顺序将图片数组拼接成长图,并统一图片宽度
  68. 参数:
  69. - image_array: list,存放 Pillow 图片对象的数组
  70. - custom_width: int,可选参数,指定统一的图片宽度(默认为第一张图的宽度)
  71. 返回:
  72. - concatenated_image: PIL.Image,拼接后的长图对象
  73. """
  74. if not image_array:
  75. raise ValueError("图片数组为空,无法拼接")
  76. # 1. 确定统一宽度
  77. base_width = custom_width or image_array[0].width
  78. # 2. 计算总高度和调整图片尺寸
  79. total_height = 0
  80. resized_images = []
  81. for img in image_array:
  82. # 调整图片宽度并保持宽高比
  83. width_ratio = base_width / img.width
  84. new_height = int(img.height * width_ratio)
  85. resized_img = img.resize((base_width, new_height), Image.Resampling.LANCZOS)
  86. resized_images.append(resized_img)
  87. total_height += new_height
  88. # 3. 创建空白画布
  89. concatenated_image = Image.new("RGB", (base_width, total_height))
  90. # 4. 按顺序拼接图片
  91. y_offset = 0
  92. for resized_img in resized_images:
  93. concatenated_image.paste(resized_img, (0, y_offset))
  94. y_offset += resized_img.height
  95. return concatenated_image
  96. def __handler_config_data(self,config_data):
  97. '''
  98. 处理配置数据,返回一个新的数据对象
  99. '''
  100. # 深拷贝原始数据,确保不改变原数据对象
  101. new_config_data = copy.deepcopy(config_data)
  102. model_image = None
  103. scene_image = None
  104. # 如果输入是字典,则将其转换为目标结构
  105. if isinstance(new_config_data, dict):
  106. # result = []
  107. for key, item in new_config_data.items():
  108. # 提取需要添加的数据
  109. additional_data = {k: v for k, v in item.items() if k not in ["款号", "货号资料"]}
  110. # 遍历货号资料,将额外数据添加到每个货号对象中
  111. for product in item.get("货号资料", []):
  112. product.update(additional_data)
  113. # 处理 pics 字段中的 xx-抠图 转换为 Base64 并新增字段
  114. pics = product.get("pics", {})
  115. if not model_image:
  116. model_image = product.get("模特图", None)
  117. if not scene_image:
  118. scene_image = product.get("场景图", None)
  119. new_pics = {}
  120. for pic_key, pic_path in pics.items():
  121. if "-抠图" in pic_key:
  122. # 读取图片并转换为 Base64
  123. try:
  124. # base64_data = self.crop_image_and_convert_to_base64(pic_path)
  125. # 新增字段(去除 -抠图)
  126. new_key = pic_key.replace("-抠图", "")
  127. new_pics[new_key] = pic_path
  128. except Exception as e:
  129. print(f"读取图片失败: {pic_path}, 错误: {e}")
  130. else:
  131. # 非 -抠图 字段保持不变
  132. new_pics[pic_key] = pic_path
  133. # 更新 pics 字段
  134. product["pics"] = new_pics
  135. # 构建目标结构
  136. # result.append({key: item})
  137. # return result
  138. return new_config_data,model_image,scene_image
  139. def save_base64_image(self,base64_data, output_path):
  140. """
  141. 将 Base64 编码的图像保存到本地文件
  142. 参数:
  143. - base64_data: str,Base64 编码的图像数据(不包含前缀如 "data:image/png;base64,")
  144. - output_path: str,保存图像的本地路径
  145. """
  146. if "data:image/jpeg;base64," in base64_data:
  147. base64_data = base64_data.split(",")[1]
  148. try:
  149. # 1. 解码 Base64 数据
  150. image_data = base64.b64decode(base64_data)
  151. # 2. 加载图像数据
  152. image = Image.open(BytesIO(image_data))
  153. # 3. 检查路径是否存在,如果不存在则创建
  154. directory = os.path.dirname(output_path)
  155. if directory and not os.path.exists(directory):
  156. os.makedirs(directory)
  157. print(f"目录已创建: {directory}")
  158. # 4. 保存图像到本地
  159. image.save(output_path)
  160. print(f"图像已成功保存到 {output_path}")
  161. return image
  162. except Exception as e:
  163. print(f"保存图像失败: {e}")
  164. print(f"Base64 数据前 100 字符: {base64_data[:100]}")
  165. return None
  166. def crop_image_and_convert_to_base64(self,pic_path):
  167. """
  168. 使用 Pillow 裁剪图片并生成带有前缀的 Base64 图片
  169. 参数:
  170. - pic_path: str,图片文件路径
  171. 返回:
  172. - base64_data_with_prefix: str,带有前缀的 Base64 编码图片数据
  173. """
  174. try:
  175. # 1. 加载图片
  176. with Image.open(pic_path) as image:
  177. # 2. 获取图片的非透明区域边界框 (bounding box)
  178. bbox = image.getbbox()
  179. if not bbox:
  180. raise ValueError("图片可能是完全透明的,无法获取边界框")
  181. # 3. 裁剪图片
  182. cropped_image = image.crop(bbox)
  183. # 4. 将裁剪后的图片转换为 Base64
  184. buffered = BytesIO()
  185. cropped_image.save(buffered, format="PNG") # 保存为 PNG 格式
  186. base64_data = base64.b64encode(buffered.getvalue()).decode("utf-8")
  187. # 5. 添加 Base64 前缀
  188. base64_data_with_prefix = f"data:image/png;base64,{base64_data}"
  189. return base64_data_with_prefix
  190. except Exception as e:
  191. print(f"处理图片失败: {pic_path}, 错误: {e}")
  192. return None
  193. def copyImage(self,src_path,limit_path):
  194. try:
  195. shutil.copy(src_path, limit_path)
  196. return True
  197. except Exception as e:
  198. logger.info(f"copyImage 复制模特图/场景图出错:{str(e)}",src_path,limit_path)
  199. return False