customer_template_service.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  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. from natsort import ns, natsorted
  11. from service.base import get_images, check_path, get_image_mask
  12. '''前端生图接口'''
  13. #
  14. generate_templace = "/generate"
  15. class CustomerTemplateService:
  16. def __init__(self):
  17. pass
  18. def parse_template_json(self,template_json):
  19. """
  20. 解析 template_json 数据(如果它是 URL)
  21. 参数:
  22. - template_json: str,模板数据(可能是 URL 或 JSON 字符串)
  23. 返回:
  24. - dict,解析后的 JSON 数据
  25. """
  26. try:
  27. # 检查是否为 URL
  28. if isinstance(template_json, str) and (template_json.startswith("http://") or template_json.startswith("https://")):
  29. # 发送 GET 请求获取数据
  30. response = requests.get(template_json)
  31. response.raise_for_status() # 检查请求是否成功
  32. # 解析 JSON 数据
  33. parsed_data = response.json()
  34. return parsed_data
  35. else:
  36. # 如果不是 URL,直接解析为 JSON
  37. return json.loads(template_json)
  38. except requests.exceptions.RequestException as e:
  39. print(f"网络请求失败: {e}")
  40. return None
  41. except json.JSONDecodeError as e:
  42. print(f"JSON 解析失败: {e}")
  43. return None
  44. def generateTemplate(self,config_data,template_json,template_name,save_path):
  45. '''
  46. 参数:
  47. config_data: 配置数据
  48. template_json: 模板数据
  49. template_name: 模板名称
  50. save_path: 保存路径
  51. '''
  52. print("开始生成模板")
  53. template_json_data = self.parse_template_json(template_json)
  54. # print("config_data",config_data)
  55. handler_config_data,model_image,scene_image = self.__handler_config_data(config_data,save_path)
  56. self.goods_no_value = handler_config_data
  57. headers = {"Content-Type": "application/json"}
  58. json_data = {"goodsList":[{self.goods_no:handler_config_data}],"canvasList":template_json_data}
  59. json_data = json.dumps(json_data,ensure_ascii=False)
  60. # print("json_data",json_data)
  61. template_result = requests.post(CUSTOMER_TEMPLATE_URL+generate_templace,data=json_data,headers=headers)
  62. resultJson = template_result.json()
  63. code = resultJson.get("code")
  64. msg = resultJson.get("msg")
  65. images = resultJson.get("images",[])
  66. if code != 0:
  67. raise UnicornException(f"详情页生成失败,请检查模板数据是否正确:{msg}")
  68. concat_images_array = []
  69. for image in images:
  70. canvasIndex = image.get("canvasIndex")
  71. dataUrl = image.get("dataUrl")
  72. save_name = f"{save_path}/切片图-{template_name}/{self.goods_no}({int(canvasIndex)+1}).png"
  73. match dataUrl:
  74. case "model":
  75. # 复制模特图进行拼接
  76. if model_image:
  77. model_copy_res = self.copyImage(model_image,save_name)
  78. if model_copy_res:
  79. model_image_pil = Image.open(model_image)
  80. concat_images_array.append(model_image_pil)
  81. case "scene":
  82. # 复制场景图进行拼接
  83. if scene_image:
  84. scene_copy_res = self.copyImage(scene_image,save_name)
  85. if scene_copy_res:
  86. scene_image_pil = Image.open(scene_image)
  87. concat_images_array.append(scene_image_pil)
  88. case _:
  89. pillowImage = self.save_base64_image(dataUrl,save_name)
  90. concat_images_array.append(pillowImage)
  91. long_image = self.concat_images_vertically(concat_images_array)
  92. if long_image:
  93. save_name = f"{save_path}/详情页-{template_name}.jpg"
  94. long_image.save(save_name,format="JPEG")
  95. self.move_other_pic(move_main_pic=True,save_path=save_path)
  96. print("模板生成成功")
  97. try:
  98. # 删除 800x800 目录及其内容
  99. directory_to_remove = os.path.join(save_path, "800x800")
  100. if os.path.exists(directory_to_remove):
  101. shutil.rmtree(directory_to_remove)
  102. print(f"目录 {directory_to_remove} 已成功删除")
  103. except Exception as e:
  104. print(f"删除目录失败: {e}")
  105. def create_folder(self, path):
  106. # 创建目录
  107. if path and not os.path.exists(path):
  108. print(f"创建目录 详情页=================>>>>:{path}")
  109. os.makedirs(path)
  110. def move_other_pic(self, move_main_pic=True,save_path=""):
  111. sorted_list_800 = []
  112. for goods_art_no_dict in self.goods_no_value["货号资料"]:
  113. if "800x800" not in goods_art_no_dict:
  114. continue
  115. if not goods_art_no_dict["800x800"]:
  116. continue
  117. goods_art_no = ""
  118. if "编号" in goods_art_no_dict:
  119. if goods_art_no_dict["编号"]:
  120. goods_art_no = goods_art_no_dict["编号"]
  121. if not goods_art_no:
  122. goods_art_no = goods_art_no_dict["货号"]
  123. sorted_list_800 = natsorted(goods_art_no_dict["800x800"], key=lambda x: x.split("(")[1].split(")")[0])
  124. self.create_folder(save_path)
  125. # 放入一张主图
  126. old_pic_path_1 = sorted_list_800[0]
  127. shutil.copy(
  128. old_pic_path_1,
  129. "{}/颜色图{}{}".format(
  130. save_path, goods_art_no, os.path.splitext(old_pic_path_1)[1]
  131. ),
  132. )
  133. # 把其他主图放入作为款号图=====================
  134. if move_main_pic:
  135. for idx,pic_path in enumerate(sorted_list_800):
  136. index = idx + 1
  137. try:
  138. split_size = pic_path.split("_")[1].split(".")[0]
  139. except:
  140. split_size = ""
  141. suffix_name = "_"+split_size if split_size else ""
  142. print("pic_path=========>",split_size)
  143. e = os.path.splitext(pic_path)[1]
  144. shutil.copy(
  145. pic_path,
  146. "{out_put_dir}/主图{goods_no}({goods_no_main_pic_number}){suffix_name}{e}".format(
  147. out_put_dir=save_path,
  148. goods_no=goods_art_no,
  149. goods_no_main_pic_number=str(
  150. index
  151. ),
  152. e=e,
  153. suffix_name=suffix_name
  154. ),
  155. )
  156. def concat_images_vertically(self,image_array, custom_width=None):
  157. """
  158. 按照顺序将图片数组拼接成长图,并统一图片宽度
  159. 参数:
  160. - image_array: list,存放 Pillow 图片对象的数组
  161. - custom_width: int,可选参数,指定统一的图片宽度(默认为第一张图的宽度)
  162. 返回:
  163. - concatenated_image: PIL.Image,拼接后的长图对象
  164. """
  165. if not image_array:
  166. return None
  167. # 1. 确定统一宽度
  168. base_width = custom_width or image_array[0].width
  169. # 2. 计算总高度和调整图片尺寸
  170. total_height = 0
  171. resized_images = []
  172. for img in image_array:
  173. # 调整图片宽度并保持宽高比
  174. width_ratio = base_width / img.width
  175. new_height = int(img.height * width_ratio)
  176. resized_img = img.resize((base_width, new_height), Image.Resampling.LANCZOS)
  177. resized_images.append(resized_img)
  178. total_height += new_height
  179. # 3. 创建空白画布
  180. concatenated_image = Image.new("RGB", (base_width, total_height))
  181. # 4. 按顺序拼接图片
  182. y_offset = 0
  183. for resized_img in resized_images:
  184. concatenated_image.paste(resized_img, (0, y_offset))
  185. y_offset += resized_img.height
  186. return concatenated_image
  187. def __handler_config_data(self,config_data,scp_path):
  188. '''
  189. 处理配置数据,返回一个新的数据对象
  190. '''
  191. directory = os.path.dirname(scp_path)
  192. self.create_folder(directory)
  193. # 深拷贝原始数据,确保不改变原数据对象
  194. new_config_data = copy.deepcopy(config_data)
  195. print("传入的config数据",new_config_data)
  196. model_image = None
  197. scene_image = None
  198. self.goods_no = new_config_data.get("款号")
  199. # 如果输入是字典,则将其转换为目标结构
  200. # 提取需要添加的数据,排除特定字段
  201. additional_data = {k: v for k, v in new_config_data.items() if k not in ["款号", "货号资料"]}
  202. # 遍历货号资料,将额外数据添加到每个货号对象中
  203. for product in new_config_data.get("货号资料", []):
  204. product.update(additional_data)
  205. # 处理 pics 字段中的 xx-抠图 转换为 Base64 并新增字段
  206. pics = product.get("pics", {})
  207. goods_art_no = product.get("货号",None)
  208. goods_art_lens = len(new_config_data.get("货号资料", []))
  209. concat_shuffix = "" if goods_art_lens == 1 else f"_{goods_art_no}"
  210. if not model_image:
  211. model_image = product.get("模特图", None)
  212. if model_image:
  213. self.copyImage(model_image, f"{scp_path}/模特图{concat_shuffix}.jpg")
  214. if not scene_image:
  215. scene_image = product.get("场景图", None)
  216. if scene_image:
  217. self.copyImage(scene_image, f"{scp_path}/场景图{concat_shuffix}.jpg")
  218. new_pics = {}
  219. for pic_key, pic_path in pics.items():
  220. if "-抠图" in pic_key:
  221. # 读取图片并转换为 Base64
  222. try:
  223. base64_data = self.crop_image_and_convert_to_base64(pic_path)
  224. # 新增字段(去除 -抠图)
  225. new_key = pic_key.replace("-抠图", "")
  226. new_pics[new_key] = base64_data
  227. except Exception as e:
  228. print(f"读取图片失败: {pic_path}, 错误: {e}")
  229. else:
  230. # 非 -抠图 字段保持不变
  231. new_pics[pic_key] = pic_path
  232. # 更新 pics 字段
  233. product["pics"] = new_pics
  234. # 构建目标结构
  235. # result.append({key: item})
  236. # return result
  237. return new_config_data,model_image,scene_image
  238. def save_base64_image(self,base64_data, output_path):
  239. """
  240. 将 Base64 编码的图像保存到本地文件
  241. 参数:
  242. - base64_data: str,Base64 编码的图像数据(不包含前缀如 "data:image/png;base64,")
  243. - output_path: str,保存图像的本地路径
  244. """
  245. if "data:image/jpeg;base64," in base64_data:
  246. base64_data = base64_data.split(",")[1]
  247. try:
  248. # 1. 解码 Base64 数据
  249. image_data = base64.b64decode(base64_data)
  250. # 2. 加载图像数据
  251. image = Image.open(BytesIO(image_data))
  252. # 3. 检查路径是否存在,如果不存在则创建
  253. directory = os.path.dirname(output_path)
  254. self.create_folder(directory)
  255. # 4. 保存图像到本地
  256. image.save(output_path)
  257. print(f"图像已成功保存到 {output_path}")
  258. return image
  259. except Exception as e:
  260. print(f"保存图像失败: {e}")
  261. print(f"Base64 数据前 100 字符: {base64_data[:100]}")
  262. return None
  263. def crop_image_and_convert_to_base64(self,pic_path):
  264. """
  265. 使用 Pillow 裁剪图片并生成带有前缀的 Base64 图片
  266. 参数:
  267. - pic_path: str,图片文件路径
  268. 返回:
  269. - base64_data_with_prefix: str,带有前缀的 Base64 编码图片数据
  270. """
  271. try:
  272. # 1. 加载图片
  273. with Image.open(pic_path) as image:
  274. # 2. 获取图片的非透明区域边界框 (bounding box)
  275. bbox = image.getbbox()
  276. if not bbox:
  277. raise ValueError("图片可能是完全透明的,无法获取边界框")
  278. # 3. 裁剪图片
  279. cropped_image = image.crop(bbox)
  280. # 4. 将裁剪后的图片转换为 Base64
  281. buffered = BytesIO()
  282. cropped_image.save(buffered, format="PNG") # 保存为 PNG 格式
  283. base64_data = base64.b64encode(buffered.getvalue()).decode("utf-8")
  284. # 5. 添加 Base64 前缀
  285. base64_data_with_prefix = f"data:image/png;base64,{base64_data}"
  286. return base64_data_with_prefix
  287. except Exception as e:
  288. print(f"处理图片失败: {pic_path}, 错误: {e}")
  289. return None
  290. def copyImage(self,src_path,limit_path):
  291. try:
  292. shutil.copy(src_path, limit_path)
  293. return True
  294. except Exception as e:
  295. logger.info(f"copyImage 复制模特图/场景图出错:{str(e)}",src_path,limit_path)
  296. return False