├── README.md ├── data.txt ├── image ├── 1.jpg ├── 10.jpg ├── 11.jpg ├── 12.jpg ├── 13.jpg ├── 2.jpg ├── 3.jpg ├── 4.jpeg ├── 5.jpeg ├── 6.jpeg ├── 7.jpeg ├── 8.jpg └── 9.jpg ├── netease_lyric.py ├── output ├── 《东北大学校歌》1581077400.png ├── 《了不起的盖茨比》1581077407.png ├── 《锁麟囊》1581077403.png ├── 我的感慨1581077404.png ├── 无名氏1581077405.png ├── 无名氏1581077406.png ├── 李白《清平乐·画堂晨起》1581077402.png ├── 森见登美彦1581075253.png ├── 森见登美彦1581077406.png ├── 苏轼《行香子》1581077404.png ├── 许晶晶《残叶篇》1581077401.png └── 郭军老师1581077401.png ├── static ├── STHeiti_Light.ttc └── netease_icon.png └── words2card.py /README.md: -------------------------------------------------------------------------------- 1 | # Words2Card 2 | 诗词歌词格言生成配图卡片 3 | 4 | ### 实现过程 5 | 我有一个习惯,看到喜欢的句子,或有了特别的感悟,愿意记在手机的备忘录里,想必很多人都会这样。但我收藏的句子主体还是以图片的形式存在了相册里,这让句子们很分散,不方便翻看和分享,这尤其触到了我强迫症的敏感神经。于是我希望有一个类似网易云音乐推出的歌词生成图片分享的工具,来帮我整理我的摘抄。 6 | 7 | 本项目只是在[@Urinx](https://github.com/Urinx)的[NeteaseLyric](https://github.com/Urinx/NeteaseLyric)上的改进。实现过程可以参考我的博客[Python文字转图片-诗词歌词格言生成配图卡片](https://blog.csdn.net/shanchenglang/article/details/104213673)。 8 | 9 | ### 使用说明 10 | 请先安装 PIL 等第三方包依赖,然后下载本工程,工程结构说明: 11 | ``` 12 | │ data.txt # json数据文件 13 | │ words2card.py # 核心程序 14 | │ 15 | ├─image # 储存供选择的封面图片 16 | │ 1.jpg 17 | │ 2.jpg 18 | │ 3.png 19 | │ 20 | ├─output # 输出制作的卡片的位置 21 | │ 森见登美彦1581075253.png 22 | │ 23 | └─static # 制作卡片时使用的静态资源 24 | netease_icon.png # 图标文件 25 | STHeiti_Light.ttc # 字体文件 26 | ``` 27 | 然后将自己的封面图片放在`image/`路径下,并编写`json`数据文件。样例如下: 28 | ``` 29 | [{ 30 | "author": "森见登美彦", 31 | "quotes": "在世界蔓延滋生的‘烦恼’大致可分为两种:一是无关紧要的事,二是无能为力的事。两者同样都只是折磨自己。如果是努力就能解决的事,与其烦恼不如好好努力;若是努力也无法解决的事,那么付出再多也只是白费力气。", 32 | "image": "" 33 | }] 34 | ``` 35 | `json`格式要求一个`author`关键字标识作者或标题,一个`quotes`关键字标识引用内容,一个`image`关键字标识图片地址。注意这里的图片地址既可以是本地的相对地址,也可以是网络连接地址。`author`缺省时默认为`无名氏`,`image`缺省时默认在`./image/`随机选择一幅`jpg`或`png`图片。 36 | 37 | 之后直接运行`words2card.py`即可,结果将在`output/`下生成: 38 | ![alt](https://github.com/ThomasAtlantis/Words2Card/blob/master/output/%E6%A3%AE%E8%A7%81%E7%99%BB%E7%BE%8E%E5%BD%A61581077406.png) 39 | -------------------------------------------------------------------------------- /data.txt: -------------------------------------------------------------------------------- 1 | [{ 2 | "author": "《东北大学校歌》", 3 | "quotes": "唯知行合一方为责,无取乎空论之涛涛\n唯积学养气可致用,无取乎狂热之呼号\n唯深沉厚重成大道,无取乎锋芒之才辩", 4 | "image": "" 5 | },{ 6 | "author": "郭军老师", 7 | "quotes": "一等人才是聪明才辩\n二等人才是磊落豪雄\n三等人才是沉稳厚重", 8 | "image": "" 9 | },{ 10 | "author": "许晶晶《残叶篇》", 11 | "quotes": "辗转秋风动客思,浮生一梦几多时?\n昔年繁茂云霄志,尽日匆忙凌乱诗。\n薄袖寒衫无凭借,清霜素月可相知?\n他年化作春泥后,不见人间绿满枝。", 12 | "image": "" 13 | },{ 14 | "author": "李白《清平乐·画堂晨起》", 15 | "quotes": "画堂晨起,来报雪花坠。高卷帘栊看佳瑞,皓色远迷庭砌。\n盛气光引炉烟,素草寒生玉佩。应是天仙狂醉,乱把白云揉碎。", 16 | "image": "" 17 | },{ 18 | "author": "《锁麟囊》", 19 | "quotes": "他教我,收余恨、免娇嗔,且自新、改性情,休恋逝水,苦海回身,早悟兰因。", 20 | "image": "" 21 | },{ 22 | "author": "我的感慨", 23 | "quotes": "人不能为了娱乐而娱乐,否则得到的只能是空虚", 24 | "image": "" 25 | },{ 26 | "author": "苏轼《行香子》", 27 | "quotes": "清夜无尘,月色如银。\n酒斟时、须满十分。\n浮名浮利,虚苦劳神。\n叹隙中驹,石中火,梦中身。\n虽抱文章,开口谁亲。\n且陶陶、乐尽天真。\n几时归去,作个闲人。 \n对一张琴,一壶酒,一溪云。", 28 | "image": "" 29 | },{ 30 | "author": "", 31 | "quotes": "人和人之间想要保持长久舒适的关系,靠的是共性和吸引,而不是压迫、捆绑、奉承和一味的付出,以及道德式的自我感动。", 32 | "image": "" 33 | },{ 34 | "author": "森见登美彦", 35 | "quotes": "在世界蔓延滋生的‘烦恼’大致可分为两种:一是无关紧要的事,二是无能为力的事。两者同样都只是折磨自己。如果是努力就能解决的事,与其烦恼不如好好努力;若是努力也无法解决的事,那么付出再多也只是白费力气。", 36 | "image": "" 37 | },{ 38 | "author": "", 39 | "quotes": "一个人最好的状态莫过于,眼里写满了故事,脸上却不见风霜。不羡慕谁,不嘲笑谁,也不依赖谁,只是悄悄地努力,活成自己喜欢的样子。", 40 | "image": "http://gss0.baidu.com/-vo3dSag_xI4khGko9WTAnF6hhy/zhidao/pic/item/48540923dd54564ec7609c0cb2de9c82d1584ff9.jpg" 41 | },{ 42 | "author": "《了不起的盖茨比》", 43 | "quotes": "当你想要批评别人的时候,要记住,这世上并不是所有人都有你拥有的那些优势。", 44 | "image": "" 45 | }] -------------------------------------------------------------------------------- /image/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasAtlantis/Words2Card/e5d48aecb6ef075bfbfc35ff313cd54b82ff6462/image/1.jpg -------------------------------------------------------------------------------- /image/10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasAtlantis/Words2Card/e5d48aecb6ef075bfbfc35ff313cd54b82ff6462/image/10.jpg -------------------------------------------------------------------------------- /image/11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasAtlantis/Words2Card/e5d48aecb6ef075bfbfc35ff313cd54b82ff6462/image/11.jpg -------------------------------------------------------------------------------- /image/12.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasAtlantis/Words2Card/e5d48aecb6ef075bfbfc35ff313cd54b82ff6462/image/12.jpg -------------------------------------------------------------------------------- /image/13.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasAtlantis/Words2Card/e5d48aecb6ef075bfbfc35ff313cd54b82ff6462/image/13.jpg -------------------------------------------------------------------------------- /image/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasAtlantis/Words2Card/e5d48aecb6ef075bfbfc35ff313cd54b82ff6462/image/2.jpg -------------------------------------------------------------------------------- /image/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasAtlantis/Words2Card/e5d48aecb6ef075bfbfc35ff313cd54b82ff6462/image/3.jpg -------------------------------------------------------------------------------- /image/4.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasAtlantis/Words2Card/e5d48aecb6ef075bfbfc35ff313cd54b82ff6462/image/4.jpeg -------------------------------------------------------------------------------- /image/5.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasAtlantis/Words2Card/e5d48aecb6ef075bfbfc35ff313cd54b82ff6462/image/5.jpeg -------------------------------------------------------------------------------- /image/6.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasAtlantis/Words2Card/e5d48aecb6ef075bfbfc35ff313cd54b82ff6462/image/6.jpeg -------------------------------------------------------------------------------- /image/7.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasAtlantis/Words2Card/e5d48aecb6ef075bfbfc35ff313cd54b82ff6462/image/7.jpeg -------------------------------------------------------------------------------- /image/8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasAtlantis/Words2Card/e5d48aecb6ef075bfbfc35ff313cd54b82ff6462/image/8.jpg -------------------------------------------------------------------------------- /image/9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasAtlantis/Words2Card/e5d48aecb6ef075bfbfc35ff313cd54b82ff6462/image/9.jpg -------------------------------------------------------------------------------- /netease_lyric.py: -------------------------------------------------------------------------------- 1 | # /usr/bin/env python 2 | # coding: utf-8 3 | #------------------------------------------ 4 | import json 5 | import requests 6 | import re 7 | import os 8 | from io import BytesIO 9 | from PIL import Image, ImageDraw, ImageFont 10 | import optparse 11 | import random 12 | #------------------------------------------ 13 | 14 | class NetEase(): 15 | 16 | cookies_filename = "netease_cookies.json" 17 | 18 | def __init__(self): 19 | self.headers = { 20 | 'Host': 'music.163.com', 21 | 'Connection': 'keep-alive', 22 | 'Content-Type': "application/x-www-form-urlencoded; charset=UTF-8", 23 | 'Referer': 'http://music.163.com/', 24 | "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36" 25 | " (KHTML, like Gecko) Chrome/33.0.1750.152 Safari/537.36" 26 | } 27 | self.cookies = dict(appver="1.2.1", os="osx") 28 | 29 | def show_progress(self, response): 30 | content = bytes() 31 | total_size = response.headers.get('content-length') 32 | if total_size is None: 33 | content = response.content 34 | return content 35 | else: 36 | total_size = int(total_size) 37 | bytes_so_far = 0 38 | 39 | for chunk in response.iter_content(): 40 | content += chunk 41 | bytes_so_far += len(chunk) 42 | progress = round(bytes_so_far * 1.0 / total_size * 100) 43 | self.signal_load_progress.emit(progress) 44 | return content 45 | 46 | def http_request(self, method, action, query=None, urlencoded=None, callback=None, timeout=1): 47 | headers={ 48 | 'Host': 'music.163.com', 49 | 'Connection': 'keep-alive', 50 | 'Content-Type': "application/x-www-form-urlencoded; charset=UTF-8", 51 | 'Referer': 'http://music.163.com/', 52 | "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36" 53 | " (KHTML, like Gecko) Chrome/33.0.1750.152 Safari/537.36" 54 | } 55 | cookies = dict(appver="1.2.1", os="osx") 56 | res = None 57 | if method == "GET": 58 | res = requests.get(action, headers=headers, cookies=cookies, timeout=timeout) 59 | elif method == "POST": 60 | res = requests.post(action, query, headers=self.headers, cookies=self.cookies, timeout=timeout) 61 | elif method == "POST_UPDATE": 62 | res = requests.post(action, query, headers=self.headers, cookies=self.cookies, timeout=timeout) 63 | self.cookies.update(res.cookies.get_dict()) 64 | self.save_cookies() 65 | content = self.show_progress(res) 66 | content_str = content.decode('utf-8') 67 | content_dict = json.loads(content_str) 68 | return content_dict 69 | 70 | def user_playlist(self, pid): 71 | action = 'http://music.163.com/api/playlist/detail?id=' + str(pid) 72 | res_data = self.http_request('GET', action) 73 | return res_data 74 | 75 | def song_detail(self, sid): 76 | action = 'http://music.163.com/api/song/detail?ids=%5B' + str(sid) + '%5D' 77 | res_data = self.http_request('GET', action) 78 | return res_data['songs'][0] 79 | 80 | def get_lyric_by_musicid(self, mid): 81 | # 此API必须使用POST方式才能正确返回,否则提示“illegal request” 82 | url = 'http://music.163.com/api/song/lyric?id=' + str(mid) + '&lv=1&kv=1&tv=-1' 83 | return self.http_request('POST', url) 84 | 85 | def clean_lyric(self, lrc): 86 | r = [] 87 | is_empty = False 88 | for line in lrc.strip().split('\n'): 89 | line = line.strip() 90 | if not is_empty: 91 | r.append(line) 92 | if line == '': 93 | is_empty = True 94 | else: 95 | if line != '': 96 | r.append(line) 97 | is_empty = False 98 | return '\n'.join(r) 99 | 100 | 101 | class Playlist(): 102 | def __init__(self, pid): 103 | self.id = pid 104 | self.playlist_name = '' 105 | self.song_name = [] 106 | self.song_id = [] 107 | self.song_img = [] 108 | self.song_lrc = [] 109 | 110 | def get_lrc(self, random_line): 111 | pid = self.id 112 | netease = NetEase() 113 | playlist = netease.user_playlist(pid) 114 | self.playlist_name = playlist['result']['name'] 115 | self.playlist = playlist['result']['tracks'] 116 | print(u'歌单名:《%s》,歌曲数:%d' % (self.playlist_name, len(self.playlist))) 117 | 118 | for song in self.playlist: 119 | self.song_name.append(song["name"]) 120 | self.song_id.append(song["id"]) 121 | self.song_img.append(song["album"]["blurPicUrl"]) 122 | 123 | for sid in self.song_id: 124 | lrc = netease.get_lyric_by_musicid(sid) 125 | if 'lrc' in lrc and 'lyric' in lrc['lrc'] and lrc['lrc']['lyric'] != '': 126 | lrc = lrc['lrc']['lyric'] 127 | pat = re.compile(r'\[.*\]') 128 | lrc = re.sub(pat, "", lrc) 129 | lrc = lrc.strip() 130 | else: 131 | lrc = u'纯音乐,无歌词' 132 | 133 | lrc = netease.clean_lyric(lrc) 134 | if random_line != 0: 135 | lrc_arr = lrc.split('\n') 136 | n = len(lrc_arr) 137 | if n > random_line: 138 | i = random.randint(0, n - random_line) 139 | lrc = '\n'.join(lrc_arr[i:i+random_line]) 140 | 141 | self.song_lrc.append(lrc) 142 | 143 | def create_img(self, pic_style): 144 | img = Img(self.playlist_name + '/') 145 | save_func = None 146 | if pic_style == 1: 147 | save_func = img.save 148 | elif pic_style == 2: 149 | save_func = img.save2 150 | elif pic_style == 3: 151 | save_func = img.save3 152 | 153 | for s_name, s_lrc, s_img in zip(self.song_name, self.song_lrc, self.song_img): 154 | save_func(s_name, s_lrc, s_img) 155 | 156 | 157 | class Song(object): 158 | def __init__(self, uid): 159 | self.id = uid 160 | self.song_lrc = '' 161 | self.song_id = uid 162 | self.song_name = '' 163 | self.song_img = '' 164 | 165 | def get_lrc(self, random_line): 166 | uid = self.id 167 | netease = NetEase() 168 | song = netease.song_detail(uid) 169 | self.song_name = song['name'].strip() 170 | self.song_id = uid 171 | self.song_img = song["album"]["blurPicUrl"] 172 | 173 | lrc = netease.get_lyric_by_musicid(uid) 174 | if 'lrc' in lrc and 'lyric' in lrc['lrc'] and lrc['lrc']['lyric'] != '': 175 | lrc = lrc['lrc']['lyric'] 176 | pat = re.compile(r'\[.*\]') 177 | lrc = re.sub(pat, "", lrc) 178 | lrc = lrc.strip() 179 | else: 180 | lrc = u'纯音乐,无歌词' 181 | self.song_lrc = netease.clean_lyric(lrc) 182 | 183 | if random_line != 0: 184 | lrc = self.song_lrc.split('\n') 185 | n = len(lrc) 186 | if n > random_line: 187 | i = random.randint(0, n - random_line) 188 | self.song_lrc = '\n'.join(lrc[i:i+random_line]) 189 | 190 | def show_lrc(self): 191 | lrcs = self.song_lrc.split('\n') 192 | n = len(lrcs) 193 | for i in range(n): 194 | print(('%'+str(len(str(n)))+'d %s') % (i+1, lrcs[i])) 195 | 196 | def create_img(self, pic_style): 197 | if pic_style == 1: 198 | Img().save(self.song_name, self.song_lrc, self.song_img) 199 | elif pic_style == 2: 200 | Img().save2(self.song_name, self.song_lrc, self.song_img) 201 | elif pic_style == 3: 202 | Img().save3(self.song_name, self.song_lrc, self.song_img) 203 | 204 | 205 | class Img(): 206 | 207 | def __init__(self, save_dir=None): 208 | self.save_dir = save_dir 209 | 210 | self.font_family = 'res/STHeiti_Light.ttc' 211 | self.font_size = 30 # 字体大小 212 | self.line_space = 30 # 行间隔大小 213 | self.word_space = 5 214 | self.share_img_width = 640 215 | self.padding = 50 216 | self.song_name_space = 50 217 | self.banner_space = 60 218 | self.text_color = '#767676' 219 | self.netease_banner = u'来自网易云音乐•歌词分享' 220 | self.netease_banner_color = '#D3D7D9' 221 | self.netease_banner_size = 20 222 | self.netease_icon = 'res/netease_icon.png' 223 | self.icon_width = 25 224 | 225 | self.style2_margin = 50 226 | self.style2_padding = 30 227 | self.style2_line_width = 2 228 | self.style2_quote_width = 30 229 | self.quote_icon = 'res/quote.png' 230 | 231 | if self.save_dir is not None: 232 | try: 233 | os.mkdir(self.save_dir) 234 | except: 235 | pass 236 | 237 | def save(self, name, lrc, img_url): 238 | lyric_font = ImageFont.truetype(self.font_family, self.font_size) 239 | banner_font = ImageFont.truetype(self.font_family, self.netease_banner_size) 240 | lyric_w, lyric_h = ImageDraw.Draw(Image.new(mode='RGB', size=(1, 1))).textsize(lrc, font=lyric_font, spacing=self.line_space) 241 | 242 | padding = self.padding 243 | w = self.share_img_width 244 | 245 | album_img = None 246 | if img_url.startswith('http'): 247 | raw_img = requests.get(img_url) 248 | album_img = Image.open(BytesIO(raw_img.content)) 249 | else: 250 | album_img = Image.open(img_url) 251 | 252 | iw, ih = album_img.size 253 | album_h = ih * w // iw 254 | 255 | h = album_h + padding + lyric_h + self.song_name_space + \ 256 | self.font_size + self.banner_space + self.netease_banner_size + padding 257 | 258 | resized_album = album_img.resize((w, album_h), resample=3) 259 | icon = Image.open(self.netease_icon).resize((self.icon_width, self.icon_width), resample=3) 260 | 261 | out_img = Image.new(mode='RGB', size=(w, h), color=(255, 255, 255)) 262 | draw = ImageDraw.Draw(out_img) 263 | 264 | # 添加封面 265 | out_img.paste(resized_album, (0, 0)) 266 | 267 | # 添加文字 268 | draw.text((padding, album_h + padding), lrc, font=lyric_font, fill=self.text_color, spacing=self.line_space) 269 | 270 | # Python中字符串类型分为byte string 和 unicode string两种,'——'为中文标点byte string,需转换为unicode string 271 | y_song_name = album_h + padding + lyric_h + self.song_name_space 272 | # song_name = unicode('—— 「', "utf-8") + name + unicode('」', "utf-8") 273 | song_name = u'—— 「' + name + u'」' 274 | sw, sh = draw.textsize(song_name, font=lyric_font) 275 | draw.text((w - padding - sw, y_song_name), song_name, font=lyric_font, fill=self.text_color) 276 | 277 | # 添加网易标签 278 | y_netease_banner = h - padding - self.netease_banner_size 279 | out_img.paste(icon, (padding, y_netease_banner - 2)) 280 | draw.text((padding + self.icon_width + 5, y_netease_banner), self.netease_banner, font=banner_font, fill=self.netease_banner_color) 281 | 282 | img_save_path = '' 283 | if self.save_dir is not None: 284 | img_save_path = self.save_dir 285 | out_img.save(img_save_path + name + '.png') 286 | 287 | def save2(self, name, lrc, img_url): 288 | lyric_font = ImageFont.truetype(self.font_family, self.font_size) 289 | banner_font = ImageFont.truetype(self.font_family, self.netease_banner_size) 290 | lyric_w, lyric_h = ImageDraw.Draw(Image.new(mode='RGB', size=(1, 1))).textsize(lrc, font=lyric_font, spacing=self.line_space) 291 | 292 | margin = self.style2_margin 293 | padding = self.style2_padding 294 | w = self.share_img_width 295 | h = margin + padding + lyric_h + self.song_name_space + \ 296 | self.font_size + self.banner_space + self.netease_banner_size + padding + margin 297 | 298 | icon = Image.open(self.netease_icon).resize((self.icon_width, self.icon_width), resample=3) 299 | quote = Image.open(self.quote_icon).resize((self.style2_quote_width, self.style2_quote_width), resample=3) 300 | 301 | out_img = Image.new(mode='RGB', size=(w, h), color=(255, 255, 255)) 302 | draw = ImageDraw.Draw(out_img) 303 | 304 | def draw_rectangle(draw, rect, width): 305 | for i in range(width): 306 | draw.rectangle((rect[0] + i, rect[1] + i, rect[2] - i, rect[3] - i), outline=self.netease_banner_color) 307 | 308 | 309 | # 画边框 310 | rect_h = padding + lyric_h + self.song_name_space + self.font_size + self.banner_space 311 | draw_rectangle(draw, (margin, margin, w - margin, margin + rect_h ), 2) 312 | out_img.paste(quote, (margin - self.style2_quote_width // 2, margin + self.style2_quote_width // 2)) 313 | quote = quote.rotate(180) 314 | out_img.paste(quote, (w - margin - self.style2_quote_width // 2, margin + rect_h - self.style2_quote_width - self.style2_quote_width // 2)) 315 | 316 | # 添加文字 317 | draw.text((margin + padding, margin + padding), lrc, font=lyric_font, fill=self.text_color, spacing=self.line_space) 318 | 319 | y_song_name = margin + padding + lyric_h + self.song_name_space 320 | # song_name = unicode('—— 「', "utf-8") + name + unicode('」', "utf-8") 321 | song_name = u'—— 「' + name + u'」' 322 | sw, sh = draw.textsize(song_name, font=lyric_font) 323 | draw.text((w - margin - padding - sw, y_song_name), song_name, font=lyric_font, fill=self.text_color) 324 | 325 | # 添加网易标签 326 | y_netease_banner = h - padding - self.netease_banner_size 327 | out_img.paste(icon, (margin, y_netease_banner - 2)) 328 | draw.text((margin + self.icon_width + 5, y_netease_banner), self.netease_banner, font=banner_font, fill=self.netease_banner_color) 329 | 330 | img_save_path = '' 331 | if self.save_dir is not None: 332 | img_save_path = self.save_dir 333 | out_img.save(img_save_path + name + '.png') 334 | 335 | def save3(self, name, lrc, img_url): 336 | lyric_font = ImageFont.truetype(self.font_family, self.font_size) 337 | banner_font = ImageFont.truetype(self.font_family, self.netease_banner_size) 338 | lyric_w, lyric_h = ImageDraw.Draw(Image.new(mode='RGB', size=(1, 1))).textsize(lrc, font=lyric_font, spacing=self.line_space) 339 | 340 | margin = self.style2_margin 341 | padding = self.style2_padding 342 | w = self.share_img_width 343 | 344 | album_img = None 345 | if img_url.startswith('http'): 346 | raw_img = requests.get(img_url) 347 | album_img = Image.open(BytesIO(raw_img.content)) 348 | else: 349 | album_img = Image.open(img_url) 350 | 351 | iw, ih = album_img.size 352 | album_h = ih * w // iw 353 | 354 | h = album_h + margin + padding + lyric_h + self.song_name_space + \ 355 | self.font_size + self.banner_space + self.netease_banner_size + padding + margin 356 | 357 | resized_album = album_img.resize((w, album_h), resample=3) 358 | icon = Image.open(self.netease_icon).resize((self.icon_width, self.icon_width), resample=3) 359 | quote = Image.open(self.quote_icon).resize((self.style2_quote_width, self.style2_quote_width), resample=3) 360 | 361 | out_img = Image.new(mode='RGB', size=(w, h), color=(255, 255, 255)) 362 | draw = ImageDraw.Draw(out_img) 363 | 364 | def draw_rectangle(draw, rect, width): 365 | for i in range(width): 366 | draw.rectangle((rect[0] + i, rect[1] + i, rect[2] - i, rect[3] - i), outline=self.netease_banner_color) 367 | 368 | # 添加封面 369 | out_img.paste(resized_album, (0, 0)) 370 | 371 | # 画边框 372 | rect_h = padding + lyric_h + self.song_name_space + self.font_size + self.banner_space 373 | draw_rectangle(draw, (margin, album_h + margin, w - margin, album_h + margin + rect_h ), 2) 374 | out_img.paste(quote, (margin - self.style2_quote_width // 2, album_h + margin + self.style2_quote_width // 2)) 375 | quote = quote.rotate(180) 376 | out_img.paste(quote, (w - margin - self.style2_quote_width // 2, album_h + margin + rect_h - self.style2_quote_width - self.style2_quote_width // 2)) 377 | 378 | # 添加文字 379 | draw.text((margin + padding, album_h + margin + padding), lrc, font=lyric_font, fill=self.text_color, spacing=self.line_space) 380 | 381 | y_song_name = album_h + margin + padding + lyric_h + self.song_name_space 382 | # song_name = unicode('—— 「', "utf-8") + name + unicode('」', "utf-8") 383 | song_name = u'—— 「' + name + u'」' 384 | sw, sh = draw.textsize(song_name, font=lyric_font) 385 | draw.text((w - margin - padding - sw, y_song_name), song_name, font=lyric_font, fill=self.text_color) 386 | 387 | # 添加网易标签 388 | y_netease_banner = h - padding - self.netease_banner_size 389 | out_img.paste(icon, (margin, y_netease_banner - 2)) 390 | draw.text((margin + self.icon_width + 5, y_netease_banner), self.netease_banner, font=banner_font, fill=self.netease_banner_color) 391 | 392 | img_save_path = '' 393 | if self.save_dir is not None: 394 | img_save_path = self.save_dir 395 | out_img.save(img_save_path + name + '.png') 396 | 397 | def unicode_str(s): 398 | try: 399 | t = unicode(s, 'utf-8') 400 | return t 401 | except: 402 | return s 403 | 404 | def main(): 405 | parser = optparse.OptionParser('usage: [--sid | --pid ] -t -r \n\t-i -w -n -l -p ') 406 | parser.add_option('--sid', dest='sid', type='string', help='song id') 407 | parser.add_option('--pid', dest='pid', type='string', help='playlist id') 408 | parser.add_option('-t', dest='pic_style', type='int', help='1: has album image, 2: lyric only, 3: combine 1 & 2') 409 | parser.add_option('-r', dest='random_line', type='int', help='number of random lyric lines') 410 | parser.add_option('-p', dest='line_range', type='string', help='range of lyric lines') 411 | parser.add_option('-i', dest='img_file', type='string', help='your own image') 412 | parser.add_option('-w', dest='text', type='string', help='some text') 413 | parser.add_option('-n', dest='name', type='string', help='name') 414 | parser.add_option('-l', dest="show_lyrics", action="store_true", default=False, 415 | help="only show the lyrics with line number to stdout") 416 | (options, args) = parser.parse_args() 417 | sid = options.sid 418 | pid = options.pid 419 | pic_style = options.pic_style 420 | line_range = options.line_range 421 | random_line = options.random_line 422 | img_file = options.img_file 423 | text = options.text 424 | name = options.name 425 | 426 | if pic_style is None: 427 | pic_style = 1 428 | if random_line is None: 429 | random_line = 0 430 | 431 | if sid is not None: 432 | song = Song(sid) 433 | song.get_lrc(random_line) 434 | 435 | if options.show_lyrics: 436 | song.show_lrc() 437 | else: 438 | if line_range is not None: 439 | lrcs = song.song_lrc.split('\n') 440 | tmp_lrcs = [] 441 | for i in line_range.split(','): 442 | if '-' in i: 443 | a, b = i.split('-') 444 | tmp_lrcs += lrcs[int(a)-1:int(b)] 445 | else: 446 | tmp_lrcs.append(lrcs[int(i)-1]) 447 | song.song_lrc = '\n'.join(tmp_lrcs) 448 | 449 | song.create_img(pic_style) 450 | 451 | elif pid is not None: 452 | playlist = Playlist(pid) 453 | playlist.get_lrc(random_line) 454 | playlist.create_img(pic_style) 455 | elif text is not None: 456 | text = text.replace('\\n','\n') 457 | text = unicode_str(text) 458 | 459 | if img_file is None: 460 | img_file = 'res/dog.png' 461 | if name is None: 462 | name = u'Anonymous' 463 | else: 464 | name = unicode_str(name) 465 | 466 | save_func = None 467 | if pic_style == 1: 468 | save_func = Img().save 469 | elif pic_style == 2: 470 | save_func = Img().save2 471 | elif pic_style == 3: 472 | save_func = Img().save3 473 | save_func(name, text, img_file) 474 | 475 | if __name__ == '__main__': 476 | main() 477 | -------------------------------------------------------------------------------- /output/《东北大学校歌》1581077400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasAtlantis/Words2Card/e5d48aecb6ef075bfbfc35ff313cd54b82ff6462/output/《东北大学校歌》1581077400.png -------------------------------------------------------------------------------- /output/《了不起的盖茨比》1581077407.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasAtlantis/Words2Card/e5d48aecb6ef075bfbfc35ff313cd54b82ff6462/output/《了不起的盖茨比》1581077407.png -------------------------------------------------------------------------------- /output/《锁麟囊》1581077403.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasAtlantis/Words2Card/e5d48aecb6ef075bfbfc35ff313cd54b82ff6462/output/《锁麟囊》1581077403.png -------------------------------------------------------------------------------- /output/我的感慨1581077404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasAtlantis/Words2Card/e5d48aecb6ef075bfbfc35ff313cd54b82ff6462/output/我的感慨1581077404.png -------------------------------------------------------------------------------- /output/无名氏1581077405.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasAtlantis/Words2Card/e5d48aecb6ef075bfbfc35ff313cd54b82ff6462/output/无名氏1581077405.png -------------------------------------------------------------------------------- /output/无名氏1581077406.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasAtlantis/Words2Card/e5d48aecb6ef075bfbfc35ff313cd54b82ff6462/output/无名氏1581077406.png -------------------------------------------------------------------------------- /output/李白《清平乐·画堂晨起》1581077402.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasAtlantis/Words2Card/e5d48aecb6ef075bfbfc35ff313cd54b82ff6462/output/李白《清平乐·画堂晨起》1581077402.png -------------------------------------------------------------------------------- /output/森见登美彦1581075253.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasAtlantis/Words2Card/e5d48aecb6ef075bfbfc35ff313cd54b82ff6462/output/森见登美彦1581075253.png -------------------------------------------------------------------------------- /output/森见登美彦1581077406.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasAtlantis/Words2Card/e5d48aecb6ef075bfbfc35ff313cd54b82ff6462/output/森见登美彦1581077406.png -------------------------------------------------------------------------------- /output/苏轼《行香子》1581077404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasAtlantis/Words2Card/e5d48aecb6ef075bfbfc35ff313cd54b82ff6462/output/苏轼《行香子》1581077404.png -------------------------------------------------------------------------------- /output/许晶晶《残叶篇》1581077401.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasAtlantis/Words2Card/e5d48aecb6ef075bfbfc35ff313cd54b82ff6462/output/许晶晶《残叶篇》1581077401.png -------------------------------------------------------------------------------- /output/郭军老师1581077401.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasAtlantis/Words2Card/e5d48aecb6ef075bfbfc35ff313cd54b82ff6462/output/郭军老师1581077401.png -------------------------------------------------------------------------------- /static/STHeiti_Light.ttc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasAtlantis/Words2Card/e5d48aecb6ef075bfbfc35ff313cd54b82ff6462/static/STHeiti_Light.ttc -------------------------------------------------------------------------------- /static/netease_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasAtlantis/Words2Card/e5d48aecb6ef075bfbfc35ff313cd54b82ff6462/static/netease_icon.png -------------------------------------------------------------------------------- /words2card.py: -------------------------------------------------------------------------------- 1 | from PIL import Image, ImageDraw, ImageFont 2 | import json, os, random, time, glob, requests 3 | from io import BytesIO 4 | 5 | class Img(): 6 | 7 | def __init__(self, save_dir=None): 8 | self.save_dir = save_dir 9 | self.font_family = 'static/STHeiti_Light.ttc' 10 | self.font_size = 30 # 字体大小 11 | self.line_space = 30 # 行间隔大小 12 | self.share_img_width = 640 13 | self.padding = 50 14 | self.song_name_space = 50 15 | self.banner_space = 60 16 | self.text_color = '#767676' 17 | self.netease_banner = u'来自我的摘抄' 18 | self.netease_banner_color = '#D3D7D9' 19 | self.netease_banner_size = 20 20 | self.netease_icon = 'static/netease_icon.png' 21 | self.icon_width = 25 22 | if self.save_dir is not None: 23 | try: 24 | os.mkdir(self.save_dir) 25 | except: 26 | pass 27 | self.chars_width = {} 28 | self.chars = [ 29 | '。', ',', '、', ':', '?', '(', ')', '【', '】', '《', '》', '’', '‘', '“', '”', '!', '~', '—', '…', ';', 30 | '.', ',', '\\', ':', '?', '(', ')', '[', ']', '<', '>', '\'', '\"', '!', '-', '_', '+', '-', '*', '/', 31 | '&', '%', '^', '$', '¥', '#', '@', '`', '·', ' ' 32 | ] 33 | for i in range(ord('0'), ord('9') + 1): 34 | self.chars.append(chr(i)) 35 | for i in range(ord('a'), ord('z') + 1): 36 | self.chars.append(chr(i)) 37 | for i in range(ord('A'), ord('Z') + 1): 38 | self.chars.append(chr(i)) 39 | 40 | for char in self.chars: 41 | self.chars_width[char], _ = ImageDraw.Draw(Image.new(mode='RGB', size=(1, 1))).textsize( 42 | char, font=ImageFont.truetype(self.font_family, self.font_size), spacing=self.font_size) 43 | 44 | def save(self, name, lrc, img_url): 45 | lyric_font = ImageFont.truetype(self.font_family, self.font_size) 46 | banner_font = ImageFont.truetype(self.font_family, self.netease_banner_size) 47 | 48 | padding = self.padding 49 | w = self.share_img_width 50 | 51 | album_img = None 52 | if img_url.startswith('http'): 53 | raw_img = requests.get(img_url) 54 | album_img = Image.open(BytesIO(raw_img.content)) 55 | else: 56 | album_img = Image.open(img_url) 57 | 58 | iw, ih = album_img.size 59 | album_h = ih * w // iw 60 | 61 | lrc_revised = "" 62 | for line in lrc.split('\n'): 63 | line_list = list(line) 64 | x = k = delta_x = 0 65 | for index in range(len(line)): 66 | delta_x = self.chars_width.get(line[index], self.font_size) 67 | x += delta_x 68 | if x > w - padding * 2: 69 | if line[index] in self.chars[:50]: 70 | continue 71 | line_list.insert(index + k, '\n') 72 | x = delta_x 73 | k += 1 74 | lrc_revised += ''.join(line_list) + '\n' 75 | lrc = lrc_revised 76 | 77 | lyric_w, lyric_h = ImageDraw.Draw(Image.new(mode='RGB', size=(1, 1))).textsize(lrc, font=lyric_font, spacing=self.line_space) 78 | 79 | h = album_h + padding + lyric_h + self.song_name_space + \ 80 | self.font_size + self.banner_space + self.netease_banner_size + padding 81 | 82 | resized_album = album_img.resize((w, album_h), resample=3) 83 | icon = Image.open(self.netease_icon).resize((self.icon_width, self.icon_width), resample=3) 84 | 85 | out_img = Image.new(mode='RGB', size=(w, h), color=(255, 255, 255)) 86 | draw = ImageDraw.Draw(out_img) 87 | 88 | # 添加封面 89 | out_img.paste(resized_album, (0, 0)) 90 | 91 | # 添加文字 92 | draw.text((padding, album_h + padding), lrc, font=lyric_font, fill=self.text_color, spacing=self.line_space) 93 | 94 | # Python中字符串类型分为byte string 和 unicode string两种,'——'为中文标点byte string,需转换为unicode string 95 | y_song_name = album_h + padding + lyric_h + self.song_name_space 96 | # song_name = unicode('—— 「', "utf-8") + name + unicode('」', "utf-8") 97 | song_name = u'—— 「' + name + u'」' 98 | sw, sh = draw.textsize(song_name, font=lyric_font) 99 | draw.text((w - padding - sw, y_song_name), song_name, font=lyric_font, fill=self.text_color) 100 | 101 | # 添加网易标签 102 | y_netease_banner = h - padding - self.netease_banner_size 103 | out_img.paste(icon, (padding, y_netease_banner - 2)) 104 | draw.text((padding + self.icon_width + 5, y_netease_banner), self.netease_banner, font=banner_font, fill=self.netease_banner_color) 105 | 106 | img_save_path = '' 107 | if self.save_dir is not None: 108 | img_save_path = self.save_dir 109 | out_img.save(img_save_path + '/' + name + str(int(time.time())) + '.png') 110 | 111 | def main(): 112 | generater = Img("output") 113 | images = glob.glob("image/*.jpg") + glob.glob("image/*.png") 114 | with open('data.txt', 'r', encoding='utf-8') as reader: 115 | data = json.loads(reader.read()) 116 | for item in data: 117 | image = random.choice(images) 118 | if item['image']: 119 | image = item['image'] 120 | if not item['image'].startswith("http"): 121 | image = "image/" + image 122 | author = item['author'] if item['author'] else "无名氏" 123 | print("Generating %s's words ... " % author, end="") 124 | generater.save(author, item['quotes'], image) 125 | print("done!") 126 | 127 | if __name__ == '__main__': 128 | main() --------------------------------------------------------------------------------