def prefix_match(self, target_chain: MessageChain): target_chain = target_chain.asMerged() chain_frames: List[MessageChain] = target_chain.split(" ", raw_string=True) # 前缀匹配 if len(self.prefixs) > len(chain_frames): return for index, current_prefix in enumerate(self.prefixs): current_frame = chain_frames[index] if (not current_frame.__root__ or type(current_frame.__root__[0]) is not Plain): return if current_frame.__root__[0].text != current_prefix: return chain_frames = chain_frames[len(self.prefixs):] return MessageChain.create( list( itertools.chain( *[i.__root__ + [Plain(" ")] for i in chain_frames]))[:-1]).asMerged()
def detect_index( signature_chain: Tuple[Union[NormalMatch, PatternReceiver]], message_chain: MessageChain, ) -> Optional[Dict[str, Tuple[MessageIndex, MessageIndex]]]: merged_chain = merge_signature_chain(signature_chain) message_chain = message_chain.asMerged() element_num = len(message_chain.__root__) end_index: MessageIndex = ( element_num - 1, len(message_chain.__root__[-1].text) if element_num != 0 and message_chain.__root__[-1].__class__ is Plain else None, ) reached_message_index: MessageIndex = (0, None) # [0] => real_index # [1] => text_index(optional) start_index: MessageIndex = (0, None) match_result: Dict[Arguments, Tuple[ MessageIndex, MessageIndex], # start(include) # stop(exclude) ] = {} signature_iterable = InsertGenerator(enumerate(merged_chain)) latest_index = None matching_recevier: Optional[Arguments] = None for signature_index, signature in signature_iterable: if isinstance(signature, (Arguments, PatternReceiver)): if matching_recevier: # 已经选中了一个... if isinstance(signature, Arguments): if latest_index == signature_index: matching_recevier.content.extend(signature.content) continue else: raise TypeError( "a unexpected case: match conflict") if isinstance(signature, PatternReceiver): matching_recevier.content.append(signature) continue else: if isinstance(signature, PatternReceiver): signature = Arguments([signature]) matching_recevier = signature start_index = reached_message_index elif isinstance(signature, NormalMatch): if not matching_recevier: # 如果不要求匹配参数, 从当前位置(reached_message_index)开始匹配FullMatch. current_chain = message_chain.subchain( slice(reached_message_index, None, None)) if not current_chain.__root__: # index 越界 return if not isinstance(current_chain.__root__[0], Plain): # 切片后第一个 **不是** Plain. return re_match_result = re.match(signature.operator(), current_chain.__root__[0].text) if not re_match_result: # 不匹配的 return # 推进当前进度. plain_text_length = len(current_chain.__root__[0].text) pattern_length = re_match_result.end( ) - re_match_result.start() if (pattern_length + 1) > plain_text_length: # 推进后可能造成错误 # 不推进 text_index 进度, 转而推进 element_index 进度 reached_message_index = (reached_message_index[0] + 1, None) else: # 推进 element_index 进度至已匹配到的地方后. reached_message_index = ( reached_message_index[0], origin_or_zero(reached_message_index[1]) + re_match_result.start() + pattern_length, ) else: # 需要匹配参数(是否贪婪模式查找, 即是否从后向前) greed = matching_recevier.isGreed for element_index, element in enumerate( message_chain.subchain( slice(reached_message_index, None, None)).__root__): if isinstance(element, Plain): current_text: str = element.text # 完成贪婪判断 text_find_result_list = list( re.finditer(signature.operator(), current_text)) if not text_find_result_list: continue text_find_result = text_find_result_list[-int(greed )] if not text_find_result: continue text_find_index = text_find_result.start() # 找到了! 这里不仅要推进进度, 还要把当前匹配的参数记录结束位置并清理. stop_index = ( reached_message_index[0] + element_index + int(element_index == 0), origin_or_zero(reached_message_index[1]) + text_find_index, ) match_result[matching_recevier] = ( copy.copy(start_index), stop_index, ) start_index = (0, None) matching_recevier = None pattern_length = (text_find_result.end() - text_find_result.start()) if (current_text == text_find_result.string[slice( *text_find_result.span())]): # 此处是如果推进 text_index 就会被爆破.... # 推进 element_index 而不是 text_index reached_message_index = ( reached_message_index[0] + element_index + int(element_index != 0), None, ) else: reached_message_index = ( reached_message_index[0] + element_index, origin_or_zero(reached_message_index[1]) + text_find_index + pattern_length, ) break else: # 找遍了都没匹配到. return latest_index = signature_index else: if matching_recevier: # 到达了终点, 却仍然还要做点事的. # 计算终点坐标. text_index = None latest_element = message_chain.__root__[-1] if isinstance(latest_element, Plain): text_index = len(latest_element.text) stop_index = (len(message_chain.__root__), text_index) match_result[matching_recevier] = (start_index, stop_index) else: # 如果不需要继续捕获消息作为参数, 但 Signature 已经无法指示 Message 的样式时, 判定本次匹配非法. if reached_message_index < end_index: return return match_result
def detect_index(self, target_chain: MessageChain): target_chain = target_chain.asMerged() detect_result: Dict[str, List[Tuple[MessageIndex, MessageIndex]]] = { "_": [] # 相当于 *args. } chain_frames: List[MessageChain] = target_chain.split(self.delimiter, raw_string=True) # 前缀匹配 if len(self.prefixs) > len(chain_frames): return for index in range(len(self.prefixs)): current_prefix = self.prefixs[index] current_frame = chain_frames[index] if (not current_frame.__root__ or type(current_frame.__root__[0]) is not Plain): return if current_frame.__root__[0].text != current_prefix: return chain_frames = chain_frames[len(self.prefixs):] # 清除无关数据, 开始执行. collections: List[Element] = detect_result["_"] detecting_param = None local_iter = MultiUsageGenerator(enumerate(chain_frames)) # print(list(local_iter)) for iter_item in local_iter: print(92, iter_item) index, current_frame = iter_item current_frame: MessageChain if not current_frame.__root__: collections.append(current_frame) continue if detecting_param and isinstance( self.param_settings[detecting_param], BoxParameter): detect_result[detecting_param] = current_frame detecting_param = None continue splited = current_frame.split("=", raw_string=True) origin_data = self.param_settings_index.get(splited[0].asDisplay()) if not origin_data: if not detecting_param: if current_frame.startswith( '"'): # TODO: 我现在还需要一个更加合理的引号出现判断. # debug("ocur", index, chain_frames, current_frame) afters_root = MessageChain.create( sum( [[*i.__root__, Plain(self.delimiter)] for i in chain_frames[max(0, index - 1):]], [], )[:-1]).asMerged() print("aftersRoot", index, afters_root) break_flag_root = False param_content_root = [] for elem in afters_root.subchain( slice((0, 1), None, None), ignore_text_index=True): print(elem) if break_flag_root: break_flag_root = False break if isinstance(elem, Plain): continue_flag = False for text_index, text_i in enumerate(elem.text): if continue_flag: continue_flag = False continue if text_i == "\\": continue_flag = True if text_i == '"': param_content_root.append( Plain(elem.text[:text_index])) break_flag_root = True break else: # 没找到 param_content_root.append(elem) break else: param_content_root.append(elem) break else: if not break_flag_root: raise ValueError("no closing quotes") param_content_chain_root = MessageChain.create( param_content_root) local_iter.continue_count += len( param_content_chain_root.split(self.delimiter, raw_string=True)) collections.append(param_content_chain_root) else: collections.append(current_frame) continue param_name, setting = origin_data detecting_param = param_name if param_name in detect_result: continue # 用户重复输入了参数 if isinstance(setting, SwitchParameter): # 这里是已经被 catch 到了. if setting.auto_reverse: detect_result[param_name] = not setting.default else: detect_result[param_name] = True elif isinstance(setting, BoxParameter): afters = MessageChain.create( sum( ([i.__root__ for i in splited[1:]] + [[Plain(self.delimiter)]] if len(splited) > 1 else []) + [[*i.__root__, Plain(self.delimiter)] for i in chain_frames[index + 1:]], [], )[:-1]).asMerged() break_flag = False if afters.startswith('"'): param_content = [] for elem in afters.subchain(slice((0, 1), None, None), ignore_text_index=True): if break_flag: break_flag = False break if isinstance(elem, Plain): continue_flag = False for text_index, text_i in enumerate(elem.text): if continue_flag: continue_flag = False continue if text_i == "\\": continue_flag = True if text_i == '"': param_content.append( Plain(elem.text[:text_index])) break_flag = True break else: # 没找到 param_content.append(elem) else: param_content.append(elem) else: if not break_flag: raise ValueError("no closing quotes") param_content_chain = MessageChain.create(param_content) print( param_content, chain_frames[len( param_content_chain. split(self.delimiter, raw_string=True)) + 1:], ) local_iter.continue_count += (len( param_content_chain.split(self.delimiter, raw_string=True)) - 1) detect_result[param_name] = [ MessageChain.create(param_content) ] else: detecting_param = param_name if len(splited) > 1 and not break_flag: local_iter.insert_items.extend([ (index + l_index, value) for l_index, value in enumerate(splited[1:], 1) ]) detecting_param = None else: if detecting_param and detecting_param not in detect_result: if self.param_settings[detecting_param].default is None: raise ValueError("require for " + detecting_param) # 后处理 for k, v in self.param_settings.items(): if isinstance(v, SwitchParameter) and k not in detect_result: detect_result[k] = v.default elif isinstance(v, BoxParameter) and k not in detect_result: detect_result[k] = Force(None) return detect_result
async def messagechain_to_img( message: MessageChain, max_width: int = 1080, font_size: int = 40, spacing: int = 15, padding_x: int = 20, padding_y: int = 15, img_fixed: bool = False, font_path: str = "./simhei.ttf", save_path: str = "./statics/temp/tempMessageChainToImg.png" ) -> MessageChain: """ 将 MessageChain 转换为图片,仅支持只含有本地图片/文本的 MessageChain Args: message: 要转换的MessageChain max_width: 最大长度 font_size: 字体尺寸 spacing: 行间距 padding_x: x轴距离边框大小 padding_y: y轴距离边框大小 img_fixed: 图片是否适应大小(仅适用于图片小于最大长度时) font_path: 字体文件路径 save_path: 图片存储路径 Examples: msg = await messagechain_to_img(message=message) Returns: MessageChain (内含图片Image类) """ font = ImageFont.truetype(font_path, font_size, encoding="utf-8") message = message.asMerged() elements = message.__root__ plains = message.get(Plain) text_gather = "\n".join([plain.text for plain in plains]) print( max(font.getsize(text)[0] for text in text_gather.split("\n")) + 2 * padding_x) final_width = min( max(font.getsize(text)[0] for text in text_gather.split("\n")) + 2 * padding_x, max_width) text_width = final_width - 2 * padding_x text_height = (font_size + spacing) * await get_final_text_lines( text_gather, text_width, font) # text_height = (font_size + spacing) * sum([await get_final_text_lines(plain.text, text_width, font)for plain in plains]) img_height_sum = 0 temp_img_list = [] images = message.get(Image_LocalFile) for image in images: if isinstance(image, Image_LocalFile): # print(img_height_sum) temp_img = IMG.open(image.filepath) # print(temp_img.size) img_width, img_height = temp_img.size temp_img_list.append( temp_img := temp_img.resize(( int(final_width - 2 * spacing), int( float(img_height * (final_width - 2 * spacing)) / float(img_width)))) if img_width > final_width - 2 * spacing or (img_fixed and img_width < final_width - 2 * spacing) else temp_img) img_height_sum = img_height_sum + temp_img.size[1] # print(temp_img.size[1]) # print(img_height) else: raise Exception("messagechain_to_img:仅支持本地图片即Image_LocalFile类的处理!") final_height = 2 * padding_y + text_height + img_height_sum picture = IMG.new('RGB', (final_width, final_height), (255, 255, 255)) draw = ImageDraw.Draw(picture) present_x = padding_x present_y = padding_y image_index = 0 # print(temp_img_list) for element in elements: if isinstance(element, Image_LocalFile): print(f"adding img {image_index}") picture.paste(temp_img_list[image_index], (present_x, present_y)) present_y += (spacing + temp_img_list[image_index].size[1]) image_index += 1 elif isinstance(element, Plain): print(f"adding text '{element.text}'") # if font.getsize(element.text)[0] <= text_width: # draw.text((present_x, present_y), element.text, font=font, fill=(0, 0, 0)) # else: for char in element.text: if char == "\n": present_y += (font_size + spacing) present_x = padding_x continue if present_x + font.getsize(char)[0] > text_width: present_y += (font_size + spacing) present_x = padding_x draw.text((present_x, present_y), char, font=font, fill=(0, 0, 0)) present_x += font.getsize(char)[0] present_y += (font_size + spacing) present_x = padding_x # print(f"textHeight: {text_height}\nimgHeight: {img_height}\nfinalHeight: {final_height}") # print(f"present_x: {present_x}, present_y: {present_y}") picture.save(save_path) print(f"process finished! Image saved at {save_path}") return MessageChain.create([Image.fromLocalFile(save_path)])
async def messagechain_to_img( message: MessageChain, max_width: int = 1080, font_size: int = 40, spacing: int = 15, padding_x: int = 20, padding_y: int = 15, img_fixed: bool = False, font_path: str = f"{os.getcwd()}/statics/fonts/STKAITI.TTF", ) -> MessageChain: """ 将 MessageChain 转换为图片,仅支持只含有本地图片/文本的 MessageChain Args: message: 要转换的MessageChain max_width: 最大长度 font_size: 字体尺寸 spacing: 行间距 padding_x: x轴距离边框大小 padding_y: y轴距离边框大小 img_fixed: 图片是否适应大小(仅适用于图片小于最大长度时) font_path: 字体文件路径 Examples: msg = await messagechain_to_img(message=message) Returns: MessageChain (内含图片Image类) """ def get_final_text_lines(text: str, text_width: int, font: ImageFont.FreeTypeFont) -> int: lines = text.split("\n") line_count = 0 for line in lines: if not line: line_count += 1 continue line_count += int(math.ceil(float(font.getsize(line)[0]) / float(text_width))) return line_count + 1 font = ImageFont.truetype(font_path, font_size, encoding="utf-8") message = message.asMerged() elements = message.__root__ plains = message.get(Plain) text_gather = "\n".join([plain.text for plain in plains]) # print(max(font.getsize(text)[0] for text in text_gather.split("\n")) + 2 * padding_x) final_width = min(max(font.getsize(text)[0] for text in text_gather.split("\n")) + 2 * padding_x, max_width) text_width = final_width - 2 * padding_x text_height = (font_size + spacing) * get_final_text_lines(text_gather, text_width, font) img_height_sum = 0 temp_img_list = [] images = [element for element in message.__root__ if (isinstance(element, Image_LocalFile) or isinstance(element, Image_UnsafeBytes))] for image in images: if isinstance(image, Image_LocalFile): temp_img = IMG.open(image.filepath) elif isinstance(image, Image_UnsafeBytes): temp_img = IMG.open(BytesIO(image.image_bytes)) else: raise ValueError("messagechain_to_img:仅支持Image_LocalFile和Image_UnsafeBytes类的处理!") img_width, img_height = temp_img.size temp_img_list.append( temp_img := temp_img.resize(( int(final_width - 2 * spacing), int(float(img_height * (final_width - 2 * spacing)) / float(img_width)) )) if img_width > final_width - 2 * spacing or (img_fixed and img_width < final_width - 2 * spacing) else temp_img ) img_height_sum = img_height_sum + temp_img.size[1] final_height = 2 * padding_y + text_height + img_height_sum picture = IMG.new('RGB', (final_width, final_height), (255, 255, 255)) draw = ImageDraw.Draw(picture) present_x = padding_x present_y = padding_y image_index = 0 for element in elements: if isinstance(element, Image) or isinstance(element, Image_UnsafeBytes) or isinstance(element, Image_LocalFile): picture.paste(temp_img_list[image_index], (present_x, present_y)) present_y += (spacing + temp_img_list[image_index].size[1]) image_index += 1 elif isinstance(element, Plain): for char in element.text: if char == "\n": present_y += (font_size + spacing) present_x = padding_x continue if char == "\r": continue if present_x + font.getsize(char)[0] > text_width: present_y += (font_size + spacing) present_x = padding_x draw.text((present_x, present_y), char, font=font, fill=(0, 0, 0)) present_x += font.getsize(char)[0] present_y += (font_size + spacing) present_x = padding_x bytes_io = BytesIO() picture.save(bytes_io, format='PNG') logger.success("消息转图片处理成功!") return MessageChain.create([ Image.fromUnsafeBytes(bytes_io.getvalue()) ])