@@ -108,6 +108,26 @@ def clear(self):
108108 self .layers .clear ()
109109 self .min_cached_layer = 0
110110
111+
112+ class LayerBlendMode (enum .Enum ):
113+ """图层混合模式。NORMAL 为默认 alpha 合成,MULTIPLY 为正片叠底。
114+ 序列化时使用 name.lower()(如 "normal", "multiply"),反序列化用 LayerBlendMode.from_serialized(str)。"""
115+ NORMAL = enum .auto ()
116+ MULTIPLY = enum .auto ()
117+
118+ def to_serialized (self ) -> str :
119+ return self .name .lower ()
120+
121+ @classmethod
122+ def from_serialized (cls , s : str | None ) -> "LayerBlendMode" :
123+ if s is None or s .strip () == "" :
124+ return cls .NORMAL
125+ try :
126+ return cls [s .upper ()]
127+ except KeyError :
128+ raise ValueError ("Unsupported layer mode: " + s + " (only normal and multiply are supported)" )
129+
130+
111131@AssetClassDecl ("imagepack" )
112132@ToolClassDecl ("imagepack" )
113133class ImagePack (NamedAssetClassBase ):
@@ -175,11 +195,13 @@ class LayerInfo:
175195 base : bool
176196 # 如果是 True 的话,该层可以不受限制地单独被选取
177197 toggle : bool
198+ # 图层混合模式
199+ mode : LayerBlendMode
178200
179201 def __init__ (self , patch : ImageWrapper ,
180202 offset_x : int = 0 , offset_y : int = 0 ,
181203 width : int = 0 , height : int = 0 ,
182- base : bool = False , toggle : bool = False , basename : str = '' ) -> None :
204+ base : bool = False , toggle : bool = False , basename : str = '' , mode : LayerBlendMode | None = None ) -> None :
183205 self .patch = patch
184206 self .basename = basename
185207 self .offset_x = offset_x
@@ -188,6 +210,7 @@ def __init__(self, patch : ImageWrapper,
188210 self .height = height
189211 self .base = base
190212 self .toggle = toggle
213+ self .mode = mode if mode is not None else LayerBlendMode .NORMAL
191214 if width == 0 or height == 0 :
192215 raise RuntimeError ("Zero-sized layer?" )
193216
@@ -199,7 +222,7 @@ def get_shrinked(self, ratio : decimal.Decimal):
199222 offset_x = int (self .offset_x * ratio ),
200223 offset_y = int (self .offset_y * ratio ),
201224 width = newwidth , height = newheight ,
202- base = self .base , toggle = self .toggle , basename = self .basename )
225+ base = self .base , toggle = self .toggle , basename = self .basename , mode = self . mode )
203226
204227 class CompositeInfo :
205228 # 保存时每个差分的信息
@@ -328,6 +351,8 @@ def collect_layer_group(prefix : str, layers : list[ImagePack.LayerInfo]):
328351 flags .append ("toggle" )
329352 if len (flags ) > 0 :
330353 jsonobj ["flags" ] = flags
354+ if l .mode != LayerBlendMode .NORMAL :
355+ jsonobj ["mode" ] = l .mode .to_serialized ()
331356 result .append (jsonobj )
332357 self ._write_image_to_path (l .patch , filename , path )
333358 return result
@@ -430,11 +455,16 @@ def read_layer_group(prefix, group_name):
430455 height = layer_info ["h" ]
431456 base = "base" in layer_info .get ("flags" , [])
432457 toggle = "toggle" in layer_info .get ("flags" , [])
458+ mode_str = layer_info .get ("mode" , None )
459+ try :
460+ mode = LayerBlendMode .from_serialized (mode_str )
461+ except ValueError as e :
462+ raise PPInternalError (str (e ))
433463 layer_img = ImageWrapper (path = os .path .join (path , layer_filename ))
434464 layers .append (ImagePack .LayerInfo (patch = layer_img ,
435465 offset_x = offset_x , offset_y = offset_y ,
436466 width = width , height = height ,
437- base = base , toggle = toggle , basename = layer_info ["p" ]))
467+ base = base , toggle = toggle , basename = layer_info ["p" ], mode = mode ))
438468 return layers
439469
440470 self .layers = read_layer_group ("l" , "layers" )
@@ -503,7 +533,11 @@ def get_composed_image_lower(self, layer_indices : list[int], composition_cache:
503533 for li in layer_indices :
504534 layer = self .layers [li ]
505535 cur = result .crop ((layer .offset_x , layer .offset_y , layer .offset_x + layer .width , layer .offset_y + layer .height ))
506- cur = PIL .Image .alpha_composite (cur , layer .patch .get ())
536+ layer_patch = layer .patch .get ()
537+ if layer .mode == LayerBlendMode .MULTIPLY :
538+ cur = ImagePack .apply_multiply_blend_mode (cur , layer_patch )
539+ else :
540+ cur = PIL .Image .alpha_composite (cur , layer_patch )
507541 if composition_cache is not None and li >= composition_cache .min_cached_layer :
508542 result = result .copy ()
509543 result .paste (cur , (layer .offset_x , layer .offset_y ))
@@ -513,6 +547,21 @@ def get_composed_image_lower(self, layer_indices : list[int], composition_cache:
513547 # print(f"get_composed_image_lower: {end-start} s")
514548 return ImageWrapper (image = result )
515549
550+ @staticmethod
551+ def apply_multiply_blend_mode (base : PIL .Image .Image , overlay : PIL .Image .Image ) -> PIL .Image .Image :
552+ base_array = np .array (base , dtype = np .float32 )
553+ overlay_array = np .array (overlay , dtype = np .float32 )
554+ overlay_alpha = overlay_array [:, :, 3 :4 ] / 255.0
555+ base_alpha = base_array [:, :, 3 :4 ] / 255.0
556+ result_rgb = base_array [:, :, :3 ] * overlay_array [:, :, :3 ] / 255.0
557+ result_alpha = overlay_alpha + base_alpha * (1.0 - overlay_alpha )
558+ base_contribution = 1.0 - overlay_alpha
559+ final_rgb = result_rgb * overlay_alpha + base_array [:, :, :3 ] * base_contribution
560+ final_rgb = np .clip (final_rgb , 0 , 255 ).astype (np .uint8 )
561+ final_alpha = np .clip (result_alpha * 255 , 0 , 255 ).astype (np .uint8 )
562+ result_array = np .dstack ((final_rgb , final_alpha ))
563+ return PIL .Image .fromarray (result_array , mode = 'RGBA' )
564+
516565 @staticmethod
517566 def ndarray_hsv_to_rgb (hsv : np .ndarray ) -> np .ndarray :
518567 #return np.apply_along_axis(ImagePack.hsv_to_rgb, 1, hsv)
@@ -726,7 +775,7 @@ def create_forked_layer(imgwidth : int, imgheight : int, l : LayerInfo, layerind
726775 return ImagePack .LayerInfo (ImageWrapper (image = newbase .crop (bbox )),
727776 offset_x = offset_x , offset_y = offset_y ,
728777 width = xmax - offset_x , height = ymax - offset_y ,
729- base = True , toggle = l .toggle , basename = l .basename )
778+ base = True , toggle = l .toggle , basename = l .basename , mode = l . mode )
730779
731780 def fork_applying_mask (self , args : list [Color | PIL .Image .Image | str | tuple [str , Color ] | None ], enable_parallelization : bool = False ):
732781 # 创建一个新的 imagepack, 将 mask 所影响的部分替换掉
@@ -1336,6 +1385,7 @@ def lookup_file(filename : str) -> str:
13361385 layer_dict [imgpathbase ] = layerindex
13371386 flag_base = False
13381387 flag_toggle = False
1388+ flag_mode : LayerBlendMode = LayerBlendMode .NORMAL
13391389 def add_flag (flag : str ):
13401390 nonlocal flag_base
13411391 nonlocal flag_toggle
@@ -1357,6 +1407,13 @@ def add_flag(flag : str):
13571407 elif isinstance (value , list ):
13581408 for flag in value :
13591409 add_flag (flag )
1410+ elif key in ImagePack .TR_imagepack_yamlparse_mode .get_all_candidates ():
1411+ if not isinstance (value , str ):
1412+ raise PPInternalError ("Invalid mode in " + yamlpath + ": expecting a str but got " + str (value ))
1413+ if value in ImagePack .TR_imagepack_yamlparse_multiply .get_all_candidates ():
1414+ flag_mode = LayerBlendMode .MULTIPLY
1415+ else :
1416+ raise PPInternalError ("Unsupported layer mode: " + value + " (only normal and multiply are supported)" )
13601417 imgpath = lookup_file (imgpathbase + ".png" )
13611418 if not os .path .exists (os .path .join (basepath , imgpath )):
13621419 raise PPInternalError ("Image file not found: " + imgpath )
@@ -1379,7 +1436,7 @@ def add_flag(flag : str):
13791436 newlayer = ImagePack .LayerInfo (patch ,
13801437 offset_x = offset_x , offset_y = offset_y ,
13811438 width = img .width , height = img .height ,
1382- base = flag_base , toggle = flag_toggle , basename = imgpathbase )
1439+ base = flag_base , toggle = flag_toggle , basename = imgpathbase , mode = flag_mode )
13831440 result .layers .append (newlayer )
13841441 if composites is None :
13851442 for i , layer in enumerate (result .layers ):
@@ -1639,6 +1696,21 @@ class CharacterSpritePartsBased_PartKind(enum.Enum):
16391696 zh_cn = "基底简写" ,
16401697 zh_hk = "基底簡寫" ,
16411698 )
1699+ TR_imagepack_yamlgen_layer_modes = TR_imagepack .tr ("layer_modes" ,
1700+ en = "layer_modes" ,
1701+ zh_cn = "图层模式" ,
1702+ zh_hk = "圖層模式" ,
1703+ )
1704+ TR_imagepack_yamlparse_mode = TR_imagepack .tr ("mode" ,
1705+ en = "mode" ,
1706+ zh_cn = "模式" ,
1707+ zh_hk = "模式" ,
1708+ )
1709+ TR_imagepack_yamlparse_multiply = TR_imagepack .tr ("multiply" ,
1710+ en = "multiply" ,
1711+ zh_cn = "正片叠底" ,
1712+ zh_hk = "正片疊底" ,
1713+ )
16421714
16431715 @dataclasses .dataclass
16441716 class CharacterSpritePartsBased_PartsDecl :
@@ -1661,6 +1733,9 @@ def yaml_generation_charactersprite_parts_based(data : dict, layers : dict | Non
16611733 # 为了避免基底所用的名称太长(比如每个选区都有独立的图层、立绘有部分需要拆成独立的置顶的图层),我们支持定义基底组合的简称,如果有简称的话所有基底组合都必须有简称
16621734 base_abbreviation : dict [str , tuple [str ,...]] | None = None
16631735
1736+ # 存储图层模式映射:图层名 -> 模式
1737+ layer_modes : dict [str , LayerBlendMode ] = {}
1738+
16641739 kinds_enum_map : dict [str , ImagePack .CharacterSpritePartsBased_PartKind ] = {}
16651740
16661741 # 除了装饰部件以外,每种部件类型最多只能声明一次
@@ -1785,11 +1860,22 @@ def add_parsed_info_to_metadata():
17851860 if not isinstance (abbr_list , str ):
17861861 raise PPInternalError ("Invalid abbreviation list in generation: expecting a str but got " + str (abbr_list ) + " (type: " + str (type (abbr_list )) + ")" )
17871862 base_abbreviation [abbr_name ] = parse_dep_liststr (abbr_list )
1863+ elif k in ImagePack .TR_imagepack_yamlgen_layer_modes .get_all_candidates ():
1864+ if not isinstance (v , dict ):
1865+ raise PPInternalError ("Invalid layer_modes in generation: expecting a dict but got " + str (v ) + " (type: " + str (type (v )) + ")" )
1866+ for layer_name , mode in v .items ():
1867+ if not isinstance (mode , str ):
1868+ raise PPInternalError ("Invalid layer mode for " + layer_name + ": expecting a str but got " + str (mode ) + " (type: " + str (type (mode )) + ")" )
1869+ if mode in ImagePack .TR_imagepack_yamlparse_multiply .get_all_candidates ():
1870+ layer_modes [layer_name ] = LayerBlendMode .MULTIPLY
1871+ else :
1872+ raise PPInternalError ("Unsupported layer mode for " + layer_name + ": " + mode + " (only normal and multiply are supported)" )
17881873 else :
17891874 raise PPInternalError ("Unknown key in generation: " + k + "(supported keys: "
17901875 + str (ImagePack .TR_imagepack_yamlgen_parts .get_all_candidates ()) + ", "
17911876 + str (ImagePack .TR_imagepack_yamlgen_parts_kind .get_all_candidates ()) + ", "
1792- + str (ImagePack .TR_imagepack_yamlgen_tags .get_all_candidates ()) + ")" )
1877+ + str (ImagePack .TR_imagepack_yamlgen_tags .get_all_candidates ()) + ", "
1878+ + str (ImagePack .TR_imagepack_yamlgen_layer_modes .get_all_candidates ()) + ")" )
17931879 # 初步解析完毕,我们将解析结果写入 metadata
17941880 add_parsed_info_to_metadata ()
17951881 # 检查一下输入有没有问题,所有标签是否都有定义,所有部件是否都有定义
@@ -1937,6 +2023,8 @@ def try_add_combinations(parts_list : tuple[str, ...]):
19372023 part_dict = {}
19382024 if kinds_enum_map [part .kind ] == ImagePack .CharacterSpritePartsBased_PartKind .BASE :
19392025 part_dict [ImagePack .TR_imagepack_yamlparse_flags .get ()] = ImagePack .TR_imagepack_yamlparse_base .get ()
2026+ if part .name in layer_modes and layer_modes [part .name ] == LayerBlendMode .MULTIPLY :
2027+ part_dict [ImagePack .TR_imagepack_yamlparse_mode .get ()] = ImagePack .TR_imagepack_yamlparse_multiply .get ()
19402028 layer_orders [part .name ] = len (result_layers )
19412029 result_layers [part .name ] = part_dict
19422030 result_composites : dict [str , list [str ]] = {}
0 commit comments