From 2d21647f10bf31dea33a08c7791df46d7e123795 Mon Sep 17 00:00:00 2001 From: jinye_huang Date: Wed, 10 Dec 2025 16:28:25 +0800 Subject: [PATCH] =?UTF-8?q?fix(poster=5Fv2):=20=E7=A1=AE=E4=BF=9D=20Fabric?= =?UTF-8?q?=20JSON=20=E5=92=8C=20PNG=20=E4=BD=8D=E7=BD=AE=E4=B8=80?= =?UTF-8?q?=E8=87=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在所有布局类中添加 _add_object() 调用 - 渲染时同时记录元素位置到 _fabric_objects - V2 引擎直接从布局类获取 Fabric 对象 - 移除硬编码的 fallback 位置 --- .../poster_smart_v2.cpython-312.pyc | Bin 25848 -> 26556 bytes domain/aigc/engines/poster_smart_v2.py | 94 +++++++----- .../layouts/__pycache__/base.cpython-312.pyc | Bin 4069 -> 4855 bytes .../__pycache__/card_float.cpython-312.pyc | Bin 8075 -> 9297 bytes .../__pycache__/hero_bottom.cpython-312.pyc | Bin 9567 -> 12683 bytes .../overlay_bottom.cpython-312.pyc | Bin 7603 -> 9513 bytes .../overlay_center.cpython-312.pyc | Bin 5900 -> 7104 bytes .../split_vertical.cpython-312.pyc | Bin 7071 -> 8575 bytes poster_v2/layouts/base.py | 16 ++ poster_v2/layouts/card_float.py | 55 ++++++- poster_v2/layouts/hero_bottom.py | 142 +++++++++++++++--- poster_v2/layouts/overlay_bottom.py | 83 +++++++++- poster_v2/layouts/overlay_center.py | 54 ++++++- poster_v2/layouts/split_vertical.py | 66 +++++++- 14 files changed, 448 insertions(+), 62 deletions(-) diff --git a/domain/aigc/engines/__pycache__/poster_smart_v2.cpython-312.pyc b/domain/aigc/engines/__pycache__/poster_smart_v2.cpython-312.pyc index 389c79f653a50047a76cb84b6906d1b625ca354c..9f07f167543e1b0d091837f587ae4fdb09f3476a 100644 GIT binary patch delta 5779 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 diff --git a/domain/aigc/engines/poster_smart_v2.py b/domain/aigc/engines/poster_smart_v2.py index 35ba517..1895bf2 100644 --- a/domain/aigc/engines/poster_smart_v2.py +++ b/domain/aigc/engines/poster_smart_v2.py @@ -110,11 +110,21 @@ class PosterSmartEngineV2(BaseEngine): theme = THEMES[theme_name] - # 4. 生成预览 PNG (无底图) - preview_base64 = self._generate_preview(content, layout, theme) + # 4. 生成预览 PNG 和 Fabric 对象 + preview_base64, fabric_objects = self._generate_preview(content, layout, theme, image_url) - # 5. 生成 Fabric JSON - fabric_json = self._generate_fabric_json(content, layout, theme, image_url) + # 5. 构建 Fabric JSON + fabric_json = { + "version": "5.3.0", + "canvas": { + "width": self.CANVAS_WIDTH, + "height": self.CANVAS_HEIGHT, + "backgroundColor": theme.secondary, + }, + "layout": layout, + "theme": theme.name, + "objects": fabric_objects if fabric_objects else self._generate_fallback_objects(content, layout, theme, image_url), + } return EngineResult( success=True, @@ -229,10 +239,12 @@ class PosterSmartEngineV2(BaseEngine): "suggested_theme": self.CATEGORY_THEME_MAP.get(category, "sunset"), } - def _generate_preview(self, content: dict, layout: str, theme: Theme) -> str: - """生成预览 PNG (无底图)""" - factory = self._get_poster_factory() + def _generate_preview(self, content: dict, layout: str, theme: Theme, image_url: str = "") -> tuple: + """生成预览 PNG (无底图) 并返回 Fabric 对象 + Returns: + (preview_base64, fabric_objects) + """ # 构建 PosterContent poster_content = PosterContent( title=content.get("title", ""), @@ -247,38 +259,56 @@ class PosterSmartEngineV2(BaseEngine): image=None, # 无底图 ) - # 生成海报 - poster_image = factory.generate_from_content(poster_content, layout=layout, theme=theme.name) + # 获取布局实例 + from poster_v2.layouts import ( + HeroBottomLayout, OverlayCenterLayout, OverlayBottomLayout, + SplitVerticalLayout, CardFloatLayout + ) + + layout_map = { + "hero_bottom": HeroBottomLayout, + "overlay_center": OverlayCenterLayout, + "overlay_bottom": OverlayBottomLayout, + "split_vertical": SplitVerticalLayout, + "card_float": CardFloatLayout, + } + + layout_class = layout_map.get(layout, HeroBottomLayout) + layout_instance = layout_class() + + # 生成海报 (同时记录 Fabric 对象) + # 所有布局都支持 image_url 参数 + try: + poster_image = layout_instance.generate(poster_content, theme, image_url=image_url) + except TypeError: + # 兼容旧版布局 + poster_image = layout_instance.generate(poster_content, theme) + + # 获取 Fabric 对象 + fabric_objects = layout_instance.get_fabric_objects() # 转 Base64 buffer = io.BytesIO() poster_image.convert("RGB").save(buffer, format="PNG") - return base64.b64encode(buffer.getvalue()).decode("utf-8") - - def _generate_fabric_json(self, content: dict, layout: str, theme: Theme, image_url: str = "") -> dict: - """生成 Fabric.js JSON""" - objects = [] + preview_base64 = base64.b64encode(buffer.getvalue()).decode("utf-8") - # 通用配置 + return preview_base64, fabric_objects + + def _generate_fallback_objects(self, content: dict, layout: str, theme: Theme, image_url: str = "") -> list: + """生成后备 Fabric.js 对象 (当布局类没有提供时)""" margin = 48 content_width = self.CANVAS_WIDTH - margin * 2 - # 1. 背景图片占位 - objects.append({ + objects = [{ "id": "background_image", "type": "image", "src": image_url or "", - "left": 0, - "top": 0, - "width": self.CANVAS_WIDTH, - "height": self.CANVAS_HEIGHT, - "scaleX": 1, - "scaleY": 1, + "left": 0, "top": 0, + "width": self.CANVAS_WIDTH, "height": self.CANVAS_HEIGHT, "selectable": True, - "evented": True, - }) + }] - # 2. 根据布局生成不同的结构 + # 简化的后备对象 if layout == "hero_bottom": objects.extend(self._fabric_hero_bottom(content, theme, margin, content_width)) elif layout == "overlay_center": @@ -290,17 +320,7 @@ class PosterSmartEngineV2(BaseEngine): elif layout == "card_float": objects.extend(self._fabric_card_float(content, theme, margin, content_width)) - return { - "version": "5.3.0", - "canvas": { - "width": self.CANVAS_WIDTH, - "height": self.CANVAS_HEIGHT, - "backgroundColor": theme.secondary, - }, - "layout": layout, - "theme": theme.name, - "objects": objects, - } + return objects def _fabric_hero_bottom(self, content: dict, theme: Theme, margin: int, content_width: int) -> List[dict]: """hero_bottom 布局的 Fabric 对象""" diff --git a/poster_v2/layouts/__pycache__/base.cpython-312.pyc b/poster_v2/layouts/__pycache__/base.cpython-312.pyc index 840fb3982be19d25ec5c8b73ca86d6475f03bf55..a0ae6e4e455691290abb39a2954b906d1855e7e5 100644 GIT binary patch delta 1688 zcmZ`(ZD?Cn7``XDxjoIzN1HZj`hC}KGbkLRc^@aa3Pxlj1bo6ePgCBCftMMj`+g896lwTmJ-g{An|Ew zfy8xHv4o*M4}v;zswOlDR195iQ| zw;Iw{OC~^(=aDy0e-W|z44XZg2KL+22#~2cSXq+ZjD)LB~z7GOc zuYXrOTPV|y>n?}5tt6+W*rWkV6u_O1VK=mj3^kE7jBG{4Ok}$Ta8&I3A;F9QfB@=S zp>0dFZH0C((eA?0TQpV{qi|+r_~ifh{V7O(ygK?=$ngBqT9&A_%b}t3HSAwyn zU~DQoS&oyyVc@XUf3wJKWf8k)Uqryj{;z7+W^#u$VK+r@#T?jcv4#3*nJT~zc z-9z3GO|^Sv4ea7Djfl5uf76^uWMV6+^mv+^Ta3`yA||~pBI)fRZ;Dy(Gei@=c+Yk? zNzZ|wA3-<@V9Ed>1eHvv10*R5YAb|)uio%iHC6$mel%IL*~?hEJ5&Z2~@Sv;QTyD|zXji+X4&A~kBc6Y!T2sLKuA1~`6C_IkQ5 zzFOEf?=B2}n%IzFc|Yp%bd{*fqhGW(B>3RGx9~j9drMT5{C@>f(5rLaO&q7kheIt=r22&+wbUMFF!5d-Tag2TkTGr;fmeq{w4 zO=;&>bKdH?p}{oLHI3pvY#!`^O(NfP;Jemq%B!-3Er<~N?{W!8o6>w7$NnA;$N$l{ zdmgLcTmo!tl7KWxczd!dbE+;ohy3u=f=gx>>Xn*T+hO~m7wnCgNAFo%{Gir_ZB%l? z?2}YvXXGX27^HWvu}!&@d1Q3>TYPn~vg(ml&zHU1WQF(&C0>oLvX}ZW+tl9n9{~}~ zats4p5s=Mh=@7fCAGODz>ZpK*(CQa-lV;d+{YT)xwh9moffrM}NmBq?(g}PmqX|srC88Ih8`^0>h4zv^8{v1aE z;7S0<%L8Av#?cLS-x>t@y0sT|5al;q#3eAVMo*$Swh_%|Gti+afP);7=6Ll7P9MF* zzDB1}nq80eubtpShvPKIS&m77uT^QKN~l-&Mc1SBG9Lv0B`_c2W$ZU>IJh?CXbSC+ zDr>Ezkjyq_TZ&-C8pV%*>9(ZGqw8~9`Dfx*VdGXufU;!^OpNbafg?97IVoCXo%qv% h-c7!CVWom=%j6=x$zI#zi6TN*(Bs56AvjQO@Dn*1wTu7& diff --git a/poster_v2/layouts/__pycache__/card_float.cpython-312.pyc b/poster_v2/layouts/__pycache__/card_float.cpython-312.pyc index 36c608aca20cfed013d825186675c5b472971d98..6840e57c889778e95c7df818773753a5cbff0e08 100644 GIT binary patch delta 3471 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 Q!Ln0~eNg?EfGs!sFJdhRL;wH) delta 2961 zcmaJ@U2GIp6rS0g{hi%y_iy&M+wS&v+on(+6oeokNfjf6pbrqFti8LiW1)3Aq|n^S z_QAvlctEZRi3B6Yx0VD`O=A3iC z^K;M4@2_5cId(r94GZvlrGHN|QurdaM-2OM55Gr??6`PjB3O@-P(4P%^|)HGo^-Aa zKZ(3Aol0DZR%07Ki*HETId8}F5b(HvBt_Uhe}0OJ)U)QX6bvsFRGaTG5vX?sFcEPj z%vSvc_E#X?BAykWAfVXrff0Ir>IZH`OJw+|sPdq;s%R&=%hl*sJ>0F?BDIGz?8QK| z1?TqK;trxGoQ;}tIG46V4bXs^apew2yd9_oHS5S-supaX-4Do9)JMGy4r~=(q-P!qh?$)Y`C1aR0X%M?Ubkq&>++gyFACJ7jy>zk zh=6>&i#!VQ$u9DkzQd7b)twIOQh>6H-3u4IJ^Bt7k^9+u;gV=^C* zg|@DD`(`vwtKZ4<2I!-P17v=b65-#!Ifr7)G7^$ofByE zGm!*86z}@C6hu~M+D_UGO_QF4?V^{v@Yt|Yc^X}hcN8>B5-V^qjVz?>w|L)c90zo# zs2*v%YcsTmMrej4*Hdc&OL!XQijg-8fW1I^@F>e071?SmC25B&x1qLM5T{$c5KaWb zNn1}vO1`}C?qqBJQmOSJvAuq(|OcGWux=RA7`F1Dr$UGL&(E_VGyyRz?MqQzrTLeh}M& zJ&BFBWLlwvR@!KV@Jhj+N7#_;l5?Qz1$S7`?j^5~BCq%$6tADg?F{$vhUqBywM9wL zPg=nMSUZ{)DWS0)?GOMSvH)97otmnroGiFhdkK1_i~O+g_6P(&ybWCFb|^-cM95?+a78_a1Jgu`7IwcIwBZr0IpsnZoy*@0+m2ogddC1^ou`<4so~?k*5$!f920H%5%n=Vf3!A2il^OWCiz3VV=h0b#stgLh8Hs`UDvB;2Ea^m4?g1$FNjA+B6VTYBcBPwK$5QONEE{`g|CHMZ?!o)dLC(7j_??qZad^&tJ7K)&CrYKTW%8lJR8VV1E#Nep?0p$j%Fs=2bm`Wrli*|>t>v2 zwdL7mt+u#WC$n7S)0_Hyo!!mOOqsq#tvOdWLtJaVd9iLrxMX(eQlrtlT%FX0@i6=X z($Z`yH@)BVFP*QEg=I5n(b>yw?usd)A2Wo<%#xb+uIaDM&DAgJW&pfisA*R=uIId> zgs*6JCqKyUB9vlr-omW8q{Afy-mm~PZ@qyXgAp(m6E8l$CWpCZ5|d6dVI>K z(MHIK4+<+ Q$@r2-RIaQ42}rrx|D0Iy6#xJL diff --git a/poster_v2/layouts/__pycache__/hero_bottom.cpython-312.pyc b/poster_v2/layouts/__pycache__/hero_bottom.cpython-312.pyc index 5250b051cce58350973c70fa17807279c166183a..a115515fb2c5e61268728299b0e6854f50266b4e 100644 GIT binary patch literal 12683 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 TGd~v(2zmK)Gk?ZV&eHRLaHx;! delta 3154 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 IMMg#Uf1Zu0V*mgE delta 2573 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 delta 1572 zcma)6U5Fc16rMXXcV?2ANivztBs)pWb}6gb7HvyyUD*a&l=a2xy7+?>bm=5*X4A%w zv$S3Bh{b|+>4SO%g{3G3A7mpaiwJ_yKJ`Hqs!s-#KCSFqp$bJ&ymvCwWL+P;59gkH z?sv}l&bdF=4qkpR^R2F@IXb>8Pq!}^Uu2#~X^E}rxbp&Z(LNP7Wmjq{uH00?kPBK% za#I(@7q#>0y1M=Y`bC&HE({zZnWB=d?{s92d#myRM>r3`9B)ESN@IZ_kB6F6wD=L9 zSoJXcj2+&SDcq_l+W|pDmDxQFR;4l)iRfkFrfkCZQrZ@1s@yiO_f%_)2rl0f*5aI@ z5Lht`ayF6;HJpmyW%pfB2--*YH&fZdG8^%i7|!kVcNsm z5JuD%PqY&pk*^|GdKY~rt%-xgh$E^iFY7E+>}kPc8M8#=V~%4XLVcvB{VcSAyaQ1( zow|&(wr7=IDhU~8f-3x!s-XyDWs(YxF}~R_mk=ywhXg}om;>h0mWqiHkxnvY=qb#w zOw(ys?P+WDPO-u0lbowBQ#~#Md5vgmi|~)foEnU`mD<87ADRg#BR|5aK(Kd+;Zuc} zOXOWo(MC#1BYT;o?ud)&W`|t<^IYox_gv^>Q475PBb?D}$-hA9x1CB5z3E zsm-xy>zrk#$j*MEzA|&F>n?Sg4gYm`v%K3_M9BVd>UWOK5$qo$ CX*dZ0 diff --git a/poster_v2/layouts/__pycache__/split_vertical.cpython-312.pyc b/poster_v2/layouts/__pycache__/split_vertical.cpython-312.pyc index 162718005f5759f7df2d3c583674f46697e35bee..6311d4a860c6fcfb4c6e1a751ad0f5ca5e907a68 100644 GIT binary patch delta 3696 zcmb7HTWk~A8J_WN#`nv39A74m<6MX#39t(bVMDks-L@owrO+*9F^>u$ABxmUOQcGfjw-dQec~;sS*dUBe~#_Mgz``; z`RAN7|M~y#`~UBpICuB_=dkC`ZnuMj&(+?(+;3VRcnX{LOD<{;-SB3~WNSUgriv*u-*i(It2EgGK>kq4G0 zKExtMY8XYUsEPR)jh}p3{IXKdM@!>b1S0fP5$g&3BC^z18Rb?A$7@A4&5TUQq*2;F zx)u|Vcw4M9$p}r>01z|L zWIUZq&7if4q)A#}21dwEZ6$~t6}esjr<1LPpdJ0>Q$cP3JwSL-=KIOK7!1~{d5(&v zsS<|m2U*{a(1yNl(*l|dzfChh8M&y{WaOsLQ48{nahl_O9(k|xzqZ};E!NZuow4QL zp!?+jK&$TYvppsTXI_-I1nx-<{xS;AfT{t$T!}Q4;~0%1FchOE@{v6h*Ib%i3nKqL zc~OAK@L|xyl_)@GaxjNb6Lobk>_J%Wri!qJQ3RSBw1&3N;Y;G9ywQ=%}fCSWbdtLa9cMK^T zyvaXYp%DInqDSpiJ&eO`El^S5Ra2w`bw;_P+&kZcMm0Au8=y>fHf#Rp#1scHDO#L{ zuO`u?@uN@^6VFY&^McG2$XfTV%WMmij0JVo3Lkr!%)YF=ATvNytE`jlY7`sfL8*1x z%C#R@sHnGAn@DEeP5xO{gKg_IxD2bIPSkPq2@*x&|zO zSRo)ySCv$aQV#>yyGZYK_-ve8MuXI zi8gclI*}6~pfmI|^wgU<09+D`%MRvM=d!Dz!R!={)z86BEde@v<&IgS=bwuA)tY!; zJd~{vxwY|ZT{{RU`TW~ezK!-LJnn<~vD;hfp;#)(1`O3p>uoAEvQ_j5ZI$+VwfV2p z)!h_d5~Kz@p=Q#?xQ5b2Q6lrAwVHLK)^^wjI{+1pBl$Gvb9YniS`l`k-E|)`>>xiD zLLwYKxK#4zXh|gjz4qMk;`8Up9Z$r$r_!Ny;6nXU+#k@|*r}*WBB`~rS*ixSV_ms> zUr=rhN=F|+=>WPfjRU3GOEDeze=di8x9ts=qV*J)dYwq#?jgfBlF5#b;o{T;O0(}K zhLt_Ok`fJ54jGp6^rUK-OE_y-3hHRtFqeyyhV^m|l`rBZD7WD#sRcD#&WsdPa=?3V zb_XnCYEmY{GFqG{<6f9N?WQZd;VR@N)O2Ywlg&+x8P;;HTu=?6tX?UP6tB#5oq*M2 znTfH)sUzKq!CV#>OU2Q0;%k{V)m(R?l$j{?lvJD>H7p~=0!le?0>a`}Fn|<-;I>)1 zK>yt14!SB*1gCN{s^OrcW0_02!gO`~RW&zuv5F3Ng74E;s0mZ^U1~%yhFF>!VIXI$ zcx)uoW$@iI*6tm>uccCsjpTR__~JHdqU*WwUOMlihE@P~lHYlE@x5f#+cs;R#JQ}> zw!I_{3$qbn-$oc18Qjw=^x4?Ely4wHt>N0VhddojvpF2a(4KX;Y?=`s%<3mzP}u1WDGyTnakJ6Yn)(rt_3(>d~WmG@%D0H-@hd)5x-uMt0hrt6|a}`q2d!=k~9-lx3HqhZ1_Y zRS&nUgu9l*T`*qg&?B9?l2}oCmz7>U{-z#nnX@mn&bw}YYvJU=^pgKq+QP)rp{3ko z{E9=#f8lL@wtKf{`-APCm;0R?|sBSN`O{TZ0yEH#K4m6}m%#;BqH zEn)au!Wrfq$R)}~r7~s`$3G*-qH+EbQKLutpNWUe*3g`Lm7~x5YW4ZpTzx({eRk#a zh2_&1*4S%?1;ck;0OZ9#Cx43;qjzX3iV@QHz+LjKq3;W*w4F Icrvg50-aQI&j0`b delta 3002 zcmb7GTZ|J`7@lcoI@5N0pI)}@_O{EVun-}r>$)zmVl+mVMU7FixRyOFc7WZ@P+ZuY zNdYAyYz%N>LgL*RA6$$Wo{Wj&6EVE#qx0m0QTN4=fY17$GndZVqOnQO`TukN+xK5i z``5$=4~B1rLIDmwd&>_sU(9?Np5OyM%ERXlBK!IA%~CyptQu--niF>4+UQr z_D4?kRYTXm=Rf5$vtHLdHBR|EZw1NIo@@p2OCFBXy_i$oVwMK>r~ER{6^j9DIZqhi2t1?cY(AC(AHM9(JTF(Hc3<}VY zOgCl<+UnyytgsX`JSgr4yC&9zWrSCs=+@UB0cXj>kZWz^KhU%-R{*DnA>0Q6$dGM? zW*cxNyKwb`tA3Pl;o{XNdxZ1}4GE=acd@|U#W6jL`F1a1lt!5>w}(6M8q_1_LNMBG z80Y@cU3uuPEY8z_%&>6$FjPwO@Z8)1X!@}=HppJT*uF;_W+7zTlHG>=)GoAP)~Zar zR~xSJz!;qE>Pm>)N^VK^Zc)z3X?FRnKxZKD6#c1g7h0QTc)-c<#RJ0!MJb z{j!q#!YT58R0^)w2l1e`(SE4MahZ0F)k7s*VO-9NWzZRq4PDbAJj+!#lJl`)BE)x% ztp_6-U8){KXw1*Gv>{+thv^A#^c`u=)n|?^)MlG=hgP@Kn+#pf&_yQGi5>h9xs(`; zaH}V%#;_qSHc{&pGO0{UhS)q>J5(oMCj-^1v`M-w_OOI~5 zE${Y-sSiW9Il5n~oBIv%X7Ib list: + """获取 Fabric.js 对象列表""" + return self._fabric_objects.copy() @abstractmethod def generate(self, content: PosterContent, theme: Theme) -> Image.Image: diff --git a/poster_v2/layouts/card_float.py b/poster_v2/layouts/card_float.py index 0a1d6c8..80adc09 100644 --- a/poster_v2/layouts/card_float.py +++ b/poster_v2/layouts/card_float.py @@ -64,8 +64,10 @@ class CardFloatLayout(BaseLayout): # 限制范围 return max(500, min(height, 800)) - def generate(self, content: PosterContent, theme: Theme) -> Image.Image: + def generate(self, content: PosterContent, theme: Theme, image_url: str = "") -> Image.Image: """生成海报""" + self._reset_objects() + # 计算卡片高度 card_height = self.calculate_content_height(content, theme) card_y = self.height - card_height - 100 @@ -79,6 +81,16 @@ class CardFloatLayout(BaseLayout): img = content.image.copy().resize(self.size, Image.LANCZOS) canvas = img.convert("RGBA") + # 背景图片 Fabric 对象 + self._add_object({ + "id": "background_image", + "type": "image", + "src": image_url, + "left": 0, "top": 0, + "width": self.width, "height": self.height, + "selectable": True, + }) + # 卡片阴影 shadow = self.effect.create_shadow( (card_width, card_height), @@ -95,6 +107,18 @@ class CardFloatLayout(BaseLayout): (self.CARD_MARGIN, card_y, self.width - self.CARD_MARGIN, card_y + card_height), radius=self.CARD_RADIUS, fill=(255, 255, 255, 250)) + # 卡片 Fabric 对象 + self._add_object({ + "id": "card_bg", + "type": "rect", + "left": self.CARD_MARGIN, "top": card_y, + "width": card_width, "height": card_height, + "rx": self.CARD_RADIUS, "ry": self.CARD_RADIUS, + "fill": "rgba(255,255,255,0.98)", + "shadow": "rgba(0,0,0,0.1) 0 8px 30px", + "selectable": False, + }) + # 颜色 text_dark = theme.text_dark_rgb accent = theme.accent_rgb @@ -128,6 +152,20 @@ class CardFloatLayout(BaseLayout): draw, (content_x, cur_y), content.title, title_font, text_dark, content_width, line_spacing=4 ) + + # 标题 Fabric 对象 + self._add_object({ + "id": "title", + "type": "textbox", + "text": content.title, + "left": content_x, "top": cur_y, + "width": content_width, + "fontSize": self.TITLE_SIZE, + "fontFamily": "PingFang SC, Microsoft YaHei, sans-serif", + "fontWeight": "bold", + "fill": theme.text_dark, + "selectable": True, + }) cur_y += title_h + 12 # === 副标题 (支持换行) === @@ -178,13 +216,26 @@ class CardFloatLayout(BaseLayout): price_w, price_h = TextRenderer.measure_text(content.price, price_font) draw.text((content_x, cur_y), content.price, font=price_font, fill=primary) + # 价格 Fabric 对象 + self._add_object({ + "id": "price", + "type": "text", + "text": content.price, + "left": content_x, "top": cur_y, + "fontSize": self.PRICE_SIZE, + "fontFamily": "PingFang SC, Microsoft YaHei, sans-serif", + "fontWeight": "bold", + "fill": theme.primary, + "selectable": True, + }) + # 后缀 (如果有) suffix = content.price_suffix or "" if suffix: draw.text((content_x + price_w + 8, cur_y + price_h - 28), suffix, font=small_font, fill=(*text_dark, 120)) - # 查看详情按钮 (更明显的颜色) + # 查看详情按钮 link_text = "查看详情" link_w, _ = TextRenderer.measure_text(link_text, subtitle_font) link_x = content_right - link_w - 28 diff --git a/poster_v2/layouts/hero_bottom.py b/poster_v2/layouts/hero_bottom.py index 04858b6..d112e37 100644 --- a/poster_v2/layouts/hero_bottom.py +++ b/poster_v2/layouts/hero_bottom.py @@ -50,8 +50,11 @@ class HeroBottomLayout(BaseLayout): return height - def generate(self, content: PosterContent, theme: Theme) -> Image.Image: + def generate(self, content: PosterContent, theme: Theme, image_url: str = "") -> Image.Image: """生成海报 (与 design_samples 保持一致)""" + # 重置 Fabric 对象列表 + self._reset_objects() + # 计算内容高度 content_height = self.calculate_content_height(content, theme) content_y = self.height - content_height - 40 @@ -73,11 +76,9 @@ class HeroBottomLayout(BaseLayout): bottom_region = img.crop((0, int(self.height * 0.7), self.width, self.height)) bottom_small = bottom_region.resize((50, 50), Image.LANCZOS) pixels = list(bottom_small.getdata()) - # 计算平均颜色 r = sum(p[0] for p in pixels) // len(pixels) g = sum(p[1] for p in pixels) // len(pixels) b = sum(p[2] for p in pixels) // len(pixels) - # 稍微加深以便文字可读 overlay_color = (max(0, r - 30), max(0, g - 30), max(0, b - 30)) else: # 无图片: 渐变色 @@ -88,23 +89,48 @@ class HeroBottomLayout(BaseLayout): ) canvas.paste(gradient, (0, 0)) - # === 底部渐变遮罩 (根据图片颜色自适应) === - # 清晰范围更大,只遮盖底部文字区域 + # 记录背景图片 Fabric 对象 + self._add_object({ + "id": "background_image", + "type": "image", + "src": image_url, + "left": 0, "top": 0, + "width": self.width, "height": self.height, + "scaleX": 1, "scaleY": 1, + "selectable": True, + }) - # 实色底(仅覆盖文字区域) - solid_start = content_y + 50 # 更低的起点,让图片更清晰 + # === 底部渐变遮罩 === + solid_start = content_y + 50 solid_bg = Image.new("RGBA", (self.width, self.height - solid_start), (*overlay_color, 200)) canvas.paste(solid_bg, (0, solid_start)) - # 渐变过渡(更大范围,更柔和) - fade_height = 350 # 更大的渐变范围 + fade_height = 350 fade = self.effect.create_gradient( (self.width, fade_height), - (*overlay_color, 0), # 顶部:完全透明 - (*overlay_color, 200) # 底部:与实色底衔接 + (*overlay_color, 0), + (*overlay_color, 200) ) canvas.paste(fade, (0, solid_start - fade_height), fade) + # 记录渐变遮罩 Fabric 对象 + self._add_object({ + "id": "gradient_overlay", + "type": "rect", + "left": 0, "top": solid_start - fade_height, + "width": self.width, "height": self.height - solid_start + fade_height, + "fill": { + "type": "linear", + "coords": {"x1": 0, "y1": 0, "x2": 0, "y2": self.height - solid_start + fade_height}, + "colorStops": [ + {"offset": 0, "color": f"rgba({overlay_color[0]},{overlay_color[1]},{overlay_color[2]},0)"}, + {"offset": 0.5, "color": f"rgba({overlay_color[0]},{overlay_color[1]},{overlay_color[2]},0.6)"}, + {"offset": 1, "color": f"rgba({overlay_color[0]},{overlay_color[1]},{overlay_color[2]},0.85)"}, + ] + }, + "selectable": False, + }) + # 重新获取 draw (因为 paste 后需要) draw = ImageDraw.Draw(canvas) @@ -127,6 +153,7 @@ class HeroBottomLayout(BaseLayout): content.title, content_width, base_size=self.TITLE_SIZE, min_size=56 ) title_w, title_h = TextRenderer.measure_text(content.title, adaptive_title_font) + title_font_size = adaptive_title_font.size # 居中显示 title_x = self.MARGIN + (content_width - title_w) // 2 @@ -134,14 +161,43 @@ class HeroBottomLayout(BaseLayout): draw, (title_x, cur_y), content.title, adaptive_title_font, theme.text, shadow_color=(0, 0, 0, 60), offset=(2, 2) ) + + # 记录标题 Fabric 对象 + self._add_object({ + "id": "title", + "type": "textbox", + "text": content.title, + "left": title_x, "top": cur_y, + "width": content_width, + "fontSize": title_font_size, + "fontFamily": "PingFang SC, Microsoft YaHei, sans-serif", + "fontWeight": "bold", + "fill": theme.text, + "shadow": "rgba(0,0,0,0.3) 2px 2px 4px", + "selectable": True, + }) cur_y += title_h + 14 # === 副标题 (支持换行) === + subtitle_top = cur_y if content.subtitle: _, sub_h = TextRenderer.draw_wrapped_text( draw, (self.MARGIN, cur_y), content.subtitle, subtitle_font, (*text_white, 200), content_width, line_spacing=4 ) + + # 记录副标题 Fabric 对象 + self._add_object({ + "id": "subtitle", + "type": "textbox", + "text": content.subtitle, + "left": self.MARGIN, "top": cur_y, + "width": content_width, + "fontSize": self.SUBTITLE_SIZE, + "fontFamily": "PingFang SC, Microsoft YaHei, sans-serif", + "fill": f"rgba({text_white[0]},{text_white[1]},{text_white[2]},0.85)", + "selectable": True, + }) cur_y += max(sub_h, 36) + 8 # === 装饰线 === @@ -160,42 +216,92 @@ class HeroBottomLayout(BaseLayout): cur_y += 20 # === 价格 === + price_top = cur_y if content.price: price_w, price_h = TextRenderer.measure_text(content.price, price_font) - # 计算后缀宽度 suffix = content.price_suffix or "" suffix_w = 0 if suffix: suffix_w, _ = TextRenderer.measure_text(suffix, suffix_font) - # 价格背景 (半透明主题色) + # 价格背景 bg_width = price_w + suffix_w + 40 if suffix else price_w + 24 self.shape.draw_rounded_rect(draw, (self.MARGIN - 12, cur_y - 6, self.MARGIN + bg_width, cur_y + price_h + 8), radius=12, fill=(*accent, 40)) - # 价格文字 (白色,与背景形成对比) + # 记录价格背景 Fabric 对象 + self._add_object({ + "id": "price_bg", + "type": "rect", + "left": self.MARGIN - 12, "top": cur_y - 6, + "width": bg_width + 12, "height": price_h + 14, + "rx": 12, "ry": 12, + "fill": f"rgba({accent[0]},{accent[1]},{accent[2]},0.25)", + "selectable": False, + }) + + # 价格文字 draw.text((self.MARGIN, cur_y), content.price, font=price_font, fill=text_white) - # 后缀 (如果有) + # 记录价格 Fabric 对象 + self._add_object({ + "id": "price", + "type": "text", + "text": content.price, + "left": self.MARGIN, "top": cur_y, + "fontSize": self.PRICE_SIZE, + "fontFamily": "PingFang SC, Microsoft YaHei, sans-serif", + "fontWeight": "bold", + "fill": theme.text, + "selectable": True, + }) + + # 后缀 if suffix: draw.text((self.MARGIN + price_w + 8, cur_y + price_h - 28), suffix, font=suffix_font, fill=(*text_white, 200)) + + self._add_object({ + "id": "price_suffix", + "type": "text", + "text": suffix, + "left": self.MARGIN + price_w + 8, "top": cur_y + price_h - 28, + "fontSize": 28, + "fontFamily": "PingFang SC, Microsoft YaHei, sans-serif", + "fill": f"rgba({text_white[0]},{text_white[1]},{text_white[2]},0.85)", + "selectable": True, + }) # === 标签 === if content.tags: tag_x = self.width - self.MARGIN - for tag in reversed(content.tags): + tag_top = cur_y + 20 + for i, tag in enumerate(reversed(content.tags)): tag_text = f"#{tag}" tag_w, _ = TextRenderer.measure_text(tag_text, tag_font) tag_x -= tag_w + 22 self.shape.draw_rounded_rect(draw, - (tag_x, cur_y + 20, tag_x + tag_w + 16, cur_y + 54), + (tag_x, tag_top, tag_x + tag_w + 16, tag_top + 34), radius=17, fill=(*accent, 35)) - draw.text((tag_x + 8, cur_y + 24), tag_text, + draw.text((tag_x + 8, tag_top + 4), tag_text, font=tag_font, fill=text_white) + + # 记录标签 Fabric 对象 + self._add_object({ + "id": f"tag_{i}", + "type": "text", + "text": tag_text, + "left": tag_x + 8, "top": tag_top + 4, + "fontSize": self.TAG_SIZE, + "fontFamily": "PingFang SC, Microsoft YaHei, sans-serif", + "fill": theme.text, + "backgroundColor": f"rgba({accent[0]},{accent[1]},{accent[2]},0.25)", + "padding": 8, + "selectable": True, + }) tag_x -= 8 return canvas diff --git a/poster_v2/layouts/overlay_bottom.py b/poster_v2/layouts/overlay_bottom.py index 20eac40..ab0b438 100644 --- a/poster_v2/layouts/overlay_bottom.py +++ b/poster_v2/layouts/overlay_bottom.py @@ -59,8 +59,10 @@ class OverlayBottomLayout(BaseLayout): return height - def generate(self, content: PosterContent, theme: Theme) -> Image.Image: + def generate(self, content: PosterContent, theme: Theme, image_url: str = "") -> Image.Image: """生成海报""" + self._reset_objects() + # 计算内容高度 content_height = self.calculate_content_height(content, theme) glass_y = self.height - content_height - 60 @@ -74,6 +76,16 @@ class OverlayBottomLayout(BaseLayout): img = content.image.copy().resize(self.size, Image.LANCZOS) canvas = img.convert("RGBA") + # 背景图片 Fabric 对象 + self._add_object({ + "id": "background_image", + "type": "image", + "src": image_url, + "left": 0, "top": 0, + "width": self.width, "height": self.height, + "selectable": True, + }) + # 毛玻璃效果 glass = self.effect.create_frosted_glass( canvas, (0, glass_y, self.width, self.height), @@ -81,6 +93,16 @@ class OverlayBottomLayout(BaseLayout): ) canvas.paste(glass, (0, glass_y)) + # 毛玻璃区域 Fabric 对象 + self._add_object({ + "id": "glass_bg", + "type": "rect", + "left": 0, "top": glass_y, + "width": self.width, "height": glass_height, + "fill": "rgba(255,255,255,0.92)", + "selectable": False, + }) + draw = ImageDraw.Draw(canvas) # 颜色 @@ -108,6 +130,20 @@ class OverlayBottomLayout(BaseLayout): draw, (self.MARGIN, cur_y), content.title, title_font, text_dark, content_width, line_spacing=4 ) + + # 标题 Fabric 对象 + self._add_object({ + "id": "title", + "type": "textbox", + "text": content.title, + "left": self.MARGIN, "top": cur_y, + "width": content_width, + "fontSize": self.TITLE_SIZE, + "fontFamily": "PingFang SC, Microsoft YaHei, sans-serif", + "fontWeight": "bold", + "fill": theme.text_dark, + "selectable": True, + }) cur_y += title_h + 12 # === 副标题 (支持换行) === @@ -116,13 +152,27 @@ class OverlayBottomLayout(BaseLayout): draw, (self.MARGIN, cur_y), content.subtitle, subtitle_font, (*text_dark, 150), content_width, line_spacing=4 ) + + # 副标题 Fabric 对象 + self._add_object({ + "id": "subtitle", + "type": "textbox", + "text": content.subtitle, + "left": self.MARGIN, "top": cur_y, + "width": content_width, + "fontSize": self.SUBTITLE_SIZE, + "fontFamily": "PingFang SC, Microsoft YaHei, sans-serif", + "fill": f"rgba({text_dark[0]},{text_dark[1]},{text_dark[2]},0.7)", + "selectable": True, + }) cur_y += max(sub_h, 34) + 16 # === 亮点标签 (胶囊,自动换行) === max_y = self.height - 150 # 预留底部价格区域 if content.highlights: hl_x = self.MARGIN - for hl in content.highlights[:4]: # 最多4个 + hl_start_y = cur_y + for i, hl in enumerate(content.highlights[:4]): # 最多4个 hl_w, _ = TextRenderer.measure_text(hl, hl_font) # 换行检测 if hl_x + hl_w + 36 > self.width - self.MARGIN: @@ -131,6 +181,20 @@ class OverlayBottomLayout(BaseLayout): if cur_y < max_y: self.shape.draw_capsule(draw, (hl_x, cur_y), hl_w, 40, (*accent, 50)) draw.text((hl_x + 14, cur_y + 6), hl, font=hl_font, fill=text_dark) + + # 亮点 Fabric 对象 + self._add_object({ + "id": f"highlight_{i}", + "type": "textbox", + "text": hl, + "left": hl_x + 14, "top": cur_y + 6, + "fontSize": self.HL_SIZE, + "fontFamily": "PingFang SC, Microsoft YaHei, sans-serif", + "fill": theme.text_dark, + "backgroundColor": f"rgba({accent[0]},{accent[1]},{accent[2]},0.25)", + "padding": 10, + "selectable": True, + }) hl_x += hl_w + 28 cur_y += 44 + 16 @@ -154,12 +218,25 @@ class OverlayBottomLayout(BaseLayout): if content.price: price_w, price_h = TextRenderer.measure_text(content.price, price_font) - # 价格背景 (更明显的颜色) + # 价格背景 self.shape.draw_rounded_rect(draw, (self.MARGIN - 10, cur_y - 6, self.MARGIN + price_w + 20, cur_y + price_h + 10), radius=12, fill=(*accent, 60)) draw.text((self.MARGIN, cur_y), content.price, font=price_font, fill=text_dark) + + # 价格 Fabric 对象 + self._add_object({ + "id": "price", + "type": "text", + "text": content.price, + "left": self.MARGIN, "top": cur_y, + "fontSize": self.PRICE_SIZE, + "fontFamily": "PingFang SC, Microsoft YaHei, sans-serif", + "fontWeight": "bold", + "fill": theme.text_dark, + "selectable": True, + }) # === 标签 === if content.tags: diff --git a/poster_v2/layouts/overlay_center.py b/poster_v2/layouts/overlay_center.py index e411cf8..21a028d 100644 --- a/poster_v2/layouts/overlay_center.py +++ b/poster_v2/layouts/overlay_center.py @@ -41,8 +41,10 @@ class OverlayCenterLayout(BaseLayout): return height - def generate(self, content: PosterContent, theme: Theme) -> Image.Image: + def generate(self, content: PosterContent, theme: Theme, image_url: str = "") -> Image.Image: """生成海报""" + self._reset_objects() + # 创建渐变背景 canvas = self.create_gradient_background(theme) @@ -51,10 +53,30 @@ class OverlayCenterLayout(BaseLayout): img = content.image.copy().resize(self.size, Image.LANCZOS) canvas = img.convert("RGBA") + # 背景图片 Fabric 对象 + self._add_object({ + "id": "background_image", + "type": "image", + "src": image_url, + "left": 0, "top": 0, + "width": self.width, "height": self.height, + "selectable": True, + }) + # 暗化叠加 canvas = self.effect.darken_overlay(canvas, alpha=60) draw = ImageDraw.Draw(canvas) + # 暗化遮罩 Fabric 对象 + self._add_object({ + "id": "dark_overlay", + "type": "rect", + "left": 0, "top": 0, + "width": self.width, "height": self.height, + "fill": "rgba(0,0,0,0.35)", + "selectable": False, + }) + text_white = theme.text_rgb accent = theme.accent_rgb @@ -85,6 +107,22 @@ class OverlayCenterLayout(BaseLayout): draw, (title_x, center_y), content.title, adaptive_font, theme.text, shadow_color=(0, 0, 0, 80), offset=(3, 3) ) + + # 标题 Fabric 对象 + self._add_object({ + "id": "title", + "type": "textbox", + "text": content.title, + "left": title_x, "top": center_y, + "width": content_width, + "fontSize": adaptive_font.size, + "fontFamily": "PingFang SC, Microsoft YaHei, sans-serif", + "fontWeight": "bold", + "fill": theme.text, + "textAlign": "center", + "shadow": "rgba(0,0,0,0.5) 3px 3px 6px", + "selectable": True, + }) center_y += title_h + 24 # === 副标题 (居中) === @@ -93,6 +131,20 @@ class OverlayCenterLayout(BaseLayout): sub_x = (self.width - sub_w) // 2 draw.text((sub_x, center_y), content.subtitle, font=subtitle_font, fill=(*text_white, 200)) + + # 副标题 Fabric 对象 + self._add_object({ + "id": "subtitle", + "type": "textbox", + "text": content.subtitle, + "left": sub_x, "top": center_y, + "width": content_width, + "fontSize": self.SUBTITLE_SIZE, + "fontFamily": "PingFang SC, Microsoft YaHei, sans-serif", + "fill": "rgba(255,255,255,0.85)", + "textAlign": "center", + "selectable": True, + }) center_y += sub_h + 40 # === 装饰线 (副标题下方) === diff --git a/poster_v2/layouts/split_vertical.py b/poster_v2/layouts/split_vertical.py index 928cac6..0159839 100644 --- a/poster_v2/layouts/split_vertical.py +++ b/poster_v2/layouts/split_vertical.py @@ -29,8 +29,10 @@ class SplitVerticalLayout(BaseLayout): """计算内容区域高度 (此布局不需要动态高度)""" return self.height - def generate(self, content: PosterContent, theme: Theme) -> Image.Image: + def generate(self, content: PosterContent, theme: Theme, image_url: str = "") -> Image.Image: """生成海报""" + self._reset_objects() + split = self.width // 2 # 左右各50% # 创建画布 @@ -51,6 +53,16 @@ class SplitVerticalLayout(BaseLayout): img = img.resize((split, self.height), Image.LANCZOS) canvas.paste(img, (0, 0)) + # 左侧图片 Fabric 对象 + self._add_object({ + "id": "background_image", + "type": "image", + "src": image_url, + "left": 0, "top": 0, + "width": split, "height": self.height, + "selectable": True, + }) + draw = ImageDraw.Draw(canvas) # 颜色 @@ -84,10 +96,25 @@ class SplitVerticalLayout(BaseLayout): cur_y += 44 # === 标题 (支持换行) === + title_top = cur_y _, title_h = TextRenderer.draw_wrapped_text( draw, (content_x, cur_y), content.title, title_font, text_dark, content_width, line_spacing=6 ) + + # 标题 Fabric 对象 + self._add_object({ + "id": "title", + "type": "textbox", + "text": content.title, + "left": content_x, "top": cur_y, + "width": content_width, + "fontSize": self.TITLE_SIZE, + "fontFamily": "PingFang SC, Microsoft YaHei, sans-serif", + "fontWeight": "bold", + "fill": theme.text_dark, + "selectable": True, + }) cur_y += title_h + 16 # === 装饰线 (标题下方) === @@ -100,6 +127,19 @@ class SplitVerticalLayout(BaseLayout): draw, (content_x, cur_y), content.subtitle, subtitle_font, (*text_dark, 130), content_width, line_spacing=8 ) + + # 副标题 Fabric 对象 + self._add_object({ + "id": "subtitle", + "type": "textbox", + "text": content.subtitle, + "left": content_x, "top": cur_y, + "width": content_width, + "fontSize": self.SUBTITLE_SIZE, + "fontFamily": "PingFang SC, Microsoft YaHei, sans-serif", + "fill": f"rgba({text_dark[0]},{text_dark[1]},{text_dark[2]},0.6)", + "selectable": True, + }) cur_y += sub_h + 24 # === 特色亮点 (横排标签) === @@ -152,10 +192,34 @@ class SplitVerticalLayout(BaseLayout): draw.text((content_x, price_y), content.price, font=price_font, fill=primary) + # 价格 Fabric 对象 + self._add_object({ + "id": "price", + "type": "text", + "text": content.price, + "left": content_x, "top": price_y, + "fontSize": self.PRICE_SIZE, + "fontFamily": "PingFang SC, Microsoft YaHei, sans-serif", + "fontWeight": "bold", + "fill": theme.primary, + "selectable": True, + }) + # 后缀 (如果有) suffix = content.price_suffix or "" if suffix: draw.text((content_x + price_w + 8, price_y + price_h - 28), suffix, font=small_font, fill=(*text_dark, 120)) + + self._add_object({ + "id": "price_suffix", + "type": "text", + "text": suffix, + "left": content_x + price_w + 8, "top": price_y + price_h - 28, + "fontSize": self.SMALL_SIZE, + "fontFamily": "PingFang SC, Microsoft YaHei, sans-serif", + "fill": f"rgba({text_dark[0]},{text_dark[1]},{text_dark[2]},0.6)", + "selectable": True, + }) return canvas