customer_template_service.py 17 KB

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