customer_template_service.py 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  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. print("模板生成成功")
  62. def concat_images_vertically(image_array, custom_width=None):
  63. """
  64. 按照顺序将图片数组拼接成长图,并统一图片宽度
  65. 参数:
  66. - image_array: list,存放 Pillow 图片对象的数组
  67. - custom_width: int,可选参数,指定统一的图片宽度(默认为第一张图的宽度)
  68. 返回:
  69. - concatenated_image: PIL.Image,拼接后的长图对象
  70. """
  71. if not image_array:
  72. raise ValueError("图片数组为空,无法拼接")
  73. # 1. 确定统一宽度
  74. base_width = custom_width or image_array[0].width
  75. # 2. 计算总高度和调整图片尺寸
  76. total_height = 0
  77. resized_images = []
  78. for img in image_array:
  79. # 调整图片宽度并保持宽高比
  80. width_ratio = base_width / img.width
  81. new_height = int(img.height * width_ratio)
  82. resized_img = img.resize((base_width, new_height), Image.ANTIALIAS)
  83. resized_images.append(resized_img)
  84. total_height += new_height
  85. # 3. 创建空白画布
  86. concatenated_image = Image.new("RGB", (base_width, total_height))
  87. # 4. 按顺序拼接图片
  88. y_offset = 0
  89. for resized_img in resized_images:
  90. concatenated_image.paste(resized_img, (0, y_offset))
  91. y_offset += resized_img.height
  92. return concatenated_image
  93. def __handler_config_data(self,config_data):
  94. '''
  95. 处理配置数据,返回一个新的数据对象
  96. '''
  97. # 深拷贝原始数据,确保不改变原数据对象
  98. new_config_data = copy.deepcopy(config_data)
  99. model_image = None
  100. scene_image = None
  101. # 如果输入是字典,则将其转换为目标结构
  102. if isinstance(new_config_data, dict):
  103. # result = []
  104. for key, item in new_config_data.items():
  105. # 提取需要添加的数据
  106. additional_data = {k: v for k, v in item.items() if k not in ["款号", "货号资料"]}
  107. # 遍历货号资料,将额外数据添加到每个货号对象中
  108. for product in item.get("货号资料", []):
  109. product.update(additional_data)
  110. # 处理 pics 字段中的 xx-抠图 转换为 Base64 并新增字段
  111. pics = product.get("pics", {})
  112. if not model_image:
  113. model_image = product.get("模特图", None)
  114. if not scene_image:
  115. scene_image = product.get("场景图", None)
  116. new_pics = {}
  117. for pic_key, pic_path in pics.items():
  118. if "-抠图" in pic_key:
  119. # 读取图片并转换为 Base64
  120. try:
  121. # base64_data = self.crop_image_and_convert_to_base64(pic_path)
  122. # 新增字段(去除 -抠图)
  123. new_key = pic_key.replace("-抠图", "")
  124. new_pics[new_key] = pic_path
  125. except Exception as e:
  126. print(f"读取图片失败: {pic_path}, 错误: {e}")
  127. else:
  128. # 非 -抠图 字段保持不变
  129. new_pics[pic_key] = pic_path
  130. # 更新 pics 字段
  131. product["pics"] = new_pics
  132. # 构建目标结构
  133. # result.append({key: item})
  134. # return result
  135. return new_config_data,model_image,scene_image
  136. def save_base64_image(self,base64_data, output_path):
  137. """
  138. 将 Base64 编码的图像保存到本地文件
  139. 参数:
  140. - base64_data: str,Base64 编码的图像数据(不包含前缀如 "data:image/png;base64,")
  141. - output_path: str,保存图像的本地路径
  142. """
  143. if "data:image/jpeg;base64," in base64_data:
  144. base64_data = base64_data.split(",")[1]
  145. try:
  146. # 1. 解码 Base64 数据
  147. image_data = base64.b64decode(base64_data)
  148. # 2. 加载图像数据
  149. image = Image.open(BytesIO(image_data))
  150. # 3. 检查路径是否存在,如果不存在则创建
  151. directory = os.path.dirname(output_path)
  152. if directory and not os.path.exists(directory):
  153. os.makedirs(directory)
  154. print(f"目录已创建: {directory}")
  155. # 4. 保存图像到本地
  156. image.save(output_path)
  157. print(f"图像已成功保存到 {output_path}")
  158. return image
  159. except Exception as e:
  160. print(f"保存图像失败: {e}")
  161. print(f"Base64 数据前 100 字符: {base64_data[:100]}")
  162. return None
  163. def crop_image_and_convert_to_base64(self,pic_path):
  164. """
  165. 使用 Pillow 裁剪图片并生成带有前缀的 Base64 图片
  166. 参数:
  167. - pic_path: str,图片文件路径
  168. 返回:
  169. - base64_data_with_prefix: str,带有前缀的 Base64 编码图片数据
  170. """
  171. try:
  172. # 1. 加载图片
  173. with Image.open(pic_path) as image:
  174. # 2. 获取图片的非透明区域边界框 (bounding box)
  175. bbox = image.getbbox()
  176. if not bbox:
  177. raise ValueError("图片可能是完全透明的,无法获取边界框")
  178. # 3. 裁剪图片
  179. cropped_image = image.crop(bbox)
  180. # 4. 将裁剪后的图片转换为 Base64
  181. buffered = BytesIO()
  182. cropped_image.save(buffered, format="PNG") # 保存为 PNG 格式
  183. base64_data = base64.b64encode(buffered.getvalue()).decode("utf-8")
  184. # 5. 添加 Base64 前缀
  185. base64_data_with_prefix = f"data:image/png;base64,{base64_data}"
  186. return base64_data_with_prefix
  187. except Exception as e:
  188. print(f"处理图片失败: {pic_path}, 错误: {e}")
  189. return None
  190. def copyImage(self,src_path,limit_path):
  191. try:
  192. shutil.copy(src_path, limit_path)
  193. return True
  194. except Exception as e:
  195. logger.info(f"copyImage 复制模特图/场景图出错:{str(e)}",src_path,limit_path)
  196. return False