├── .gitignore
├── LICENSE
├── README.md
├── fonts
├── EmblemaOne.woff2
├── josefinsans.woff2
├── lilitaone.woff2
├── multi_1_en.otf
├── multi_1_zh.ttf
├── phosphate.ttf
└── wendao.ttf
├── icons
├── douban.png
├── emby.png
└── speed.png
├── images
├── multi_1.jpg
├── plugin.webp
├── single_1.jpg
└── single_2.jpg
├── package.v2.json
└── plugins.v2
└── mediacovergenerator
├── __init__.py
├── requirements.txt
├── static
├── multi_1.py
├── single_1.py
└── single_2.py
├── style_multi_1.py
├── style_single_1.py
└── style_single_2.py
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | share/python-wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 | MANIFEST
28 |
29 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | pip-log.txt
37 | pip-delete-this-directory.txt
38 |
39 | # Unit test / coverage reports
40 | htmlcov/
41 | .tox/
42 | .nox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *.cover
49 | *.py,cover
50 | .hypothesis/
51 | .pytest_cache/
52 | cover/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | .pybuilder/
76 | target/
77 |
78 | # Jupyter Notebook
79 | .ipynb_checkpoints
80 |
81 | # IPython
82 | profile_default/
83 | ipython_config.py
84 |
85 | # pyenv
86 | # For a library or package, you might want to ignore these files since the code is
87 | # intended to run in multiple environments; otherwise, check them in:
88 | # .python-version
89 |
90 | # pipenv
91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
94 | # install all needed dependencies.
95 | #Pipfile.lock
96 |
97 | # poetry
98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99 | # This is especially recommended for binary packages to ensure reproducibility, and is more
100 | # commonly ignored for libraries.
101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102 | #poetry.lock
103 |
104 | # pdm
105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
106 | #pdm.lock
107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
108 | # in version control.
109 | # https://pdm.fming.dev/#use-with-ide
110 | .pdm.toml
111 |
112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
113 | __pypackages__/
114 |
115 | # Celery stuff
116 | celerybeat-schedule
117 | celerybeat.pid
118 |
119 | # SageMath parsed files
120 | *.sage.py
121 |
122 | # Environments
123 | .env
124 | .venv
125 | env/
126 | venv/
127 | ENV/
128 | env.bak/
129 | venv.bak/
130 |
131 | # Spyder project settings
132 | .spyderproject
133 | .spyproject
134 |
135 | # Rope project settings
136 | .ropeproject
137 |
138 | # mkdocs documentation
139 | /site
140 |
141 | # mypy
142 | .mypy_cache/
143 | .dmypy.json
144 | dmypy.json
145 |
146 | # Pyre type checker
147 | .pyre/
148 |
149 | # pytype static type analyzer
150 | .pytype/
151 |
152 | # Cython debug symbols
153 | cython_debug/
154 |
155 | # PyCharm
156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
158 | # and can be added to the global gitignore or merged into this file. For a more nuclear
159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
160 | #.idea/
161 |
162 | .idea/
163 | .vscode/
164 | .DS_Store
165 | plugins.v2/.DS_Store
166 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU General Public License is a free, copyleft license for
11 | software and other kinds of works.
12 |
13 | The licenses for most software and other practical works are designed
14 | to take away your freedom to share and change the works. By contrast,
15 | the GNU General Public License is intended to guarantee your freedom to
16 | share and change all versions of a program--to make sure it remains free
17 | software for all its users. We, the Free Software Foundation, use the
18 | GNU General Public License for most of our software; it applies also to
19 | any other work released this way by its authors. You can apply it to
20 | your programs, too.
21 |
22 | When we speak of free software, we are referring to freedom, not
23 | price. Our General Public Licenses are designed to make sure that you
24 | have the freedom to distribute copies of free software (and charge for
25 | them if you wish), that you receive source code or can get it if you
26 | want it, that you can change the software or use pieces of it in new
27 | free programs, and that you know you can do these things.
28 |
29 | To protect your rights, we need to prevent others from denying you
30 | these rights or asking you to surrender the rights. Therefore, you have
31 | certain responsibilities if you distribute copies of the software, or if
32 | you modify it: responsibilities to respect the freedom of others.
33 |
34 | For example, if you distribute copies of such a program, whether
35 | gratis or for a fee, you must pass on to the recipients the same
36 | freedoms that you received. You must make sure that they, too, receive
37 | or can get the source code. And you must show them these terms so they
38 | know their rights.
39 |
40 | Developers that use the GNU GPL protect your rights with two steps:
41 | (1) assert copyright on the software, and (2) offer you this License
42 | giving you legal permission to copy, distribute and/or modify it.
43 |
44 | For the developers' and authors' protection, the GPL clearly explains
45 | that there is no warranty for this free software. For both users' and
46 | authors' sake, the GPL requires that modified versions be marked as
47 | changed, so that their problems will not be attributed erroneously to
48 | authors of previous versions.
49 |
50 | Some devices are designed to deny users access to install or run
51 | modified versions of the software inside them, although the manufacturer
52 | can do so. This is fundamentally incompatible with the aim of
53 | protecting users' freedom to change the software. The systematic
54 | pattern of such abuse occurs in the area of products for individuals to
55 | use, which is precisely where it is most unacceptable. Therefore, we
56 | have designed this version of the GPL to prohibit the practice for those
57 | products. If such problems arise substantially in other domains, we
58 | stand ready to extend this provision to those domains in future versions
59 | of the GPL, as needed to protect the freedom of users.
60 |
61 | Finally, every program is threatened constantly by software patents.
62 | States should not allow patents to restrict development and use of
63 | software on general-purpose computers, but in those that do, we wish to
64 | avoid the special danger that patents applied to a free program could
65 | make it effectively proprietary. To prevent this, the GPL assures that
66 | patents cannot be used to render the program non-free.
67 |
68 | The precise terms and conditions for copying, distribution and
69 | modification follow.
70 |
71 | TERMS AND CONDITIONS
72 |
73 | 0. Definitions.
74 |
75 | "This License" refers to version 3 of the GNU General Public License.
76 |
77 | "Copyright" also means copyright-like laws that apply to other kinds of
78 | works, such as semiconductor masks.
79 |
80 | "The Program" refers to any copyrightable work licensed under this
81 | License. Each licensee is addressed as "you". "Licensees" and
82 | "recipients" may be individuals or organizations.
83 |
84 | To "modify" a work means to copy from or adapt all or part of the work
85 | in a fashion requiring copyright permission, other than the making of an
86 | exact copy. The resulting work is called a "modified version" of the
87 | earlier work or a work "based on" the earlier work.
88 |
89 | A "covered work" means either the unmodified Program or a work based
90 | on the Program.
91 |
92 | To "propagate" a work means to do anything with it that, without
93 | permission, would make you directly or secondarily liable for
94 | infringement under applicable copyright law, except executing it on a
95 | computer or modifying a private copy. Propagation includes copying,
96 | distribution (with or without modification), making available to the
97 | public, and in some countries other activities as well.
98 |
99 | To "convey" a work means any kind of propagation that enables other
100 | parties to make or receive copies. Mere interaction with a user through
101 | a computer network, with no transfer of a copy, is not conveying.
102 |
103 | An interactive user interface displays "Appropriate Legal Notices"
104 | to the extent that it includes a convenient and prominently visible
105 | feature that (1) displays an appropriate copyright notice, and (2)
106 | tells the user that there is no warranty for the work (except to the
107 | extent that warranties are provided), that licensees may convey the
108 | work under this License, and how to view a copy of this License. If
109 | the interface presents a list of user commands or options, such as a
110 | menu, a prominent item in the list meets this criterion.
111 |
112 | 1. Source Code.
113 |
114 | The "source code" for a work means the preferred form of the work
115 | for making modifications to it. "Object code" means any non-source
116 | form of a work.
117 |
118 | A "Standard Interface" means an interface that either is an official
119 | standard defined by a recognized standards body, or, in the case of
120 | interfaces specified for a particular programming language, one that
121 | is widely used among developers working in that language.
122 |
123 | The "System Libraries" of an executable work include anything, other
124 | than the work as a whole, that (a) is included in the normal form of
125 | packaging a Major Component, but which is not part of that Major
126 | Component, and (b) serves only to enable use of the work with that
127 | Major Component, or to implement a Standard Interface for which an
128 | implementation is available to the public in source code form. A
129 | "Major Component", in this context, means a major essential component
130 | (kernel, window system, and so on) of the specific operating system
131 | (if any) on which the executable work runs, or a compiler used to
132 | produce the work, or an object code interpreter used to run it.
133 |
134 | The "Corresponding Source" for a work in object code form means all
135 | the source code needed to generate, install, and (for an executable
136 | work) run the object code and to modify the work, including scripts to
137 | control those activities. However, it does not include the work's
138 | System Libraries, or general-purpose tools or generally available free
139 | programs which are used unmodified in performing those activities but
140 | which are not part of the work. For example, Corresponding Source
141 | includes interface definition files associated with source files for
142 | the work, and the source code for shared libraries and dynamically
143 | linked subprograms that the work is specifically designed to require,
144 | such as by intimate data communication or control flow between those
145 | subprograms and other parts of the work.
146 |
147 | The Corresponding Source need not include anything that users
148 | can regenerate automatically from other parts of the Corresponding
149 | Source.
150 |
151 | The Corresponding Source for a work in source code form is that
152 | same work.
153 |
154 | 2. Basic Permissions.
155 |
156 | All rights granted under this License are granted for the term of
157 | copyright on the Program, and are irrevocable provided the stated
158 | conditions are met. This License explicitly affirms your unlimited
159 | permission to run the unmodified Program. The output from running a
160 | covered work is covered by this License only if the output, given its
161 | content, constitutes a covered work. This License acknowledges your
162 | rights of fair use or other equivalent, as provided by copyright law.
163 |
164 | You may make, run and propagate covered works that you do not
165 | convey, without conditions so long as your license otherwise remains
166 | in force. You may convey covered works to others for the sole purpose
167 | of having them make modifications exclusively for you, or provide you
168 | with facilities for running those works, provided that you comply with
169 | the terms of this License in conveying all material for which you do
170 | not control copyright. Those thus making or running the covered works
171 | for you must do so exclusively on your behalf, under your direction
172 | and control, on terms that prohibit them from making any copies of
173 | your copyrighted material outside their relationship with you.
174 |
175 | Conveying under any other circumstances is permitted solely under
176 | the conditions stated below. Sublicensing is not allowed; section 10
177 | makes it unnecessary.
178 |
179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
180 |
181 | No covered work shall be deemed part of an effective technological
182 | measure under any applicable law fulfilling obligations under article
183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
184 | similar laws prohibiting or restricting circumvention of such
185 | measures.
186 |
187 | When you convey a covered work, you waive any legal power to forbid
188 | circumvention of technological measures to the extent such circumvention
189 | is effected by exercising rights under this License with respect to
190 | the covered work, and you disclaim any intention to limit operation or
191 | modification of the work as a means of enforcing, against the work's
192 | users, your or third parties' legal rights to forbid circumvention of
193 | technological measures.
194 |
195 | 4. Conveying Verbatim Copies.
196 |
197 | You may convey verbatim copies of the Program's source code as you
198 | receive it, in any medium, provided that you conspicuously and
199 | appropriately publish on each copy an appropriate copyright notice;
200 | keep intact all notices stating that this License and any
201 | non-permissive terms added in accord with section 7 apply to the code;
202 | keep intact all notices of the absence of any warranty; and give all
203 | recipients a copy of this License along with the Program.
204 |
205 | You may charge any price or no price for each copy that you convey,
206 | and you may offer support or warranty protection for a fee.
207 |
208 | 5. Conveying Modified Source Versions.
209 |
210 | You may convey a work based on the Program, or the modifications to
211 | produce it from the Program, in the form of source code under the
212 | terms of section 4, provided that you also meet all of these conditions:
213 |
214 | a) The work must carry prominent notices stating that you modified
215 | it, and giving a relevant date.
216 |
217 | b) The work must carry prominent notices stating that it is
218 | released under this License and any conditions added under section
219 | 7. This requirement modifies the requirement in section 4 to
220 | "keep intact all notices".
221 |
222 | c) You must license the entire work, as a whole, under this
223 | License to anyone who comes into possession of a copy. This
224 | License will therefore apply, along with any applicable section 7
225 | additional terms, to the whole of the work, and all its parts,
226 | regardless of how they are packaged. This License gives no
227 | permission to license the work in any other way, but it does not
228 | invalidate such permission if you have separately received it.
229 |
230 | d) If the work has interactive user interfaces, each must display
231 | Appropriate Legal Notices; however, if the Program has interactive
232 | interfaces that do not display Appropriate Legal Notices, your
233 | work need not make them do so.
234 |
235 | A compilation of a covered work with other separate and independent
236 | works, which are not by their nature extensions of the covered work,
237 | and which are not combined with it such as to form a larger program,
238 | in or on a volume of a storage or distribution medium, is called an
239 | "aggregate" if the compilation and its resulting copyright are not
240 | used to limit the access or legal rights of the compilation's users
241 | beyond what the individual works permit. Inclusion of a covered work
242 | in an aggregate does not cause this License to apply to the other
243 | parts of the aggregate.
244 |
245 | 6. Conveying Non-Source Forms.
246 |
247 | You may convey a covered work in object code form under the terms
248 | of sections 4 and 5, provided that you also convey the
249 | machine-readable Corresponding Source under the terms of this License,
250 | in one of these ways:
251 |
252 | a) Convey the object code in, or embodied in, a physical product
253 | (including a physical distribution medium), accompanied by the
254 | Corresponding Source fixed on a durable physical medium
255 | customarily used for software interchange.
256 |
257 | b) Convey the object code in, or embodied in, a physical product
258 | (including a physical distribution medium), accompanied by a
259 | written offer, valid for at least three years and valid for as
260 | long as you offer spare parts or customer support for that product
261 | model, to give anyone who possesses the object code either (1) a
262 | copy of the Corresponding Source for all the software in the
263 | product that is covered by this License, on a durable physical
264 | medium customarily used for software interchange, for a price no
265 | more than your reasonable cost of physically performing this
266 | conveying of source, or (2) access to copy the
267 | Corresponding Source from a network server at no charge.
268 |
269 | c) Convey individual copies of the object code with a copy of the
270 | written offer to provide the Corresponding Source. This
271 | alternative is allowed only occasionally and noncommercially, and
272 | only if you received the object code with such an offer, in accord
273 | with subsection 6b.
274 |
275 | d) Convey the object code by offering access from a designated
276 | place (gratis or for a charge), and offer equivalent access to the
277 | Corresponding Source in the same way through the same place at no
278 | further charge. You need not require recipients to copy the
279 | Corresponding Source along with the object code. If the place to
280 | copy the object code is a network server, the Corresponding Source
281 | may be on a different server (operated by you or a third party)
282 | that supports equivalent copying facilities, provided you maintain
283 | clear directions next to the object code saying where to find the
284 | Corresponding Source. Regardless of what server hosts the
285 | Corresponding Source, you remain obligated to ensure that it is
286 | available for as long as needed to satisfy these requirements.
287 |
288 | e) Convey the object code using peer-to-peer transmission, provided
289 | you inform other peers where the object code and Corresponding
290 | Source of the work are being offered to the general public at no
291 | charge under subsection 6d.
292 |
293 | A separable portion of the object code, whose source code is excluded
294 | from the Corresponding Source as a System Library, need not be
295 | included in conveying the object code work.
296 |
297 | A "User Product" is either (1) a "consumer product", which means any
298 | tangible personal property which is normally used for personal, family,
299 | or household purposes, or (2) anything designed or sold for incorporation
300 | into a dwelling. In determining whether a product is a consumer product,
301 | doubtful cases shall be resolved in favor of coverage. For a particular
302 | product received by a particular user, "normally used" refers to a
303 | typical or common use of that class of product, regardless of the status
304 | of the particular user or of the way in which the particular user
305 | actually uses, or expects or is expected to use, the product. A product
306 | is a consumer product regardless of whether the product has substantial
307 | commercial, industrial or non-consumer uses, unless such uses represent
308 | the only significant mode of use of the product.
309 |
310 | "Installation Information" for a User Product means any methods,
311 | procedures, authorization keys, or other information required to install
312 | and execute modified versions of a covered work in that User Product from
313 | a modified version of its Corresponding Source. The information must
314 | suffice to ensure that the continued functioning of the modified object
315 | code is in no case prevented or interfered with solely because
316 | modification has been made.
317 |
318 | If you convey an object code work under this section in, or with, or
319 | specifically for use in, a User Product, and the conveying occurs as
320 | part of a transaction in which the right of possession and use of the
321 | User Product is transferred to the recipient in perpetuity or for a
322 | fixed term (regardless of how the transaction is characterized), the
323 | Corresponding Source conveyed under this section must be accompanied
324 | by the Installation Information. But this requirement does not apply
325 | if neither you nor any third party retains the ability to install
326 | modified object code on the User Product (for example, the work has
327 | been installed in ROM).
328 |
329 | The requirement to provide Installation Information does not include a
330 | requirement to continue to provide support service, warranty, or updates
331 | for a work that has been modified or installed by the recipient, or for
332 | the User Product in which it has been modified or installed. Access to a
333 | network may be denied when the modification itself materially and
334 | adversely affects the operation of the network or violates the rules and
335 | protocols for communication across the network.
336 |
337 | Corresponding Source conveyed, and Installation Information provided,
338 | in accord with this section must be in a format that is publicly
339 | documented (and with an implementation available to the public in
340 | source code form), and must require no special password or key for
341 | unpacking, reading or copying.
342 |
343 | 7. Additional Terms.
344 |
345 | "Additional permissions" are terms that supplement the terms of this
346 | License by making exceptions from one or more of its conditions.
347 | Additional permissions that are applicable to the entire Program shall
348 | be treated as though they were included in this License, to the extent
349 | that they are valid under applicable law. If additional permissions
350 | apply only to part of the Program, that part may be used separately
351 | under those permissions, but the entire Program remains governed by
352 | this License without regard to the additional permissions.
353 |
354 | When you convey a copy of a covered work, you may at your option
355 | remove any additional permissions from that copy, or from any part of
356 | it. (Additional permissions may be written to require their own
357 | removal in certain cases when you modify the work.) You may place
358 | additional permissions on material, added by you to a covered work,
359 | for which you have or can give appropriate copyright permission.
360 |
361 | Notwithstanding any other provision of this License, for material you
362 | add to a covered work, you may (if authorized by the copyright holders of
363 | that material) supplement the terms of this License with terms:
364 |
365 | a) Disclaiming warranty or limiting liability differently from the
366 | terms of sections 15 and 16 of this License; or
367 |
368 | b) Requiring preservation of specified reasonable legal notices or
369 | author attributions in that material or in the Appropriate Legal
370 | Notices displayed by works containing it; or
371 |
372 | c) Prohibiting misrepresentation of the origin of that material, or
373 | requiring that modified versions of such material be marked in
374 | reasonable ways as different from the original version; or
375 |
376 | d) Limiting the use for publicity purposes of names of licensors or
377 | authors of the material; or
378 |
379 | e) Declining to grant rights under trademark law for use of some
380 | trade names, trademarks, or service marks; or
381 |
382 | f) Requiring indemnification of licensors and authors of that
383 | material by anyone who conveys the material (or modified versions of
384 | it) with contractual assumptions of liability to the recipient, for
385 | any liability that these contractual assumptions directly impose on
386 | those licensors and authors.
387 |
388 | All other non-permissive additional terms are considered "further
389 | restrictions" within the meaning of section 10. If the Program as you
390 | received it, or any part of it, contains a notice stating that it is
391 | governed by this License along with a term that is a further
392 | restriction, you may remove that term. If a license document contains
393 | a further restriction but permits relicensing or conveying under this
394 | License, you may add to a covered work material governed by the terms
395 | of that license document, provided that the further restriction does
396 | not survive such relicensing or conveying.
397 |
398 | If you add terms to a covered work in accord with this section, you
399 | must place, in the relevant source files, a statement of the
400 | additional terms that apply to those files, or a notice indicating
401 | where to find the applicable terms.
402 |
403 | Additional terms, permissive or non-permissive, may be stated in the
404 | form of a separately written license, or stated as exceptions;
405 | the above requirements apply either way.
406 |
407 | 8. Termination.
408 |
409 | You may not propagate or modify a covered work except as expressly
410 | provided under this License. Any attempt otherwise to propagate or
411 | modify it is void, and will automatically terminate your rights under
412 | this License (including any patent licenses granted under the third
413 | paragraph of section 11).
414 |
415 | However, if you cease all violation of this License, then your
416 | license from a particular copyright holder is reinstated (a)
417 | provisionally, unless and until the copyright holder explicitly and
418 | finally terminates your license, and (b) permanently, if the copyright
419 | holder fails to notify you of the violation by some reasonable means
420 | prior to 60 days after the cessation.
421 |
422 | Moreover, your license from a particular copyright holder is
423 | reinstated permanently if the copyright holder notifies you of the
424 | violation by some reasonable means, this is the first time you have
425 | received notice of violation of this License (for any work) from that
426 | copyright holder, and you cure the violation prior to 30 days after
427 | your receipt of the notice.
428 |
429 | Termination of your rights under this section does not terminate the
430 | licenses of parties who have received copies or rights from you under
431 | this License. If your rights have been terminated and not permanently
432 | reinstated, you do not qualify to receive new licenses for the same
433 | material under section 10.
434 |
435 | 9. Acceptance Not Required for Having Copies.
436 |
437 | You are not required to accept this License in order to receive or
438 | run a copy of the Program. Ancillary propagation of a covered work
439 | occurring solely as a consequence of using peer-to-peer transmission
440 | to receive a copy likewise does not require acceptance. However,
441 | nothing other than this License grants you permission to propagate or
442 | modify any covered work. These actions infringe copyright if you do
443 | not accept this License. Therefore, by modifying or propagating a
444 | covered work, you indicate your acceptance of this License to do so.
445 |
446 | 10. Automatic Licensing of Downstream Recipients.
447 |
448 | Each time you convey a covered work, the recipient automatically
449 | receives a license from the original licensors, to run, modify and
450 | propagate that work, subject to this License. You are not responsible
451 | for enforcing compliance by third parties with this License.
452 |
453 | An "entity transaction" is a transaction transferring control of an
454 | organization, or substantially all assets of one, or subdividing an
455 | organization, or merging organizations. If propagation of a covered
456 | work results from an entity transaction, each party to that
457 | transaction who receives a copy of the work also receives whatever
458 | licenses to the work the party's predecessor in interest had or could
459 | give under the previous paragraph, plus a right to possession of the
460 | Corresponding Source of the work from the predecessor in interest, if
461 | the predecessor has it or can get it with reasonable efforts.
462 |
463 | You may not impose any further restrictions on the exercise of the
464 | rights granted or affirmed under this License. For example, you may
465 | not impose a license fee, royalty, or other charge for exercise of
466 | rights granted under this License, and you may not initiate litigation
467 | (including a cross-claim or counterclaim in a lawsuit) alleging that
468 | any patent claim is infringed by making, using, selling, offering for
469 | sale, or importing the Program or any portion of it.
470 |
471 | 11. Patents.
472 |
473 | A "contributor" is a copyright holder who authorizes use under this
474 | License of the Program or a work on which the Program is based. The
475 | work thus licensed is called the contributor's "contributor version".
476 |
477 | A contributor's "essential patent claims" are all patent claims
478 | owned or controlled by the contributor, whether already acquired or
479 | hereafter acquired, that would be infringed by some manner, permitted
480 | by this License, of making, using, or selling its contributor version,
481 | but do not include claims that would be infringed only as a
482 | consequence of further modification of the contributor version. For
483 | purposes of this definition, "control" includes the right to grant
484 | patent sublicenses in a manner consistent with the requirements of
485 | this License.
486 |
487 | Each contributor grants you a non-exclusive, worldwide, royalty-free
488 | patent license under the contributor's essential patent claims, to
489 | make, use, sell, offer for sale, import and otherwise run, modify and
490 | propagate the contents of its contributor version.
491 |
492 | In the following three paragraphs, a "patent license" is any express
493 | agreement or commitment, however denominated, not to enforce a patent
494 | (such as an express permission to practice a patent or covenant not to
495 | sue for patent infringement). To "grant" such a patent license to a
496 | party means to make such an agreement or commitment not to enforce a
497 | patent against the party.
498 |
499 | If you convey a covered work, knowingly relying on a patent license,
500 | and the Corresponding Source of the work is not available for anyone
501 | to copy, free of charge and under the terms of this License, through a
502 | publicly available network server or other readily accessible means,
503 | then you must either (1) cause the Corresponding Source to be so
504 | available, or (2) arrange to deprive yourself of the benefit of the
505 | patent license for this particular work, or (3) arrange, in a manner
506 | consistent with the requirements of this License, to extend the patent
507 | license to downstream recipients. "Knowingly relying" means you have
508 | actual knowledge that, but for the patent license, your conveying the
509 | covered work in a country, or your recipient's use of the covered work
510 | in a country, would infringe one or more identifiable patents in that
511 | country that you have reason to believe are valid.
512 |
513 | If, pursuant to or in connection with a single transaction or
514 | arrangement, you convey, or propagate by procuring conveyance of, a
515 | covered work, and grant a patent license to some of the parties
516 | receiving the covered work authorizing them to use, propagate, modify
517 | or convey a specific copy of the covered work, then the patent license
518 | you grant is automatically extended to all recipients of the covered
519 | work and works based on it.
520 |
521 | A patent license is "discriminatory" if it does not include within
522 | the scope of its coverage, prohibits the exercise of, or is
523 | conditioned on the non-exercise of one or more of the rights that are
524 | specifically granted under this License. You may not convey a covered
525 | work if you are a party to an arrangement with a third party that is
526 | in the business of distributing software, under which you make payment
527 | to the third party based on the extent of your activity of conveying
528 | the work, and under which the third party grants, to any of the
529 | parties who would receive the covered work from you, a discriminatory
530 | patent license (a) in connection with copies of the covered work
531 | conveyed by you (or copies made from those copies), or (b) primarily
532 | for and in connection with specific products or compilations that
533 | contain the covered work, unless you entered into that arrangement,
534 | or that patent license was granted, prior to 28 March 2007.
535 |
536 | Nothing in this License shall be construed as excluding or limiting
537 | any implied license or other defenses to infringement that may
538 | otherwise be available to you under applicable patent law.
539 |
540 | 12. No Surrender of Others' Freedom.
541 |
542 | If conditions are imposed on you (whether by court order, agreement or
543 | otherwise) that contradict the conditions of this License, they do not
544 | excuse you from the conditions of this License. If you cannot convey a
545 | covered work so as to satisfy simultaneously your obligations under this
546 | License and any other pertinent obligations, then as a consequence you may
547 | not convey it at all. For example, if you agree to terms that obligate you
548 | to collect a royalty for further conveying from those to whom you convey
549 | the Program, the only way you could satisfy both those terms and this
550 | License would be to refrain entirely from conveying the Program.
551 |
552 | 13. Use with the GNU Affero General Public License.
553 |
554 | Notwithstanding any other provision of this License, you have
555 | permission to link or combine any covered work with a work licensed
556 | under version 3 of the GNU Affero General Public License into a single
557 | combined work, and to convey the resulting work. The terms of this
558 | License will continue to apply to the part which is the covered work,
559 | but the special requirements of the GNU Affero General Public License,
560 | section 13, concerning interaction through a network will apply to the
561 | combination as such.
562 |
563 | 14. Revised Versions of this License.
564 |
565 | The Free Software Foundation may publish revised and/or new versions of
566 | the GNU General Public License from time to time. Such new versions will
567 | be similar in spirit to the present version, but may differ in detail to
568 | address new problems or concerns.
569 |
570 | Each version is given a distinguishing version number. If the
571 | Program specifies that a certain numbered version of the GNU General
572 | Public License "or any later version" applies to it, you have the
573 | option of following the terms and conditions either of that numbered
574 | version or of any later version published by the Free Software
575 | Foundation. If the Program does not specify a version number of the
576 | GNU General Public License, you may choose any version ever published
577 | by the Free Software Foundation.
578 |
579 | If the Program specifies that a proxy can decide which future
580 | versions of the GNU General Public License can be used, that proxy's
581 | public statement of acceptance of a version permanently authorizes you
582 | to choose that version for the Program.
583 |
584 | Later license versions may give you additional or different
585 | permissions. However, no additional obligations are imposed on any
586 | author or copyright holder as a result of your choosing to follow a
587 | later version.
588 |
589 | 15. Disclaimer of Warranty.
590 |
591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
599 |
600 | 16. Limitation of Liability.
601 |
602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
610 | SUCH DAMAGES.
611 |
612 | 17. Interpretation of Sections 15 and 16.
613 |
614 | If the disclaimer of warranty and limitation of liability provided
615 | above cannot be given local legal effect according to their terms,
616 | reviewing courts shall apply local law that most closely approximates
617 | an absolute waiver of all civil liability in connection with the
618 | Program, unless a warranty or assumption of liability accompanies a
619 | copy of the Program in return for a fee.
620 |
621 | END OF TERMS AND CONDITIONS
622 |
623 | How to Apply These Terms to Your New Programs
624 |
625 | If you develop a new program, and you want it to be of the greatest
626 | possible use to the public, the best way to achieve this is to make it
627 | free software which everyone can redistribute and change under these terms.
628 |
629 | To do so, attach the following notices to the program. It is safest
630 | to attach them to the start of each source file to most effectively
631 | state the exclusion of warranty; and each file should have at least
632 | the "copyright" line and a pointer to where the full notice is found.
633 |
634 |
635 | Copyright (C)
636 |
637 | This program is free software: you can redistribute it and/or modify
638 | it under the terms of the GNU General Public License as published by
639 | the Free Software Foundation, either version 3 of the License, or
640 | (at your option) any later version.
641 |
642 | This program is distributed in the hope that it will be useful,
643 | but WITHOUT ANY WARRANTY; without even the implied warranty of
644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
645 | GNU General Public License for more details.
646 |
647 | You should have received a copy of the GNU General Public License
648 | along with this program. If not, see .
649 |
650 | Also add information on how to contact you by electronic and paper mail.
651 |
652 | If the program does terminal interaction, make it output a short
653 | notice like this when it starts in an interactive mode:
654 |
655 | Copyright (C)
656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657 | This is free software, and you are welcome to redistribute it
658 | under certain conditions; type `show c' for details.
659 |
660 | The hypothetical commands `show w' and `show c' should show the appropriate
661 | parts of the General Public License. Of course, your program's commands
662 | might be different; for a GUI interface, you would use an "about box".
663 |
664 | You should also get your employer (if you work as a programmer) or school,
665 | if any, to sign a "copyright disclaimer" for the program, if necessary.
666 | For more information on this, and how to apply and follow the GNU GPL, see
667 | .
668 |
669 | The GNU General Public License does not permit incorporating your program
670 | into proprietary programs. If your program is a subroutine library, you
671 | may consider it more useful to permit linking proprietary applications with
672 | the library. If this is what you want to do, use the GNU Lesser General
673 | Public License instead of this License. But first, please read
674 | .
675 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # MoviePilot-Plugins
2 | MoviePilot官方插件市场:https://github.com/jxxghp/MoviePilot-Plugins
3 |
4 | ### [媒体库封面生成](https://github.com/justzerock/MoviePilot-Plugins/tree/main/plugins.v2/mediacovergenerator)
5 | > 参考项目:https://github.com/HappyQuQu/jellyfin-library-poster
6 |
7 | 在群里受到这个项目的启发,督促 AI 帮我写封面处理的代码,于是有了这个插件,支持切换风格
8 |
9 | 
--------------------------------------------------------------------------------
/fonts/EmblemaOne.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justzerock/MoviePilot-Plugins/8ef476f9e5ae4d3d549300bad74083c084a46f1d/fonts/EmblemaOne.woff2
--------------------------------------------------------------------------------
/fonts/josefinsans.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justzerock/MoviePilot-Plugins/8ef476f9e5ae4d3d549300bad74083c084a46f1d/fonts/josefinsans.woff2
--------------------------------------------------------------------------------
/fonts/lilitaone.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justzerock/MoviePilot-Plugins/8ef476f9e5ae4d3d549300bad74083c084a46f1d/fonts/lilitaone.woff2
--------------------------------------------------------------------------------
/fonts/multi_1_en.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justzerock/MoviePilot-Plugins/8ef476f9e5ae4d3d549300bad74083c084a46f1d/fonts/multi_1_en.otf
--------------------------------------------------------------------------------
/fonts/multi_1_zh.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justzerock/MoviePilot-Plugins/8ef476f9e5ae4d3d549300bad74083c084a46f1d/fonts/multi_1_zh.ttf
--------------------------------------------------------------------------------
/fonts/phosphate.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justzerock/MoviePilot-Plugins/8ef476f9e5ae4d3d549300bad74083c084a46f1d/fonts/phosphate.ttf
--------------------------------------------------------------------------------
/fonts/wendao.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justzerock/MoviePilot-Plugins/8ef476f9e5ae4d3d549300bad74083c084a46f1d/fonts/wendao.ttf
--------------------------------------------------------------------------------
/icons/douban.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justzerock/MoviePilot-Plugins/8ef476f9e5ae4d3d549300bad74083c084a46f1d/icons/douban.png
--------------------------------------------------------------------------------
/icons/emby.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justzerock/MoviePilot-Plugins/8ef476f9e5ae4d3d549300bad74083c084a46f1d/icons/emby.png
--------------------------------------------------------------------------------
/icons/speed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justzerock/MoviePilot-Plugins/8ef476f9e5ae4d3d549300bad74083c084a46f1d/icons/speed.png
--------------------------------------------------------------------------------
/images/multi_1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justzerock/MoviePilot-Plugins/8ef476f9e5ae4d3d549300bad74083c084a46f1d/images/multi_1.jpg
--------------------------------------------------------------------------------
/images/plugin.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justzerock/MoviePilot-Plugins/8ef476f9e5ae4d3d549300bad74083c084a46f1d/images/plugin.webp
--------------------------------------------------------------------------------
/images/single_1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justzerock/MoviePilot-Plugins/8ef476f9e5ae4d3d549300bad74083c084a46f1d/images/single_1.jpg
--------------------------------------------------------------------------------
/images/single_2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justzerock/MoviePilot-Plugins/8ef476f9e5ae4d3d549300bad74083c084a46f1d/images/single_2.jpg
--------------------------------------------------------------------------------
/package.v2.json:
--------------------------------------------------------------------------------
1 | {
2 | "MediaCoverGenerator": {
3 | "name": "媒体库封面生成",
4 | "description": "自动生成媒体库封面,支持 Emby,Jellyfin",
5 | "labels": "封面,媒体库",
6 | "version": "0.8.7",
7 | "icon": "https://raw.githubusercontent.com/justzerock/MoviePilot-Plugins/main/icons/emby.png",
8 | "author": "justzerock",
9 | "level": 1,
10 | "history": {
11 | "v0.8.7": "支持生成播放列表封面",
12 | "v0.8.6": "修复无参数造成无法生图的错误",
13 | "v0.8.5": "可选优先使用海报或者背景图,修复部分bug",
14 | "v0.8.4": "补充部分信息",
15 | "v0.8.3": "修复合集等库图片获取问题,改善字体下载问题,增加无用的若干选项",
16 | "v0.8.2": "1. 支持添加本地字体路径 2. 未设置标题或参数错误时,自动使用媒体库名",
17 | "v0.8.1": "修复 bug,也许还有漏网之鱼",
18 | "v0.8": "改界面,加风格,可能还有新 bug",
19 | "v0.7": "修复一个非常简单的小问题,我真是太菜了",
20 | "v0.6": "修复部分问题",
21 | "v0.5": "微调图片处理颜色",
22 | "v0.4": "自定义背景图目录",
23 | "v0.3": "修复bug",
24 | "v0.2": "测试",
25 | "v0.1": "自定义字体"
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/plugins.v2/mediacovergenerator/requirements.txt:
--------------------------------------------------------------------------------
1 | pillow==11.2.1
2 | numpy==2.2.0
3 | pytz==2025.2
4 | pyyaml==6.0.2
--------------------------------------------------------------------------------
/plugins.v2/mediacovergenerator/static/multi_1.py:
--------------------------------------------------------------------------------
1 | multi_1 = "data:image/jpeg;base64,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"
--------------------------------------------------------------------------------
/plugins.v2/mediacovergenerator/static/single_1.py:
--------------------------------------------------------------------------------
1 | single_1 = "data:image/jpeg;base64,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"
--------------------------------------------------------------------------------
/plugins.v2/mediacovergenerator/static/single_2.py:
--------------------------------------------------------------------------------
1 | single_2 = "data:image/jpeg;base64,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"
--------------------------------------------------------------------------------
/plugins.v2/mediacovergenerator/style_multi_1.py:
--------------------------------------------------------------------------------
1 | import base64
2 | from collections import Counter
3 | import io
4 | from pathlib import Path
5 | from PIL import Image, ImageFilter, ImageDraw, ImageFont, ImageOps
6 | import numpy as np
7 | import os
8 | import math
9 | import random # 添加随机模块
10 | import colorsys
11 | from app.log import logger
12 |
13 | """
14 | 代码修改自 https://github.com/HappyQuQu/jellyfin-library-poster/blob/main/gen_poster.py
15 | """
16 |
17 | # 海报生成配置
18 | POSTER_GEN_CONFIG = {
19 | "ROWS": 3, # 每列图片数
20 | "COLS": 3, # 总列数
21 | "MARGIN": 22, # 图片垂直间距
22 | "CORNER_RADIUS": 46.1, # 圆角半径
23 | "ROTATION_ANGLE": -15.8, # 旋转角度
24 | "START_X": 835, # 第一列的 x 坐标
25 | "START_Y": -362, # 第一列的 y 坐标
26 | "COLUMN_SPACING": 100, # 列间距
27 | "SAVE_COLUMNS": True, # 是否保存每列图片
28 | "CELL_WIDTH": 410, # 海报宽度
29 | "CELL_HEIGHT": 610, # 海报高度
30 | "CANVAS_WIDTH": 1920, # 画布宽度
31 | "CANVAS_HEIGHT": 1080, # 画布高度
32 | }
33 |
34 | def add_shadow(img, offset=(5, 5), shadow_color=(0, 0, 0, 100), blur_radius=3):
35 | """
36 | 给图片添加右侧和底部阴影
37 |
38 | 参数:
39 | img: 原始图片(PIL.Image对象)
40 | offset: 阴影偏移量,(x, y)格式
41 | shadow_color: 阴影颜色,RGBA格式
42 | blur_radius: 阴影模糊半径
43 |
44 | 返回:
45 | 添加了阴影的新图片
46 | """
47 | # 创建一个透明背景,比原图大一些,以容纳阴影
48 | shadow_width = img.width + offset[0] + blur_radius * 2
49 | shadow_height = img.height + offset[1] + blur_radius * 2
50 |
51 | shadow = Image.new("RGBA", (shadow_width, shadow_height), (0, 0, 0, 0))
52 |
53 | # 创建阴影层
54 | shadow_layer = Image.new("RGBA", img.size, shadow_color)
55 |
56 | # 将阴影层粘贴到偏移位置
57 | shadow.paste(shadow_layer, (blur_radius + offset[0], blur_radius + offset[1]))
58 |
59 | # 模糊阴影
60 | shadow = shadow.filter(ImageFilter.GaussianBlur(blur_radius))
61 |
62 | # 创建结果图像
63 | result = Image.new("RGBA", shadow.size, (0, 0, 0, 0))
64 |
65 | # 将原图粘贴到结果图像上
66 | result.paste(img, (blur_radius, blur_radius), img if img.mode == "RGBA" else None)
67 |
68 | # 合并阴影和原图(保持原图在上层)
69 | shadow_img = Image.alpha_composite(shadow, result)
70 |
71 | return shadow_img
72 |
73 |
74 | # 单行文字
75 | def draw_text_on_image(
76 | image, text, position, font_path, default_font_path, font_size, fill_color=(255, 255, 255, 255),
77 | shadow=False, shadow_color=None, shadow_offset=10, shadow_alpha=75
78 | ):
79 | """
80 | 在图像上绘制文字,可选择添加阴影效果
81 |
82 | 参数:
83 | image: PIL.Image对象
84 | text: 要绘制的文字
85 | position: 文字位置 (x, y)
86 | font_path: 字体文件路径
87 | default_font_path: 默认字体路径
88 | font_size: 字体大小
89 | fill_color: 文字颜色,RGBA格式
90 | shadow: 是否添加阴影效果
91 | shadow_color: 阴影颜色,RGB格式,如果为None则自动生成
92 | shadow_offset: 阴影偏移量
93 | shadow_alpha: 阴影透明度(0-255)
94 |
95 | 返回:
96 | 添加了文字的图像
97 | """
98 | # 创建一个可绘制的图像副本
99 | img_copy = image.copy()
100 | text_layer = Image.new('RGBA', img_copy.size, (255, 255, 255, 0))
101 | shadow_layer = Image.new('RGBA', img_copy.size, (0, 0, 0, 0))
102 | draw = ImageDraw.Draw(text_layer)
103 | shadow_draw = ImageDraw.Draw(shadow_layer)
104 | font = ImageFont.truetype(font_path, font_size)
105 |
106 | # 如果需要添加阴影
107 | if shadow:
108 | fill_color = (fill_color[0], fill_color[1], fill_color[2], 229)
109 | if shadow_color is None:
110 | if len(fill_color) >= 3:
111 | r = max(0, int(fill_color[0] * 0.7))
112 | g = max(0, int(fill_color[1] * 0.7))
113 | b = max(0, int(fill_color[2] * 0.7))
114 | shadow_color_with_alpha = (r, g, b, shadow_alpha)
115 | else:
116 | shadow_color_with_alpha = (50, 50, 50, shadow_alpha)
117 | else:
118 | # 确保 shadow_color 是 RGB 或 RGBA
119 | if len(shadow_color) == 3:
120 | shadow_color_with_alpha = shadow_color + (shadow_alpha,)
121 | elif len(shadow_color) == 4:
122 | shadow_color_with_alpha = shadow_color[:3] + (shadow_alpha,) # 修正:取前三个元素
123 | else:
124 | raise ValueError("shadow_color 格式不正确") # 抛出异常,明确错误
125 |
126 | for offset in range(3, shadow_offset + 1, 2):
127 | shadow_draw.text(
128 | (position[0] + offset, position[1] + offset),
129 | text,
130 | font=font,
131 | fill=shadow_color_with_alpha
132 | )
133 | # 绘制主文字
134 | draw.text(position, text, font=font, fill=fill_color)
135 | blurred_shadow = shadow_layer.filter(ImageFilter.GaussianBlur(radius=shadow_offset))
136 | combined = Image.alpha_composite(img_copy, blurred_shadow)
137 | img_copy = Image.alpha_composite(combined, text_layer)
138 |
139 | return img_copy
140 |
141 | # 多行文字
142 | def draw_multiline_text_on_image(
143 | image,
144 | text,
145 | position,
146 | font_path,
147 | default_font_path,
148 | font_size,
149 | line_spacing=10,
150 | fill_color=(255, 255, 255, 255),
151 | shadow=False,
152 | shadow_color=None,
153 | shadow_offset=4,
154 | shadow_alpha=100
155 | ):
156 | """
157 | 在图像上绘制多行文字,根据空格自动换行,可选择添加阴影效果
158 |
159 | 参数:
160 | image: PIL.Image对象
161 | text: 要绘制的文字
162 | position: 第一行文字位置 (x, y)
163 | font_path: 字体文件路径
164 | default_font_path: 默认字体路径
165 | font_size: 字体大小
166 | line_spacing: 行间距
167 | fill_color: 文字颜色,RGBA格式
168 | shadow: 是否添加阴影效果
169 | shadow_color: 阴影颜色,RGB格式,如果为None则自动生成
170 | shadow_offset: 阴影偏移量
171 | shadow_alpha: 阴影透明度(0-255)
172 |
173 | 返回:
174 | 添加了文字的图像和行数
175 | """
176 | # 创建一个可绘制的图像副本
177 | img_copy = image.copy()
178 | text_layer = Image.new('RGBA', img_copy.size, (255, 255, 255, 0))
179 | draw = ImageDraw.Draw(text_layer)
180 | font = ImageFont.truetype(font_path, font_size)
181 |
182 | # 按空格分割文本
183 | lines = text.split(" ")
184 |
185 | # 如果未指定阴影颜色,则根据填充颜色生成
186 | if shadow:
187 | fill_color = (fill_color[0], fill_color[1], fill_color[2], 229)
188 | if shadow_color is None:
189 | # 使用文字颜色的暗化版本作为阴影
190 | if len(fill_color) >= 3:
191 | # 暗化颜色
192 | r = max(0, int(fill_color[0] * 0.7))
193 | g = max(0, int(fill_color[1] * 0.7))
194 | b = max(0, int(fill_color[2] * 0.7))
195 | shadow_color_with_alpha = (r, g, b, shadow_alpha)
196 | else:
197 | # 默认灰色阴影
198 | shadow_color_with_alpha = (50, 50, 50, shadow_alpha)
199 | else:
200 | # 确保 shadow_color 是 RGB 或 RGBA
201 | if len(shadow_color) == 3:
202 | shadow_color_with_alpha = shadow_color + (shadow_alpha,)
203 | elif len(shadow_color) == 4:
204 | shadow_color_with_alpha = shadow_color[:3] + (shadow_alpha,)
205 | else:
206 | raise ValueError("shadow_color 格式不正确")
207 |
208 | # 如果只有一行,直接绘制并返回
209 | if len(lines) <= 1:
210 | if shadow:
211 | for offset in range(3, shadow_offset + 1, 2):
212 | draw.text(
213 | (position[0] + offset, position[1] + offset),
214 | text,
215 | font=font,
216 | fill=shadow_color_with_alpha
217 | )
218 | draw.text(position, text, font=font, fill=fill_color)
219 | img_copy = Image.alpha_composite(img_copy, text_layer)
220 | return img_copy, 1
221 |
222 | # 绘制多行文本
223 | x, y = position
224 | for i, line in enumerate(lines):
225 | current_y = y + i * (font_size + line_spacing)
226 |
227 | if shadow:
228 | for offset in range(3, shadow_offset + 1, 2):
229 | draw.text(
230 | (x + offset, current_y + offset),
231 | line,
232 | font=font,
233 | fill=shadow_color_with_alpha
234 | )
235 | draw.text((x, current_y), line, font=font, fill=fill_color)
236 | img_copy = Image.alpha_composite(img_copy, text_layer)
237 | return img_copy, len(lines)
238 |
239 |
240 | def get_random_color(image_path):
241 | """
242 | 获取图片随机位置的颜色
243 |
244 | 参数:
245 | image_path: 图片文件路径
246 |
247 | 返回:
248 | 随机点颜色,RGBA格式
249 | """
250 | try:
251 | img = Image.open(image_path)
252 | # 获取图片尺寸
253 | width, height = img.size
254 |
255 | # 在图片范围内随机选择一个点
256 | # 避免边缘区域,缩小范围到图片的20%-80%区域
257 | random_x = random.randint(int(width * 0.5), int(width * 0.8))
258 | random_y = random.randint(int(height * 0.5), int(height * 0.8))
259 |
260 | # 获取随机点的颜色
261 | if img.mode == "RGBA":
262 | r, g, b, a = img.getpixel((random_x, random_y))
263 | return (r, g, b, a)
264 | elif img.mode == "RGB":
265 | r, g, b = img.getpixel((random_x, random_y))
266 | return (r + 100, g + 50, b, 255)
267 | else:
268 | img = img.convert("RGBA")
269 | r, g, b, a = img.getpixel((random_x, random_y))
270 | return (r, g, b, a)
271 | except Exception as e:
272 | # logger.error(f"获取图片颜色时出错: {e}")
273 | # 返回随机颜色作为备选
274 | return (
275 | random.randint(50, 200),
276 | random.randint(50, 200),
277 | random.randint(50, 200),
278 | 255,
279 | )
280 |
281 |
282 | def draw_color_block(image, position, size, color):
283 | """
284 | 在图像上绘制色块
285 |
286 | 参数:
287 | image: PIL.Image对象
288 | position: 色块位置 (x, y)
289 | size: 色块大小 (width, height)
290 | color: 色块颜色,RGBA格式
291 |
292 | 返回:
293 | 添加了色块的图像
294 | """
295 | # 创建一个可绘制的图像副本
296 | img_copy = image.copy()
297 | draw = ImageDraw.Draw(img_copy)
298 |
299 | # 绘制矩形色块
300 | draw.rectangle(
301 | [position, (position[0] + size[0], position[1] + size[1])], fill=color
302 | )
303 |
304 | return img_copy
305 |
306 |
307 | def create_gradient_background(width, height, color=None):
308 | """
309 | 创建一个从左到右的渐变背景,使用遮罩技术实现渐变效果
310 | 左侧颜色更深,右侧颜色适中,提供更明显的渐变效果
311 |
312 | 参数:
313 | width: 背景宽度
314 | height: 背景高度
315 | color: 颜色数组或单个颜色,如果为None则随机生成
316 | 如果是数组,会依次尝试每个颜色,跳过太黑或太淡的颜色
317 |
318 | 返回:
319 | 渐变背景图像
320 | """
321 | def _normalize_rgb(input_rgb):
322 | """
323 | 将各种可能的输入格式,统一提取成 (r, g, b) 三元组。
324 | 支持:
325 | - (r, g, b)
326 | - (r, g, b, a)
327 | - ((r, g, b), idx) or ((r, g, b, a), idx)
328 | """
329 | if isinstance(input_rgb, tuple):
330 | # 情况 3: ((r,g,b,a), idx) 或 ((r,g,b), idx)
331 | if len(input_rgb) == 2 and isinstance(input_rgb[0], tuple):
332 | return _normalize_rgb(input_rgb[0])
333 | # 情况 2: RGBA
334 | if len(input_rgb) == 4 and all(isinstance(v, (int, float)) for v in input_rgb):
335 | return input_rgb[:3]
336 | # 情况 1: RGB
337 | if len(input_rgb) == 3 and all(isinstance(v, (int, float)) for v in input_rgb):
338 | return input_rgb
339 | raise ValueError(f"无法识别的颜色格式: {input_rgb!r}")
340 |
341 | def _is_mid_bright(input_rgb, min_lum=80, max_lum=200):
342 | """
343 | 基于相对亮度判断:不过暗(>=min_lum)也不过白(<=max_lum)。
344 | input_rgb 可为多种格式,函数内部会 normalize。
345 | """
346 | r, g, b = _normalize_rgb(input_rgb)
347 | lum = 0.299*r + 0.587*g + 0.114*b
348 | return min_lum <= lum <= max_lum
349 | # 定义用于判断颜色是否合适的函数
350 | def _is_mid_bright_hsl(input_rgb, min_l=0.3, max_l=0.7):
351 | """
352 | 基于 HSL Lightness 判断。Lightness 在 [0,1]。
353 | """
354 | r, g, b = _normalize_rgb(input_rgb)
355 | # 归一到 [0,1]
356 | r1, g1, b1 = r/255.0, g/255.0, b/255.0
357 | h, l, s = colorsys.rgb_to_hls(r1, g1, b1)
358 | return min_l <= l <= max_l
359 |
360 | selected_color = None
361 |
362 | # 如果传入的是颜色数组
363 | if isinstance(color, list) and len(color) > 0:
364 | # 尝试找到合适的颜色,最多尝试5个
365 | for i in range(min(10, len(color))):
366 | if _is_mid_bright_hsl(color[i]):
367 | # 如果是(color_tuple, count)格式,提取颜色元组
368 | if isinstance(color[i], tuple) and len(color[i]) == 2 and isinstance(color[i][0], tuple):
369 | selected_color = color[i][0]
370 | else:
371 | selected_color = color[i]
372 | # logger.info(f" 海报主题色:[{selected_color}]适合做背景")
373 | break
374 | else:
375 | pass
376 | # logger.info(f" 海报主题色:[{color[i]}]不适合做背景,尝试做下一个颜色")
377 |
378 | # 如果没有找到合适的颜色,随机生成一个颜色
379 | if selected_color is None:
380 |
381 | def random_hsl_to_rgb(
382 | hue_range=(0, 360),
383 | sat_range=(0.5, 1.0),
384 | light_range=(0.5, 0.8)
385 | ):
386 | """
387 | hue_range: 色相范围,取值 0~360
388 | sat_range: 饱和度范围,取值 0~1
389 | light_range: 明度范围,取值 0~1
390 | 返回值:RGB 三元组,每个通道 0~255
391 | """
392 | h = random.uniform(hue_range[0]/360.0, hue_range[1]/360.0)
393 | s = random.uniform(sat_range[0], sat_range[1])
394 | l = random.uniform(light_range[0], light_range[1])
395 | # colorsys.hls_to_rgb 接受 H, L, S (注意顺序) 都是 0~1
396 | r, g, b = colorsys.hls_to_rgb(h, l, s)
397 | # 转回 0~255
398 | return (int(r*255), int(g*255), int(b*255))
399 |
400 | # 生成颜色示例
401 | selected_color = random_hsl_to_rgb()
402 | # logger.info(f"海报所有主题色不适合做背景,随机生成一个颜色[{selected_color}]。")
403 |
404 | # 如果是已经提供的颜色,将其加深
405 | # 降低各通道的亮度,使颜色更深
406 | r = int(selected_color[0] * 0.65) # 降低35%
407 | g = int(selected_color[1] * 0.65) # 降低35%
408 | b = int(selected_color[2] * 0.65) # 降低35%
409 |
410 | # 确保RGB值不会小于0
411 | r = max(0, r)
412 | g = max(0, g)
413 | b = max(0, b)
414 |
415 | # 更新颜色
416 | selected_color = (r, g, b, selected_color[3] if len(selected_color) > 3 else 255)
417 |
418 | # 确保selected_color包含alpha通道
419 | if len(selected_color) == 3:
420 | selected_color = (selected_color[0], selected_color[1], selected_color[2], 255)
421 |
422 | # 基于selected_color自动生成浅色版本作为右侧颜色
423 | # 将selected_color的RGB值增加更合适的比例,使右侧颜色适中
424 | # 限制最大值为255
425 | r = min(255, int(selected_color[0] * 1.9)) # 从2.2降到1.9
426 | g = min(255, int(selected_color[1] * 1.9)) # 从2.2降到1.9
427 | b = min(255, int(selected_color[2] * 1.9)) # 从2.2降到1.9
428 |
429 | # 确保至少有一定的亮度增加,但比之前小
430 | r = max(r, selected_color[0] + 80) # 从100降到80
431 | g = max(g, selected_color[1] + 80) # 从100降到80
432 | b = max(b, selected_color[2] + 80) # 从100降到80
433 |
434 | # 确保右侧颜色不会太亮
435 | r = min(r, 230) # 限制最大亮度
436 | g = min(g, 230) # 限制最大亮度
437 | b = min(b, 230) # 限制最大亮度
438 |
439 | # 创建右侧浅色
440 | color2 = (r, g, b, selected_color[3])
441 |
442 | # 创建左右两个纯色图像
443 | left_image = Image.new("RGBA", (width, height), selected_color)
444 | right_image = Image.new("RGBA", (width, height), color2)
445 |
446 | # 创建渐变遮罩(从黑到白的横向线性渐变)
447 | mask = Image.new("L", (width, height), 0)
448 | mask_data = []
449 |
450 | # 生成遮罩数据,使用更加平滑的过渡
451 | for y in range(height):
452 | for x in range(width):
453 | # 计算从左到右的渐变值 (0-255)
454 | # 使用更加非线性的渐变,使左侧深色区域更大
455 | mask_value = int(255.0 * (x / width) ** 0.7) # 从0.85改为0.7
456 | mask_data.append(mask_value)
457 |
458 | # 应用遮罩数据到遮罩图像
459 | mask.putdata(mask_data)
460 |
461 | # 使用遮罩合成左右两个图像
462 | # 遮罩中黑色部分(0)显示left_image,白色部分(255)显示right_image
463 | gradient = Image.composite(right_image, left_image, mask)
464 |
465 | return gradient
466 |
467 |
468 | def get_poster_primary_color(image_path):
469 | """
470 | 分析图片并提取主色调
471 |
472 | 参数:
473 | image_path: 图片文件路径
474 |
475 | 返回:
476 | 主色调颜色,RGBA格式
477 | """
478 | try:
479 | from collections import Counter
480 |
481 | # 打开图片
482 | img = Image.open(image_path)
483 |
484 | # 缩小图片尺寸以加快处理速度
485 | img = img.resize((100, 150), Image.LANCZOS)
486 |
487 | # 确保图片为RGBA模式
488 | if img.mode != 'RGBA':
489 | img = img.convert('RGBA')
490 |
491 | # 获取图片中心部分的像素数据(避免边框和角落)
492 | # width, height = img.size
493 | # center_x1 = int(width * 0.2)
494 | # center_y1 = int(height * 0.2)
495 | # center_x2 = int(width * 0.8)
496 | # center_y2 = int(height * 0.8)
497 |
498 | # # 裁剪出中心区域
499 | # center_img = img.crop((center_x1, center_y1, center_x2, center_y2))
500 |
501 | # 获取所有像素
502 | pixels = list(img.getdata())
503 |
504 | # 过滤掉接近黑色和白色的像素,以及透明度低的像素
505 | filtered_pixels = []
506 | for pixel in pixels:
507 | r, g, b, a = pixel
508 |
509 | # 跳过透明度低的像素
510 | if a < 200:
511 | continue
512 |
513 | # 计算亮度
514 | brightness = (r + g + b) / 3
515 |
516 | # 跳过过暗或过亮的像素
517 | if brightness < 30 or brightness > 220:
518 | continue
519 |
520 | # 添加到过滤后的列表
521 | filtered_pixels.append((r, g, b, 255))
522 |
523 | # 如果过滤后没有像素,使用全部像素
524 | if not filtered_pixels:
525 | filtered_pixels = [(p[0], p[1], p[2], 255) for p in pixels if p[3] > 100]
526 |
527 | # 如果仍然没有像素,返回默认颜色
528 | if not filtered_pixels:
529 | return (150, 100, 50, 255)
530 |
531 | # 使用Counter找到出现最多的颜色
532 | color_counter = Counter(filtered_pixels)
533 | common_colors = color_counter.most_common(10)
534 |
535 | # 如果找到了颜色,返回最常见的颜色
536 | if common_colors:
537 | return common_colors
538 |
539 | # 如果无法找到主色调,使用平均值
540 | r_avg = sum(p[0] for p in filtered_pixels) // len(filtered_pixels)
541 | g_avg = sum(p[1] for p in filtered_pixels) // len(filtered_pixels)
542 | b_avg = sum(p[2] for p in filtered_pixels) // len(filtered_pixels)
543 |
544 | return [(r_avg, g_avg, b_avg, 255)]
545 |
546 |
547 | except Exception as e:
548 | # logger.error(f"获取图片主色调时出错: {e}")
549 | # 返回默认颜色作为备选
550 | return [(150, 100, 50, 255)]
551 |
552 | def create_blur_background(image_path, template_width, template_height, background_color, blur_size, color_ratio, lighten_gradient_strength=0.6):
553 | """
554 | 创建模糊背景图像,将原始图像模糊化并与指定颜色混合,添加胶片颗粒效果
555 |
556 | 参数:
557 | image_path (str): 原始图像的路径
558 | template_width (int): 模板宽度
559 | template_height (int): 模板高度
560 | color (tuple or list): 背景混合颜色列表或颜色元组,包含(R,G,B,A)格式的颜色
561 |
562 | 返回:
563 | PIL.Image: 处理后的背景图像
564 | """
565 |
566 | # 加载原始图像
567 | original_img = Image.open(image_path)
568 |
569 | # 确保原图像有正确的模式(RGB或RGBA)
570 | if original_img.mode != 'RGBA':
571 | original_img = original_img.convert('RGBA')
572 |
573 | canvas_size = (template_width, template_height)
574 |
575 | # 背景处理
576 | bg_img = original_img.copy()
577 | bg_img = ImageOps.fit(bg_img, canvas_size, method=Image.LANCZOS)
578 | bg_img = bg_img.filter(ImageFilter.GaussianBlur(radius=int(blur_size)))
579 |
580 | # 2. 与指定颜色混合
581 | # 假设 select_suitable_color 和 darken_color 函数存在且正常工作
582 | actual_color = darken_color(background_color, 0.85)
583 |
584 | # 确保 bg_color 是元组形式的RGB颜色
585 | if len(actual_color) >= 3:
586 | bg_color = (int(actual_color[0]), int(actual_color[1]), int(actual_color[2]))
587 | else:
588 | # 默认颜色,以防颜色格式不正确
589 | bg_color = (0, 0, 0)
590 |
591 | # 将背景图片与背景色混合
592 | bg_img_array = np.array(bg_img, dtype=float)
593 | height, width, channels = bg_img_array.shape
594 |
595 | # 创建和背景图片相同大小的颜色数组
596 | bg_color_array = np.zeros_like(bg_img_array)
597 |
598 | # 填充RGB通道
599 | for i in range(min(3, channels)):
600 | bg_color_array[:, :, i] = float(bg_color[i])
601 |
602 | # 如果有Alpha通道,设置为完全不透明
603 | if channels == 4:
604 | bg_color_array[:, :, 3] = 255.0
605 |
606 | # 混合背景图和颜色
607 | blended_bg_array = bg_img_array * (1 - float(color_ratio)) + bg_color_array * float(color_ratio)
608 | blended_bg_array = np.clip(blended_bg_array, 0, 255).astype(np.uint8)
609 |
610 | # 转回PIL图像
611 | mode = 'RGBA' if channels == 4 else 'RGB'
612 | blended_bg_img = Image.fromarray(blended_bg_array, mode)
613 |
614 | if blended_bg_img.mode != 'RGBA':
615 | blended_bg_img = blended_bg_img.convert('RGBA')
616 |
617 | # 3. 从左到右颜色变浅的渐变处理
618 | if lighten_gradient_strength > 0:
619 | gradient_mask = Image.new("L", canvas_size, 0)
620 | draw_mask = ImageDraw.Draw(gradient_mask)
621 |
622 | for x in range(template_width):
623 | max_alpha_for_gradient = int(255 * np.clip(lighten_gradient_strength, 0.0, 1.0))
624 | alpha_value = int((x / template_width) * max_alpha_for_gradient)
625 | draw_mask.line([(x, 0), (x, template_height)], fill=alpha_value)
626 |
627 | # 创建一个白色的叠加层
628 | lighten_layer = Image.new("RGBA", canvas_size, (255, 255, 255, 0))
629 | lighten_layer.putalpha(gradient_mask)
630 |
631 | blended_bg_img = Image.alpha_composite(blended_bg_img, lighten_layer)
632 |
633 | # 4. 添加胶片颗粒效果
634 | # 假设 add_film_grain 函数存在且正常工作
635 | final_bg_img = add_film_grain(blended_bg_img, intensity=0.03)
636 |
637 | return final_bg_img
638 |
639 | def add_film_grain(image, intensity=0.05):
640 | """
641 | 为图像添加胶片颗粒效果
642 |
643 | 参数:
644 | image (PIL.Image): 输入图像
645 | intensity (float): 颗粒强度,范围从0到1
646 |
647 | 返回:
648 | PIL.Image: 添加颗粒效果后的图像
649 | """
650 | # 获取图像模式
651 | mode = image.mode
652 |
653 | # 转换为numpy数组
654 | img_array = np.array(image, dtype=np.float32)
655 |
656 | # 确定通道数
657 | if mode == 'RGBA':
658 | # 只对RGB通道添加噪声
659 | channels = img_array.shape[2]
660 | for i in range(min(3, channels)): # 只处理RGB通道
661 | channel = img_array[:, :, i]
662 | noise = np.random.normal(0, 255 * intensity, channel.shape)
663 | img_array[:, :, i] = np.clip(channel + noise, 0, 255)
664 | else:
665 | # RGB或其他模式
666 | noise = np.random.normal(0, 255 * intensity, img_array.shape)
667 | img_array = np.clip(img_array + noise, 0, 255)
668 |
669 | # 转换回PIL图像
670 | grainy_image = Image.fromarray(img_array.astype(np.uint8), mode)
671 |
672 | return grainy_image
673 |
674 | def is_not_black_white_gray_near(color, threshold=20):
675 | """判断颜色既不是黑、白、灰,也不是接近黑、白。"""
676 | r, g, b = color
677 | if (r < threshold and g < threshold and b < threshold) or \
678 | (r > 255 - threshold and g > 255 - threshold and b > 255 - threshold):
679 | return False
680 | gray_diff_threshold = 10
681 | if abs(r - g) < gray_diff_threshold and abs(g - b) < gray_diff_threshold and abs(r - b) < gray_diff_threshold:
682 | return False
683 | return True
684 |
685 | def rgb_to_hsv(color):
686 | """将 RGB 颜色转换为 HSV 颜色。"""
687 | r, g, b = [x / 255.0 for x in color]
688 | return colorsys.rgb_to_hsv(r, g, b)
689 |
690 | def hsv_to_rgb(h, s, v):
691 | """将 HSV 颜色转换为 RGB 颜色。"""
692 | r, g, b = colorsys.hsv_to_rgb(h, s, v)
693 | return (int(r * 255), int(g * 255), int(b * 255))
694 |
695 | def adjust_to_macaron(h, s, v, target_saturation_range=(0.2, 0.7), target_value_range=(0.55, 0.85)):
696 | """将颜色的饱和度和亮度调整到接近马卡龙色系的范围,同时避免颜色过亮。"""
697 | adjusted_s = min(max(s, target_saturation_range[0]), target_saturation_range[1])
698 | adjusted_v = min(max(v, target_value_range[0]), target_value_range[1])
699 | return adjusted_s, adjusted_v
700 |
701 | def find_dominant_vibrant_colors(image, num_colors=5):
702 | """
703 | 从图像中提取出现次数较多的前 N 种非黑非白非灰的颜色,
704 | 并将其调整到接近马卡龙色系。
705 | """
706 | img = image.copy()
707 | img.thumbnail((100, 100))
708 | img = img.convert('RGB')
709 | pixels = list(img.getdata())
710 | filtered_pixels = [p for p in pixels if is_not_black_white_gray_near(p)]
711 | if not filtered_pixels:
712 | return []
713 | color_counter = Counter(filtered_pixels)
714 | dominant_colors = color_counter.most_common(num_colors * 3) # 提取更多候选
715 |
716 | macaron_colors = []
717 | seen_hues = set() # 避免提取过于相似的颜色
718 |
719 | for color, count in dominant_colors:
720 | h, s, v = rgb_to_hsv(color)
721 | adjusted_s, adjusted_v = adjust_to_macaron(h, s, v)
722 | adjusted_rgb = hsv_to_rgb(h, adjusted_s, adjusted_v)
723 |
724 | # 可以加入一些色调的判断,例如避免过于接近的色调
725 | hue_degree = int(h * 360)
726 | is_similar_hue = any(abs(hue_degree - seen) < 15 for seen in seen_hues) # 15度范围内的色调认为是相似的
727 |
728 | if not is_similar_hue and adjusted_rgb not in macaron_colors:
729 | macaron_colors.append(adjusted_rgb)
730 | seen_hues.add(hue_degree)
731 | if len(macaron_colors) >= num_colors:
732 | break
733 |
734 | return macaron_colors
735 |
736 | def darken_color(color, factor=0.7):
737 | """
738 | 将颜色加深。
739 | """
740 | r, g, b = color
741 | return (int(r * factor), int(g * factor), int(b * factor))
742 |
743 |
744 | def add_film_grain(image, intensity=0.05):
745 | """添加胶片颗粒效果"""
746 | img_array = np.array(image)
747 |
748 | # 创建随机噪点
749 | noise = np.random.normal(0, intensity * 255, img_array.shape)
750 |
751 | # 应用噪点
752 | img_array = img_array + noise
753 | img_array = np.clip(img_array, 0, 255).astype(np.uint8)
754 |
755 | return Image.fromarray(img_array)
756 |
757 | def create_style_multi_1(library_dir, title, font_path, font_size=(1,1), is_blur=False, blur_size=50, color_ratio=0.8):
758 | """
759 | 生成海报:多张图片以旋转列的形式排列在渐变背景上。
760 | 输入:
761 | image_datas_base64: base64编码的图片字符串列表。
762 | title_zh: 中文标题文本。
763 | title_en: 英文标题文本。
764 | zh_font_path: 首选的中文字体文件路径 (可以是None)。
765 | en_font_path: 首选的英文字体文件路径 (可以是None)。
766 | 返回:
767 | 生成的PNG海报图片的base64编码字符串,失败则返回None。
768 | """
769 | """
770 | 将多张电影海报排列成三列,每列三张,然后将每列作为整体旋转并放在渐变背景上
771 | 不再依赖外部模板文件,直接生成渐变背景
772 | """
773 |
774 | try:
775 | zh_font_size_ratio, en_font_size_ratio = font_size
776 |
777 | if int(blur_size) < 0:
778 | blur_size = 50
779 |
780 | if float(color_ratio) < 0 or float(color_ratio) > 1:
781 | color_ratio = 0.8
782 |
783 | if float(zh_font_size_ratio) <= 0:
784 | zh_font_size_ratio = 1
785 | if float(en_font_size_ratio) <= 0:
786 | en_font_size_ratio = 1
787 |
788 | zh_font_path, en_font_path = font_path
789 | title_zh, title_en = title
790 | # logger.info(f"[3/4] 正在生成海报...")
791 | # logger.info("-" * 40)
792 | poster_folder = Path(library_dir)
793 | first_image_path = poster_folder / "1.jpg"
794 | # output_path = os.path.join(cover_path, 'output', f"{library_name}.png")
795 | rows = POSTER_GEN_CONFIG["ROWS"]
796 | cols = POSTER_GEN_CONFIG["COLS"]
797 | margin = POSTER_GEN_CONFIG["MARGIN"]
798 | corner_radius = POSTER_GEN_CONFIG["CORNER_RADIUS"]
799 | rotation_angle = POSTER_GEN_CONFIG["ROTATION_ANGLE"]
800 | start_x = POSTER_GEN_CONFIG["START_X"]
801 | start_y = POSTER_GEN_CONFIG["START_Y"]
802 | column_spacing = POSTER_GEN_CONFIG["COLUMN_SPACING"]
803 | save_columns = POSTER_GEN_CONFIG["SAVE_COLUMNS"]
804 |
805 | # 定义模板尺寸(可以根据需要调整)
806 | template_width = POSTER_GEN_CONFIG["CANVAS_WIDTH"]
807 | template_height = POSTER_GEN_CONFIG["CANVAS_HEIGHT"]
808 |
809 | # 加载首图并处理
810 | color_img = Image.open(first_image_path).convert("RGB")
811 | # 获取前景图中最鲜明的颜色
812 | vibrant_colors = find_dominant_vibrant_colors(color_img)
813 |
814 | # 柔和的颜色备选(马卡龙风格)
815 | soft_colors = [
816 | (237, 159, 77), # 原默认色
817 | (255, 183, 197), # 淡粉色
818 | (186, 225, 255), # 淡蓝色
819 | (255, 223, 186), # 浅橘色
820 | (202, 231, 200), # 淡绿色
821 | (245, 203, 255), # 淡紫色
822 | ]
823 | # 如果有鲜明的颜色,则选择第一个(饱和度最高)作为背景色,否则使用默认颜色
824 | if vibrant_colors:
825 | blur_color = vibrant_colors[0]
826 | else:
827 | blur_color = random.choice(soft_colors) # 默认橙色
828 |
829 | gradient_color = get_poster_primary_color(first_image_path)
830 |
831 | # 创建渐变背景作为模板
832 | if is_blur:
833 | colored_bg_img = create_blur_background(first_image_path, template_width, template_height, blur_color, blur_size, color_ratio)
834 | else:
835 | colored_bg_img = create_gradient_background(template_width, template_height, gradient_color)
836 |
837 | # 创建保存中间文件的文件夹
838 | # output_dir = os.path.dirname(output_path)
839 | # if not os.path.exists(output_dir):
840 | # os.makedirs(output_dir)
841 | # columns_dir = os.path.join(output_dir, "columns")
842 | # if save_columns and not os.path.exists(columns_dir):
843 | # os.makedirs(columns_dir)
844 |
845 | # 支持的图片格式
846 | supported_formats = (".jpg", ".jpeg", ".png", ".bmp", ".gif", ".webp")
847 | # 自定义排序顺序,如果custom_order=123456789,则代表九宫格图第一列第一行(1,1)为1.jpg,第一列第二行(1,2)为2.jpg,第一列第三行(1,3)为3.jpg,(2,1)=4.jpg以此类推,(3,3)=9.jpg
848 | custom_order = "315426987"
849 | # 这个顺序是优先把最开始的两张图1.jpg和2.jpg放在最显眼的位置(1,2)和(2,2),而最后一个9.jpg放在看不见的位置(3,1)
850 | order_map = {num: index for index, num in enumerate(custom_order)}
851 |
852 | # 获取并排序图片
853 | poster_files = sorted(
854 | [
855 | os.path.join(poster_folder, f)
856 | for f in os.listdir(poster_folder)
857 | if os.path.isfile(os.path.join(poster_folder, f))
858 | and f.lower().endswith(supported_formats)
859 | and os.path.splitext(f)[0]
860 | in order_map # 文件名(不含扩展名)必须在自定义顺序里
861 | ],
862 | key=lambda x: order_map[os.path.splitext(os.path.basename(x))[0]],
863 | )
864 |
865 | # 确保至少有一张图片
866 | if not poster_files:
867 | # logger.error(f"错误: 在 {poster_folder} 中没有找到支持的图片文件")
868 | return False
869 |
870 | # 限制最多处理 rows*cols 张图片
871 | max_posters = rows * cols
872 | poster_files = poster_files[:max_posters]
873 |
874 | # 固定海报尺寸
875 | cell_width = POSTER_GEN_CONFIG["CELL_WIDTH"]
876 | cell_height = POSTER_GEN_CONFIG["CELL_HEIGHT"]
877 |
878 | # 将图片分成3组,每组3张
879 | grouped_posters = [
880 | poster_files[i : i + rows] for i in range(0, len(poster_files), rows)
881 | ]
882 |
883 | # 以渐变背景作为起点
884 | result = colored_bg_img.copy()
885 | # 处理每一组(每一列)图片
886 | for col_index, column_posters in enumerate(grouped_posters):
887 | if col_index >= cols:
888 | break
889 |
890 | # 计算当前列的 x 坐标
891 | column_x = start_x + col_index * column_spacing
892 |
893 | # 计算当前列所有图片组合后的高度(包括间距)
894 | column_height = rows * cell_height + (rows - 1) * margin
895 |
896 | # 创建一个透明的画布用于当前列的所有图片,增加宽度以容纳右侧阴影
897 | shadow_extra_width = 20 + 20 * 2 # 右侧阴影需要的额外宽度
898 | shadow_extra_height = 20 + 20 * 2 # 底部阴影需要的额外高度
899 |
900 | # 修改列画布的尺寸,确保有足够空间容纳阴影
901 | column_image = Image.new(
902 | "RGBA",
903 | (cell_width + shadow_extra_width, column_height + shadow_extra_height),
904 | (0, 0, 0, 0),
905 | )
906 |
907 | # 在列画布上放置每张图片
908 | for row_index, poster_path in enumerate(column_posters):
909 | try:
910 | # 打开海报
911 | poster = Image.open(poster_path)
912 |
913 | # 调整海报大小为固定尺寸
914 | # resized_poster = poster.resize(
915 | # (cell_width, cell_height), Image.LANCZOS
916 | # )
917 | resized_poster = ImageOps.fit(poster, (cell_width, cell_height), method=Image.LANCZOS)
918 |
919 | # 创建圆角遮罩(如果需要)
920 | if corner_radius > 0:
921 | # 创建一个透明的遮罩
922 | mask = Image.new("L", (cell_width, cell_height), 0)
923 |
924 | # 绘制圆角
925 | draw = ImageDraw.Draw(mask)
926 | draw.rounded_rectangle(
927 | [(0, 0), (cell_width, cell_height)],
928 | radius=corner_radius,
929 | fill=255,
930 | )
931 |
932 | # 应用遮罩
933 | poster_with_corners = Image.new(
934 | "RGBA", resized_poster.size, (0, 0, 0, 0)
935 | )
936 | poster_with_corners.paste(resized_poster, (0, 0), mask)
937 | resized_poster = poster_with_corners
938 |
939 | # 添加阴影效果到每张海报
940 | resized_poster_with_shadow = add_shadow(
941 | resized_poster,
942 | offset=(20, 20), # 较大的偏移量
943 | shadow_color=(
944 | 0,
945 | 0,
946 | 0,
947 | 216,
948 | ), # 更深的黑色,但不要超过255的透明度
949 | blur_radius=20, # 保持模糊半径
950 | )
951 |
952 | # 计算在列画布上的位置(垂直排列)
953 | y_position = row_index * (cell_height + margin)
954 | x_position = 0 # 一般为0,但在有阴影时可能需要调整
955 |
956 | # 粘贴到列画布上时,不要减去偏移量,确保阴影有空间
957 | column_image.paste(
958 | resized_poster_with_shadow,
959 | (0, y_position), # 不减去偏移量,确保阴影有空间
960 | resized_poster_with_shadow,
961 | )
962 |
963 | except Exception as e:
964 | # logger.error(f"处理图片 {os.path.basename(poster_path)} 时出错: {e}")
965 | continue
966 |
967 | # 保存原始列图像(旋转前)
968 | # if save_columns:
969 | # column_orig_path = os.path.join(
970 | # columns_dir, f"{name}_column_{col_index+1}_original.png"
971 | # )
972 | # column_image.save(column_orig_path)
973 | # # logger.debug(
974 | # f"已保存原始列图像到: {column_orig_path}"
975 | # )
976 |
977 | # 现在我们有了完整的一列图片,准备旋转它
978 | # 创建一个足够大的画布来容纳旋转后的列
979 | rotation_canvas_size = int(
980 | math.sqrt(
981 | (cell_width + shadow_extra_width) ** 2
982 | + (column_height + shadow_extra_height) ** 2
983 | )
984 | * 1.5
985 | )
986 | rotation_canvas = Image.new(
987 | "RGBA", (rotation_canvas_size, rotation_canvas_size), (0, 0, 0, 0)
988 | )
989 |
990 | # 将列图片放在旋转画布的中央
991 | paste_x = (rotation_canvas_size - cell_width) // 2
992 | paste_y = (rotation_canvas_size - column_height) // 2
993 | rotation_canvas.paste(column_image, (paste_x, paste_y), column_image)
994 |
995 | # 旋转整个列
996 | rotated_column = rotation_canvas.rotate(
997 | rotation_angle, Image.BICUBIC, expand=True
998 | )
999 |
1000 | # 保存旋转后的列图像
1001 | # if save_columns:
1002 | # column_rotated_path = os.path.join(
1003 | # columns_dir, f"column_{col_index+1}_rotated.png"
1004 | # )
1005 | # rotated_column.save(column_rotated_path)
1006 | # # logger.debug(
1007 | # f"已保存旋转后的列图像到: {column_rotated_path}"
1008 | # )
1009 |
1010 | # 计算列在模板上的位置(不同的列有不同的y起点)
1011 | column_center_y = start_y + column_height // 2
1012 | column_center_x = column_x
1013 |
1014 | # 根据列索引调整位置
1015 | if col_index == 1: # 中间列
1016 | column_center_x += cell_width - 50
1017 | elif col_index == 2: # 右侧列
1018 | column_center_y += -155
1019 | column_center_x += (cell_width) * 2 - 40
1020 |
1021 | # 计算最终放置位置
1022 | final_x = column_center_x - rotated_column.width // 2 + cell_width // 2
1023 | final_y = column_center_y - rotated_column.height // 2
1024 |
1025 | # 粘贴旋转后的列到结果图像
1026 | result.paste(rotated_column, (final_x, final_y), rotated_column)
1027 |
1028 | # 获取第一张图片的随机点颜色
1029 | if poster_files:
1030 | first_image_path = poster_files[0]
1031 | random_color = get_random_color(first_image_path)
1032 | else:
1033 | # 如果没有图片,生成一个随机颜色
1034 | random_color = (
1035 | random.randint(50, 200),
1036 | random.randint(50, 200),
1037 | random.randint(50, 200),
1038 | 255,
1039 | )
1040 |
1041 | # 根据name匹配template_mapping中的配置
1042 | library_ch_name = title_zh # 默认使用输入的name作为中文名
1043 | library_eng_name = title_en # 默认英文名为空
1044 |
1045 | text_shadow_color = darken_color(blur_color, 0.8)
1046 | result = draw_text_on_image(
1047 | result, library_ch_name, (73.32, 427.34), zh_font_path, "ch.ttf", 163 * float(zh_font_size_ratio),
1048 | shadow=is_blur, shadow_color=text_shadow_color
1049 | )
1050 |
1051 | # 如果有英文名,才添加英文名文字
1052 | if library_eng_name:
1053 | # 动态调整字体大小,但统一使用一个字体大小
1054 | base_font_size = 50 * float(en_font_size_ratio) # 默认字体大小
1055 | line_spacing = base_font_size * 0.1 # 行间距
1056 |
1057 | # 计算行数和调整字体大小
1058 | word_count = len(library_eng_name.split())
1059 | max_chars_per_line = max([len(word) for word in library_eng_name.split()])
1060 |
1061 | # 根据单词数量或最长单词长度调整字体大小
1062 | if max_chars_per_line > 10 or word_count > 3:
1063 | # 字体大小与文本长度成反比
1064 | font_size = (
1065 | base_font_size
1066 | * (10 / max(max_chars_per_line, word_count * 3)) ** 0.8
1067 | )
1068 | # 设置最小字体大小限制,确保文字不会太小
1069 | font_size = max(font_size, 30)
1070 | else:
1071 | font_size = base_font_size
1072 |
1073 | # 打印调试信息
1074 | # logger.debug(f"英文名 '{library_eng_name}' 单词数量: {word_count}, 最长单词长度: {max_chars_per_line}")
1075 | # logger.debug(f"使用字体大小: {font_size:.2f}")
1076 |
1077 |
1078 | # 使用多行文本绘制
1079 | result, line_count = draw_multiline_text_on_image(
1080 | result,
1081 | library_eng_name,
1082 | (124.68, 624.55),
1083 | en_font_path, "en.otf",
1084 | int(font_size),
1085 | line_spacing,
1086 | shadow=is_blur,
1087 | shadow_color=text_shadow_color
1088 | )
1089 |
1090 | # 根据行数调整色块高度
1091 | color_block_position = (84.38, 620.06)
1092 | # 基础高度为55,每增加一行增加(font_size + line_spacing)的高度
1093 | color_block_height = base_font_size + line_spacing + (line_count - 1) * (int(font_size) + line_spacing)
1094 | color_block_size = (21.51, color_block_height)
1095 |
1096 | # logger.debug(f"色块高度调整为: {color_block_height} (行数: {line_count})")
1097 |
1098 | result = draw_color_block(
1099 | result, color_block_position, color_block_size, random_color
1100 | )
1101 | # 保存结果
1102 | def image_to_base64(image, format="auto", quality=85):
1103 | buffer = io.BytesIO()
1104 | if format.lower() == "auto":
1105 | if image.mode == "RGBA" or (image.info.get('transparency') is not None):
1106 | format = "PNG"
1107 | else:
1108 | try:
1109 | image.save(buffer, format="WEBP", quality=quality, optimize=True)
1110 | base64_str = base64.b64encode(buffer.getvalue()).decode('utf-8')
1111 | return base64_str
1112 | except Exception:
1113 | format = "JPEG" # Fallback to JPEG if WebP fails
1114 | if format.lower() == "png":
1115 | image.save(buffer, format="PNG", optimize=True)
1116 | base64_str = base64.b64encode(buffer.getvalue()).decode('utf-8')
1117 | return base64_str
1118 | elif format.lower() == "jpeg":
1119 | image = image.convert("RGB") # Ensure RGB for JPEG
1120 | image.save(buffer, format="JPEG", quality=quality, optimize=True, progressive=True)
1121 | base64_str = base64.b64encode(buffer.getvalue()).decode('utf-8')
1122 | return base64_str
1123 | else:
1124 | raise ValueError(f"Unsupported format: {format}")
1125 |
1126 | return image_to_base64(result)
1127 |
1128 | except Exception as e:
1129 | logger.error(f"创建多图封面时出错: {e}")
1130 | return False
1131 |
--------------------------------------------------------------------------------
/plugins.v2/mediacovergenerator/style_single_1.py:
--------------------------------------------------------------------------------
1 | import base64
2 | import random
3 | import colorsys
4 | from collections import Counter
5 | from io import BytesIO
6 | from pathlib import Path
7 | import math
8 |
9 | import numpy as np
10 | from PIL import Image, ImageDraw, ImageFilter, ImageFont, ImageOps
11 |
12 | from app.log import logger
13 |
14 |
15 | # ========== 配置 ==========
16 | canvas_size = (1920, 1080)
17 |
18 | def is_not_black_white_gray_near(color, threshold=20):
19 | """判断颜色既不是黑、白、灰,也不是接近黑、白。"""
20 | r, g, b = color
21 | if (r < threshold and g < threshold and b < threshold) or \
22 | (r > 255 - threshold and g > 255 - threshold and b > 255 - threshold):
23 | return False
24 | gray_diff_threshold = 10
25 | if abs(r - g) < gray_diff_threshold and abs(g - b) < gray_diff_threshold and abs(r - b) < gray_diff_threshold:
26 | return False
27 | return True
28 |
29 | def rgb_to_hsv(color):
30 | """将 RGB 颜色转换为 HSV 颜色。"""
31 | r, g, b = [x / 255.0 for x in color]
32 | return colorsys.rgb_to_hsv(r, g, b)
33 |
34 | def hsv_to_rgb(h, s, v):
35 | """将 HSV 颜色转换为 RGB 颜色。"""
36 | r, g, b = colorsys.hsv_to_rgb(h, s, v)
37 | return (int(r * 255), int(g * 255), int(b * 255))
38 |
39 | def adjust_color_macaron(color):
40 | """
41 | 调整颜色使其更接近马卡龙风格:
42 | - 如果颜色太暗,增加亮度
43 | - 如果颜色太亮,降低亮度
44 | - 调整饱和度到适当范围
45 | """
46 | h, s, v = rgb_to_hsv(color)
47 |
48 | # 马卡龙风格的理想范围
49 | target_saturation_range = (0.3, 0.7) # 饱和度范围
50 | target_value_range = (0.6, 0.85) # 亮度范围
51 |
52 | # 调整饱和度
53 | if s < target_saturation_range[0]:
54 | s = target_saturation_range[0]
55 | elif s > target_saturation_range[1]:
56 | s = target_saturation_range[1]
57 |
58 | # 调整亮度
59 | if v < target_value_range[0]:
60 | v = target_value_range[0] # 太暗,加亮
61 | elif v > target_value_range[1]:
62 | v = target_value_range[1] # 太亮,加暗
63 |
64 | return hsv_to_rgb(h, s, v)
65 |
66 | def color_distance(color1, color2):
67 | """计算两个颜色在HSV空间中的距离"""
68 | h1, s1, v1 = rgb_to_hsv(color1)
69 | h2, s2, v2 = rgb_to_hsv(color2)
70 |
71 | # 色调在环形空间中,需要特殊处理
72 | h_dist = min(abs(h1 - h2), 1 - abs(h1 - h2))
73 |
74 | # 综合距离,给予色调更高的权重
75 | return h_dist * 5 + abs(s1 - s2) + abs(v1 - v2)
76 |
77 | def find_dominant_macaron_colors(image, num_colors=5):
78 | """
79 | 从图像中提取主要颜色并调整为马卡龙风格:
80 | 1. 过滤掉黑白灰颜色
81 | 2. 从剩余颜色中找到出现频率最高的几种
82 | 3. 调整这些颜色使其接近马卡龙风格
83 | 4. 确保提取的颜色之间有足够的差异
84 | """
85 | # 缩小图片以提高效率
86 | img = image.copy()
87 | img.thumbnail((150, 150))
88 | img = img.convert('RGB')
89 | pixels = list(img.getdata())
90 |
91 | # 过滤掉黑白灰颜色
92 | filtered_pixels = [p for p in pixels if is_not_black_white_gray_near(p)]
93 | if not filtered_pixels:
94 | return []
95 |
96 | # 统计颜色出现频率
97 | color_counter = Counter(filtered_pixels)
98 | candidate_colors = color_counter.most_common(num_colors * 5) # 提取更多候选颜色
99 |
100 | macaron_colors = []
101 | min_color_distance = 0.15 # 颜色差异阈值
102 |
103 | for color, _ in candidate_colors:
104 | # 调整为马卡龙风格
105 | adjusted_color = adjust_color_macaron(color)
106 |
107 | # 检查与已选颜色的差异
108 | if not any(color_distance(adjusted_color, existing) < min_color_distance for existing in macaron_colors):
109 | macaron_colors.append(adjusted_color)
110 | if len(macaron_colors) >= num_colors:
111 | break
112 |
113 | return macaron_colors
114 |
115 | def adjust_background_color(color, darken_factor=0.85):
116 | """
117 | 调整背景色,使其适合作为背景:
118 | - 降低亮度以减少对比度
119 | - 略微降低饱和度
120 | """
121 | h, s, v = rgb_to_hsv(color)
122 | # 降低亮度
123 | v = v * darken_factor
124 | # 略微降低饱和度
125 | s = s * 0.9
126 | return hsv_to_rgb(h, s, v)
127 |
128 | def darken_color(color, factor=0.7):
129 | """
130 | 将颜色加深。
131 | """
132 | r, g, b = color
133 | return (int(r * factor), int(g * factor), int(b * factor))
134 |
135 | def add_film_grain(image, intensity=0.05):
136 | """添加胶片颗粒效果"""
137 | img_array = np.array(image)
138 |
139 | # 创建随机噪点
140 | noise = np.random.normal(0, intensity * 255, img_array.shape)
141 |
142 | # 应用噪点
143 | img_array = img_array + noise
144 | img_array = np.clip(img_array, 0, 255).astype(np.uint8)
145 |
146 | return Image.fromarray(img_array)
147 |
148 | def crop_to_square(img):
149 | """将图片裁剪为正方形"""
150 | width, height = img.size
151 | size = min(width, height)
152 |
153 | left = (width - size) // 2
154 | top = (height - size) // 2
155 | right = left + size
156 | bottom = top + size
157 |
158 | return img.crop((left, top, right, bottom))
159 |
160 | def add_rounded_corners(img, radius=30):
161 | """
162 | 给图片添加圆角,通过超采样技术消除锯齿
163 |
164 | Args:
165 | img: PIL.Image对象
166 | radius: 圆角半径
167 |
168 | Returns:
169 | 带圆角的图片(RGBA模式)
170 | """
171 | # 超采样倍数
172 | factor = 2
173 |
174 | # 获取原始尺寸
175 | width, height = img.size
176 |
177 | # 创建更大尺寸的空白图像(用于超采样)
178 | enlarged_img = img.resize((width * factor, height * factor), Image.Resampling.LANCZOS)
179 | enlarged_img = enlarged_img.convert("RGBA")
180 |
181 | # 创建透明蒙版,尺寸为放大后的尺寸
182 | mask = Image.new('L', (width * factor, height * factor), 0)
183 | draw = ImageDraw.Draw(mask)
184 |
185 | draw.rounded_rectangle([(0, 0), (width * factor, height * factor)],
186 | radius=radius * factor, fill=255)
187 |
188 | # 创建超采样尺寸的透明背景
189 | background = Image.new("RGBA", (width * factor, height * factor), (255, 255, 255, 0))
190 |
191 | # 使用蒙版合成图像(在高分辨率下)
192 | high_res_result = Image.composite(enlarged_img, background, mask)
193 |
194 | # 将结果缩小回原来的尺寸,应用抗锯齿
195 | result = high_res_result.resize((width, height), Image.Resampling.LANCZOS)
196 |
197 | return result
198 |
199 |
200 |
201 | def add_card_shadow(img, offset=(10, 10), radius=10, opacity=0.5):
202 | """给卡片添加更真实的阴影效果"""
203 | # 获取原图尺寸
204 | width, height = img.size
205 |
206 | # 创建一个更大的画布以容纳阴影和旋转后的图像
207 | # 提供足够的边距,确保旋转后阴影不会被截断
208 | padding = max(width, height) // 2
209 | shadow = Image.new("RGBA", (width + padding * 2, height + padding * 2), (0, 0, 0, 0))
210 |
211 | # 在原图轮廓绘制黑色阴影,放置在中心偏移的位置
212 | orig_mask = Image.new("L", (width, height), 255)
213 | rounded_mask = add_rounded_corners(orig_mask, radius).convert("L")
214 |
215 | # 阴影位置计算,从中心位置开始偏移
216 | shadow_x = padding + offset[0]
217 | shadow_y = padding + offset[1]
218 | shadow.paste((0, 0, 0, int(255 * opacity)),
219 | (shadow_x, shadow_y, width + shadow_x, height + shadow_y),
220 | rounded_mask)
221 |
222 | # 模糊阴影以获得更自然的效果
223 | shadow = shadow.filter(ImageFilter.GaussianBlur(radius))
224 |
225 | # 创建结果图像
226 | result = Image.new("RGBA", shadow.size, (0, 0, 0, 0))
227 |
228 | # 先放置阴影
229 | result.paste(shadow, (0, 0), shadow)
230 |
231 | # 放置原图到中心位置
232 | result.paste(img, (padding, padding), img if img.mode == "RGBA" else None)
233 |
234 | return result
235 |
236 | def add_shadow_and_rotate(canvas, img, angle, offset=(10, 10), radius=10, opacity=0.5, center_pos=None):
237 | """
238 | 先创建阴影并旋转放置,然后旋转图像并放置
239 |
240 | Args:
241 | canvas: 目标画布
242 | img: 需要处理的图像
243 | angle: 旋转角度
244 | offset: 阴影偏移
245 | radius: 阴影模糊半径
246 | opacity: 阴影透明度
247 | center_pos: 放置中心位置 (x, y)
248 |
249 | Returns:
250 | 更新后的画布
251 | """
252 | # 获取原图尺寸
253 | width, height = img.size
254 |
255 | # 如果没有指定中心位置,默认使用画布中心
256 | if center_pos is None:
257 | center_pos = (canvas.width // 2, canvas.height // 2)
258 |
259 | # 1. 创建阴影
260 | # 创建一个更大的阴影画布,给阴影留足空间,避免截断
261 | padding = max(radius * 4, 100) # 为阴影提供足够的空间
262 | shadow_size = (width + padding * 2, height + padding * 2)
263 | shadow = Image.new("RGBA", shadow_size, (0, 0, 0, 0))
264 |
265 | # 准备阴影蒙版
266 | mask_size = (width, height)
267 | shadow_mask = Image.new("L", mask_size, 255) # 白色蒙版
268 |
269 | # 如果原图是RGBA模式,使用其透明通道作为蒙版
270 | if img.mode == "RGBA":
271 | shadow_mask = img.split()[3] # 获取Alpha通道作为蒙版
272 |
273 | # 在阴影中心位置创建阴影形状
274 | shadow_center = (padding, padding)
275 | shadow.paste((0, 0, 0, int(255 * opacity)),
276 | (shadow_center[0], shadow_center[1],
277 | shadow_center[0] + width, shadow_center[1] + height),
278 | shadow_mask)
279 |
280 | # 模糊阴影,使用较大的半径确保柔和效果
281 | shadow = shadow.filter(ImageFilter.GaussianBlur(radius))
282 |
283 | # 2. 旋转阴影和图像
284 | # 旋转阴影
285 | rotated_shadow = rotate_image(shadow, angle)
286 | shadow_width, shadow_height = rotated_shadow.size
287 |
288 | # 计算旋转后的阴影位置(考虑偏移)
289 | shadow_x = center_pos[0] - shadow_width // 2 + offset[0]
290 | shadow_y = center_pos[1] - shadow_height // 2 + offset[1]
291 |
292 | # 将阴影粘贴到画布上
293 | canvas.paste(rotated_shadow, (shadow_x, shadow_y), rotated_shadow)
294 |
295 | # 旋转原图
296 | rotated_img = rotate_image(img, angle)
297 | img_width, img_height = rotated_img.size
298 |
299 | # 计算旋转后的图片位置
300 | img_x = center_pos[0] - img_width // 2
301 | img_y = center_pos[1] - img_height // 2
302 |
303 | # 将图片粘贴到画布上
304 | canvas.paste(rotated_img, (img_x, img_y), rotated_img)
305 |
306 | return canvas
307 |
308 |
309 | def rotate_image(img, angle, bg_color=(0, 0, 0, 0)):
310 | """旋转图片并确保不会截断图片内容"""
311 | # expand=True 确保旋转后的图片不会被截断
312 | return img.rotate(angle, Image.BICUBIC, expand=True, fillcolor=bg_color)
313 |
314 |
315 | def create_style_single_1(image_path, title, font_path, font_size=(1,1), blur_size=50, color_ratio=0.8):
316 | try:
317 | zh_font_path, en_font_path = font_path
318 | title_zh, title_en = title
319 | zh_font_size_ratio, en_font_size_ratio = font_size
320 |
321 | if int(blur_size) < 0:
322 | blur_size = 50
323 |
324 | if float(color_ratio) < 0 or float(color_ratio) > 1:
325 | color_ratio = 0.8
326 |
327 | if not float(zh_font_size_ratio) > 0:
328 | zh_font_size_ratio = 1
329 | if not float(en_font_size_ratio) > 0:
330 | en_font_size_ratio = 1
331 |
332 |
333 | num_colors = 6
334 | # 加载原始图片
335 | original_img = Image.open(image_path).convert("RGB")
336 |
337 | # 从图片提取马卡龙风格的颜色
338 | candidate_colors = find_dominant_macaron_colors(original_img, num_colors=num_colors)
339 |
340 | random.shuffle(candidate_colors)
341 | extracted_colors = candidate_colors[:num_colors]
342 |
343 | # 柔和的马卡龙备选颜色
344 | soft_macaron_colors = [
345 | (237, 159, 77), # 杏色
346 | (186, 225, 255), # 淡蓝色
347 | (255, 223, 186), # 浅橘色
348 | (202, 231, 200), # 淡绿色
349 | ]
350 |
351 | # 确保有足够的颜色
352 | while len(extracted_colors) < num_colors:
353 | # 从备选颜色中选择一个与已有颜色差异最大的
354 | if not extracted_colors:
355 | extracted_colors.append(random.choice(soft_macaron_colors))
356 | else:
357 | max_diff = 0
358 | best_color = None
359 | for color in soft_macaron_colors:
360 | min_dist = min(color_distance(color, existing) for existing in extracted_colors)
361 | if min_dist > max_diff:
362 | max_diff = min_dist
363 | best_color = color
364 | extracted_colors.append(best_color or random.choice(soft_macaron_colors))
365 |
366 | # 处理颜色
367 | bg_color = darken_color(extracted_colors[0], 0.85) # 背景色
368 | card_colors = [extracted_colors[1], extracted_colors[2]] # 卡片颜色
369 |
370 | # 2. 背景处理
371 | bg_img = original_img.copy()
372 | bg_img = ImageOps.fit(bg_img, canvas_size, method=Image.LANCZOS)
373 | bg_img = bg_img.filter(ImageFilter.GaussianBlur(radius=int(blur_size))) # 强烈模糊化
374 |
375 | # 将背景图片与背景色混合
376 | bg_img_array = np.array(bg_img, dtype=float)
377 | bg_color_array = np.array([[bg_color]], dtype=float)
378 |
379 | # 混合背景图和颜色 (15% 背景图 + 85% 颜色)
380 | blended_bg = bg_img_array * (1 - float(color_ratio)) + bg_color_array * float(color_ratio)
381 | blended_bg = np.clip(blended_bg, 0, 255).astype(np.uint8)
382 | blended_bg_img = Image.fromarray(blended_bg)
383 |
384 | # 添加胶片颗粒效果增强纹理感
385 | blended_bg_img = add_film_grain(blended_bg_img, intensity=0.03)
386 |
387 | # 创建最终画布
388 | canvas = Image.new("RGBA", canvas_size, (0, 0, 0, 0))
389 | canvas.paste(blended_bg_img)
390 |
391 | # 3. 处理卡片效果
392 | # 裁剪为正方形
393 | square_img = crop_to_square(original_img)
394 |
395 | # 计算卡片尺寸 (画布高度的60%)
396 | card_size = int(canvas_size[1] * 0.7)
397 | square_img = square_img.resize((card_size, card_size), Image.LANCZOS)
398 |
399 | # 准备三张卡片图像
400 | cards = []
401 |
402 | # 主卡片 - 原始图
403 | main_card = add_rounded_corners(square_img, radius=card_size//8)
404 | main_card = main_card.convert("RGBA")
405 |
406 | # 辅助卡片1 (中间层) - 与第二种颜色混合,加深颜色
407 | aux_card1 = square_img.copy().filter(ImageFilter.GaussianBlur(radius=8))
408 | aux_card1_array = np.array(aux_card1, dtype=float)
409 | card_color1_array = np.array([[card_colors[0]]], dtype=float)
410 | # 降低原图比例,增加颜色混合比例
411 | blended_card1 = aux_card1_array * 0.5 + card_color1_array * 0.5
412 | blended_card1 = np.clip(blended_card1, 0, 255).astype(np.uint8)
413 | aux_card1 = Image.fromarray(blended_card1)
414 | aux_card1 = add_rounded_corners(aux_card1, radius=card_size//8)
415 | aux_card1 = aux_card1.convert("RGBA")
416 |
417 | # 辅助卡片2 (底层) - 与第三种颜色混合,加深颜色
418 | aux_card2 = square_img.copy().filter(ImageFilter.GaussianBlur(radius=16))
419 | aux_card2_array = np.array(aux_card2, dtype=float)
420 | card_color2_array = np.array([[card_colors[1]]], dtype=float)
421 | # 降低原图比例,增加颜色混合比例
422 | blended_card2 = aux_card2_array * 0.4 + card_color2_array * 0.6
423 | blended_card2 = np.clip(blended_card2, 0, 255).astype(np.uint8)
424 | aux_card2 = Image.fromarray(blended_card2)
425 | aux_card2 = add_rounded_corners(aux_card2, radius=card_size//8)
426 | aux_card2 = aux_card2.convert("RGBA")
427 |
428 | # 4. 分别添加阴影和旋转
429 | # 计算卡片放置中心位置 (画布右侧)
430 | center_x = int(canvas_size[0] - canvas_size[1] * 0.5) # 稍微左移,给旋转后的卡片留出空间
431 | center_y = int(canvas_size[1] * 0.5)
432 | center_pos = (center_x, center_y)
433 |
434 | # 按照需求指定旋转角度
435 | rotation_angles = [36, 18, 0] # 底层、中间层、顶层的旋转角度
436 |
437 | # 阴影配置
438 | shadow_configs = [
439 | {'offset': (10, 16), 'radius': 12, 'opacity': 0.4}, # 底层卡片阴影配置
440 | {'offset': (15, 22), 'radius': 15, 'opacity': 0.5}, # 中间层卡片阴影配置
441 | {'offset': (20, 26), 'radius': 18, 'opacity': 0.6}, # 顶层卡片阴影配置
442 | ]
443 |
444 | # 创建一个临时画布,用于叠加卡片和阴影效果
445 | cards_canvas = Image.new("RGBA", canvas_size, (0, 0, 0, 0))
446 |
447 | # 从底层到顶层依次添加阴影和卡片
448 | cards = [aux_card2, aux_card1, main_card]
449 |
450 | for i, (card, angle, shadow_config) in enumerate(zip(cards, rotation_angles, shadow_configs)):
451 | # 使用优化后的函数添加阴影和旋转图片
452 | cards_canvas = add_shadow_and_rotate(
453 | cards_canvas,
454 | card,
455 | angle,
456 | offset=shadow_config['offset'],
457 | radius=shadow_config['radius'],
458 | opacity=shadow_config['opacity'],
459 | center_pos=center_pos
460 | )
461 |
462 | # 将裁剪后的卡片画布与背景合并
463 | canvas = Image.alpha_composite(canvas.convert("RGBA"), cards_canvas)
464 |
465 | # 5. 文字处理
466 | text_layer = Image.new('RGBA', canvas_size, (255, 255, 255, 0))
467 | shadow_layer = Image.new("RGBA", canvas_size, (0, 0, 0, 0))
468 |
469 | shadow_draw = ImageDraw.Draw(shadow_layer)
470 | draw = ImageDraw.Draw(text_layer)
471 |
472 | # 计算左侧区域的中心 X 位置 (画布宽度的四分之一处)
473 | left_area_center_x = int(canvas_size[0] * 0.25)
474 | left_area_center_y = canvas_size[1] // 2
475 |
476 | zh_font_size = int(canvas_size[1] * 0.17 * float(zh_font_size_ratio))
477 | en_font_size = int(canvas_size[1] * 0.07 * float(en_font_size_ratio))
478 |
479 | zh_font = ImageFont.truetype(zh_font_path, zh_font_size)
480 | en_font = ImageFont.truetype(en_font_path, en_font_size)
481 |
482 | # 文字颜色和阴影颜色
483 | text_color = (255, 255, 255, 229) # 85% 不透明度
484 | shadow_color = darken_color(bg_color, 0.8) + (75,) # 阴影颜色加透明度
485 | shadow_offset = 12
486 | shadow_alpha = 75
487 |
488 | # 计算中文标题的位置
489 | zh_bbox = draw.textbbox((0, 0), title_zh, font=zh_font)
490 | zh_text_w = zh_bbox[2] - zh_bbox[0]
491 | zh_text_h = zh_bbox[3] - zh_bbox[1]
492 | zh_x = left_area_center_x - zh_text_w // 2
493 | zh_y = left_area_center_y - zh_text_h - en_font_size // 2 - 5
494 |
495 | # 中文标题阴影效果
496 | for offset in range(3, shadow_offset + 1, 2):
497 | current_shadow_color = shadow_color[:3] + (shadow_alpha,)
498 | shadow_draw.text((zh_x + offset, zh_y + offset), title_zh, font=zh_font, fill=current_shadow_color)
499 |
500 | # 中文标题
501 | draw.text((zh_x, zh_y), title_zh, font=zh_font, fill=text_color)
502 |
503 | if title_en:
504 | # 计算英文标题的位置
505 | en_bbox = draw.textbbox((0, 0), title_en, font=en_font)
506 | en_text_w = en_bbox[2] - en_bbox[0]
507 | en_text_h = en_bbox[3] - en_bbox[1]
508 | en_x = left_area_center_x - en_text_w // 2
509 | en_y = zh_y + zh_text_h + en_font_size # 调整英文标题位置,与中文标题有一定间距
510 |
511 | # 英文标题阴影效果
512 | for offset in range(2, shadow_offset // 2 + 1):
513 | current_shadow_color = shadow_color[:3] + (shadow_alpha,)
514 | shadow_draw.text((en_x + offset, en_y + offset), title_en, font=en_font, fill=current_shadow_color)
515 |
516 | # 英文标题
517 | draw.text((en_x, en_y), title_en, font=en_font, fill=text_color)
518 |
519 | blurred_shadow = shadow_layer.filter(ImageFilter.GaussianBlur(radius=shadow_offset))
520 | combined = Image.alpha_composite(canvas, blurred_shadow)
521 | # 合并所有图层
522 | combined = Image.alpha_composite(combined, text_layer)
523 |
524 | # 转为 RGB
525 | # rgb_image = combined.convert("RGB")
526 |
527 | def image_to_base64(image, format="auto", quality=85):
528 | buffer = BytesIO()
529 | if format.lower() == "auto":
530 | if image.mode == "RGBA" or (image.info.get('transparency') is not None):
531 | format = "PNG"
532 | else:
533 | try:
534 | image.save(buffer, format="WEBP", quality=quality, optimize=True)
535 | base64_str = base64.b64encode(buffer.getvalue()).decode('utf-8')
536 | return base64_str
537 | except Exception:
538 | format = "JPEG" # Fallback to JPEG if WebP fails
539 | if format.lower() == "png":
540 | image.save(buffer, format="PNG", optimize=True)
541 | base64_str = base64.b64encode(buffer.getvalue()).decode('utf-8')
542 | return base64_str
543 | elif format.lower() == "jpeg":
544 | image = image.convert("RGB") # Ensure RGB for JPEG
545 | image.save(buffer, format="JPEG", quality=quality, optimize=True, progressive=True)
546 | base64_str = base64.b64encode(buffer.getvalue()).decode('utf-8')
547 | return base64_str
548 | else:
549 | raise ValueError(f"Unsupported format: {format}")
550 |
551 | return image_to_base64(combined)
552 |
553 | except Exception as e:
554 | logger.error(f"创建单图封面时出错: {e}")
555 | return False
--------------------------------------------------------------------------------
/plugins.v2/mediacovergenerator/style_single_2.py:
--------------------------------------------------------------------------------
1 | import base64
2 | import os
3 | import random
4 | import colorsys
5 | from collections import Counter
6 | from io import BytesIO
7 | from pathlib import Path
8 |
9 | import numpy as np
10 | from PIL import Image, ImageDraw, ImageFilter, ImageFont, ImageOps
11 |
12 | from app.log import logger
13 |
14 | # ========== 配置 ==========
15 | canvas_size = (1920, 1080)
16 |
17 | def is_not_black_white_gray_near(color, threshold=20):
18 | """判断颜色既不是黑、白、灰,也不是接近黑、白。"""
19 | r, g, b = color
20 | if (r < threshold and g < threshold and b < threshold) or \
21 | (r > 255 - threshold and g > 255 - threshold and b > 255 - threshold):
22 | return False
23 | gray_diff_threshold = 10
24 | if abs(r - g) < gray_diff_threshold and abs(g - b) < gray_diff_threshold and abs(r - b) < gray_diff_threshold:
25 | return False
26 | return True
27 |
28 | def rgb_to_hsv(color):
29 | """将 RGB 颜色转换为 HSV 颜色。"""
30 | r, g, b = [x / 255.0 for x in color]
31 | return colorsys.rgb_to_hsv(r, g, b)
32 |
33 | def hsv_to_rgb(h, s, v):
34 | """将 HSV 颜色转换为 RGB 颜色。"""
35 | r, g, b = colorsys.hsv_to_rgb(h, s, v)
36 | return (int(r * 255), int(g * 255), int(b * 255))
37 |
38 | def adjust_to_macaron(h, s, v, target_saturation_range=(0.2, 0.7), target_value_range=(0.55, 0.85)):
39 | """将颜色的饱和度和亮度调整到接近马卡龙色系的范围,同时避免颜色过亮。"""
40 | adjusted_s = min(max(s, target_saturation_range[0]), target_saturation_range[1])
41 | adjusted_v = min(max(v, target_value_range[0]), target_value_range[1])
42 | return adjusted_s, adjusted_v
43 |
44 | def find_dominant_vibrant_colors(image, num_colors=5):
45 | """
46 | 从图像中提取出现次数较多的前 N 种非黑非白非灰的颜色,
47 | 并将其调整到接近马卡龙色系。
48 | """
49 | img = image.copy()
50 | img.thumbnail((100, 100))
51 | img = img.convert('RGB')
52 | pixels = list(img.getdata())
53 | filtered_pixels = [p for p in pixels if is_not_black_white_gray_near(p)]
54 | if not filtered_pixels:
55 | return []
56 | color_counter = Counter(filtered_pixels)
57 | dominant_colors = color_counter.most_common(num_colors * 3) # 提取更多候选
58 |
59 | macaron_colors = []
60 | seen_hues = set() # 避免提取过于相似的颜色
61 |
62 | for color, count in dominant_colors:
63 | h, s, v = rgb_to_hsv(color)
64 | adjusted_s, adjusted_v = adjust_to_macaron(h, s, v)
65 | adjusted_rgb = hsv_to_rgb(h, adjusted_s, adjusted_v)
66 |
67 | # 可以加入一些色调的判断,例如避免过于接近的色调
68 | hue_degree = int(h * 360)
69 | is_similar_hue = any(abs(hue_degree - seen) < 15 for seen in seen_hues) # 15度范围内的色调认为是相似的
70 |
71 | if not is_similar_hue and adjusted_rgb not in macaron_colors:
72 | macaron_colors.append(adjusted_rgb)
73 | seen_hues.add(hue_degree)
74 | if len(macaron_colors) >= num_colors:
75 | break
76 |
77 | return macaron_colors
78 |
79 | def darken_color(color, factor=0.7):
80 | """
81 | 将颜色加深。
82 | """
83 | r, g, b = color
84 | return (int(r * factor), int(g * factor), int(b * factor))
85 |
86 |
87 | def add_film_grain(image, intensity=0.05):
88 | """添加胶片颗粒效果"""
89 | img_array = np.array(image)
90 |
91 | # 创建随机噪点
92 | noise = np.random.normal(0, intensity * 255, img_array.shape)
93 |
94 | # 应用噪点
95 | img_array = img_array + noise
96 | img_array = np.clip(img_array, 0, 255).astype(np.uint8)
97 |
98 | return Image.fromarray(img_array)
99 |
100 |
101 | def crop_to_16_9(img):
102 | """直接将图片裁剪为16:9的比例"""
103 | target_ratio = 16 / 9
104 | current_ratio = img.width / img.height
105 |
106 | if current_ratio > target_ratio:
107 | # 图片太宽,裁剪两侧
108 | new_width = int(img.height * target_ratio)
109 | left = (img.width - new_width) // 2
110 | img = img.crop((left, 0, left + new_width, img.height))
111 | else:
112 | # 图片太高,裁剪上下
113 | new_height = int(img.width / target_ratio)
114 | top = (img.height - new_height) // 2
115 | img = img.crop((0, top, img.width, top + new_height))
116 |
117 | return img
118 |
119 |
120 | def align_image_right(img, canvas_size):
121 | """
122 | 将图片调整为与画布相同高度,裁剪出画布60%宽度的部分,
123 | 然后将裁剪后的图片靠右放置(因为左侧40%会被其他内容遮盖)。
124 | """
125 | canvas_width, canvas_height = canvas_size
126 | target_width = int(canvas_width * 0.675) # 只需要画布60%的宽度
127 | img_width, img_height = img.size
128 |
129 | # 计算缩放比例以匹配画布高度
130 | scale_factor = canvas_height / img_height
131 | new_img_width = int(img_width * scale_factor)
132 | resized_img = img.resize((new_img_width, canvas_height), Image.LANCZOS)
133 |
134 | # 检查缩放后的图片是否足够宽以覆盖目标宽度
135 | if new_img_width < target_width:
136 | # 如果图片不够宽,基于宽度而非高度进行缩放
137 | scale_factor = target_width / img_width
138 | new_img_height = int(img_height * scale_factor)
139 | resized_img = img.resize((target_width, new_img_height), Image.LANCZOS)
140 |
141 | # 将图片垂直居中裁剪
142 | if new_img_height > canvas_height:
143 | crop_top = (new_img_height - canvas_height) // 2
144 | resized_img = resized_img.crop((0, crop_top, target_width, crop_top + canvas_height))
145 |
146 | # 创建画布并将图片靠右放置
147 | final_img = Image.new("RGB", canvas_size)
148 | final_img.paste(resized_img, (canvas_width - target_width, 0))
149 | return final_img
150 |
151 | # 以下是原始图片足够宽的情况处理
152 |
153 | # 计算图片中心,确保主体在截取的部分中居中
154 | resized_img_center_x = new_img_width / 2
155 |
156 | # 计算裁剪的左右边界,使目标部分居中
157 | crop_left = max(0, resized_img_center_x - target_width / 2)
158 | # 确保右边界不超过图片宽度
159 | if crop_left + target_width > new_img_width:
160 | crop_left = new_img_width - target_width
161 | crop_right = crop_left + target_width
162 |
163 | # 确保裁剪边界不为负
164 | crop_left = max(0, crop_left)
165 | crop_right = min(new_img_width, crop_right)
166 |
167 | # 进行裁剪
168 | cropped_img = resized_img.crop((int(crop_left), 0, int(crop_right), canvas_height))
169 |
170 | # 创建画布并将裁剪后的图片靠右放置
171 | final_img = Image.new("RGB", canvas_size)
172 | paste_x = canvas_width - cropped_img.width + int(canvas_width * 0.075)
173 | final_img.paste(cropped_img, (paste_x, 0))
174 |
175 | return final_img
176 |
177 | def create_diagonal_mask(size, split_top=0.5, split_bottom=0.33):
178 | """
179 | 创建斜线分割的蒙版。左侧为背景 (255),右侧为前景 (0)。
180 | """
181 | mask = Image.new('L', size, 255)
182 | draw = ImageDraw.Draw(mask)
183 | width, height = size
184 | top_x = int(width * split_top)
185 | bottom_x = int(width * split_bottom)
186 |
187 | # 绘制前景区域 (右侧) - 填充为黑色
188 | draw.polygon(
189 | [
190 | (top_x, 0),
191 | (width, 0),
192 | (width, height),
193 | (bottom_x, height)
194 | ],
195 | fill=0
196 | )
197 |
198 | # 绘制背景区域 (左侧) - 填充为白色
199 | draw.polygon(
200 | [
201 | (0, 0),
202 | (top_x, 0),
203 | (bottom_x, height),
204 | (0, height)
205 | ],
206 | fill=255
207 | )
208 | return mask
209 |
210 | def create_shadow_mask(size, split_top=0.5, split_bottom=0.33, feather_size=40):
211 | """
212 | 创建一个阴影蒙版,用于左侧图片向右侧图片投射阴影
213 | """
214 | width, height = size
215 | top_x = int(width * split_top)
216 | bottom_x = int(width * split_bottom)
217 |
218 | # 创建基础蒙版 - 左侧完全透明,右侧完全不透明
219 | mask = Image.new('L', size, 0)
220 | draw = ImageDraw.Draw(mask)
221 |
222 | # 阴影宽度再缩小一半 (原来的六分之一)
223 | shadow_width = feather_size // 3
224 |
225 | # 绘制阴影区域的多边形 - 向左靠拢
226 | draw.polygon(
227 | [
228 | (top_x - 5, 0), # 向左偏移5像素,确保没有空隙
229 | (top_x - 5 + shadow_width, 0),
230 | (bottom_x - 5 + shadow_width, height),
231 | (bottom_x - 5, height)
232 | ],
233 | fill=255
234 | )
235 |
236 | # 模糊阴影边缘,创造渐变效果,但保持较小的模糊半径
237 | mask = mask.filter(ImageFilter.GaussianBlur(radius=feather_size//3))
238 |
239 | return mask
240 |
241 | def create_style_single_2(image_path, title, font_path, font_size=(1,1), blur_size=50, color_ratio=0.8):
242 | try:
243 | zh_font_path, en_font_path = font_path
244 | title_zh, title_en = title
245 |
246 | zh_font_size_ratio, en_font_size_ratio = font_size
247 |
248 | if int(blur_size) < 0:
249 | blur_size = 50
250 |
251 | if float(color_ratio) < 0 or float(color_ratio) > 1:
252 | color_ratio = 0.8
253 |
254 | if not float(zh_font_size_ratio) > 0:
255 | zh_font_size_ratio = 1
256 | if not float(en_font_size_ratio) > 0:
257 | en_font_size_ratio = 1
258 |
259 | # 定义斜线分割位置
260 | split_top = 0.55 # 顶部分割点在画面五分之三的位置
261 | split_bottom = 0.4 # 底部分割点在画面二分之一的位置
262 |
263 | # 加载前景图片并处理
264 | fg_img_original = Image.open(image_path).convert("RGB")
265 | # 以画面四分之三处为中心处理前景图
266 | fg_img = align_image_right(fg_img_original, canvas_size)
267 |
268 | # 获取前景图中最鲜明的颜色
269 | vibrant_colors = find_dominant_vibrant_colors(fg_img)
270 |
271 | # 柔和的颜色备选(马卡龙风格)
272 | soft_colors = [
273 | (237, 159, 77), # 原默认色
274 | (255, 183, 197), # 淡粉色
275 | (186, 225, 255), # 淡蓝色
276 | (255, 223, 186), # 浅橘色
277 | (202, 231, 200), # 淡绿色
278 | (245, 203, 255), # 淡紫色
279 | ]
280 | # 如果有鲜明的颜色,则选择第一个(饱和度最高)作为背景色,否则使用默认颜色
281 | if vibrant_colors:
282 | bg_color = vibrant_colors[0]
283 | else:
284 | bg_color = random.choice(soft_colors) # 默认橙色
285 | shadow_color = darken_color(bg_color, 0.5) # 加深阴影颜色到50%
286 |
287 | # 加载背景图片
288 | bg_img_original = Image.open(image_path).convert("RGB")
289 | bg_img = ImageOps.fit(bg_img_original, canvas_size, method=Image.LANCZOS)
290 |
291 | # 强烈模糊化背景图
292 | bg_img = bg_img.filter(ImageFilter.GaussianBlur(radius=int(blur_size)))
293 |
294 | # 将背景图片与背景色混合
295 | bg_color = darken_color(bg_color, 0.85)
296 | bg_img_array = np.array(bg_img, dtype=float)
297 | bg_color_array = np.array([[bg_color]], dtype=float)
298 |
299 | # 混合背景图和颜色 (10% 背景图 + 90% 颜色) - 使原图几乎不可见,只保留极少纹理
300 | blended_bg = bg_img_array * (1 - float(color_ratio)) + bg_color_array * float(color_ratio)
301 | blended_bg = np.clip(blended_bg, 0, 255).astype(np.uint8)
302 | blended_bg_img = Image.fromarray(blended_bg)
303 |
304 | # 添加胶片颗粒效果增强纹理感
305 | blended_bg_img = add_film_grain(blended_bg_img, intensity=0.05)
306 |
307 | # 创建斜线分割的蒙版
308 | diagonal_mask = create_diagonal_mask(canvas_size, split_top, split_bottom)
309 |
310 | # 创建基础画布 - 前景图
311 | canvas = fg_img.copy()
312 |
313 | # 创建阴影蒙版 - 使用加深的背景色作为阴影颜色,减小阴影距离
314 | shadow_mask = create_shadow_mask(canvas_size, split_top, split_bottom, feather_size=30)
315 |
316 | # 创建阴影层 - 使用更加深的背景色
317 | shadow_layer = Image.new('RGB', canvas_size, shadow_color)
318 |
319 | # 创建临时画布用于组合
320 | temp_canvas = Image.new('RGB', canvas_size)
321 |
322 | # 应用阴影到前景图(先将阴影应用到前景图上)
323 | temp_canvas.paste(canvas)
324 | temp_canvas.paste(shadow_layer, mask=shadow_mask)
325 |
326 | # 使用蒙版将背景图应用到画布上(背景图会覆盖前景图的左侧部分)
327 | canvas = Image.composite(blended_bg_img, temp_canvas, diagonal_mask)
328 |
329 | # ===== 标题绘制 =====
330 | # 使用RGBA模式进行绘制,以便设置文字透明度
331 |
332 | canvas_rgba = canvas.convert('RGBA')
333 | text_layer = Image.new('RGBA', canvas_size, (255, 255, 255, 0))
334 | shadow_layer = Image.new("RGBA", canvas_size, (0, 0, 0, 0))
335 |
336 | shadow_draw = ImageDraw.Draw(shadow_layer)
337 | draw = ImageDraw.Draw(text_layer)
338 |
339 | # 计算左侧区域的中心 X 位置 (画布宽度的四分之一处)
340 | left_area_center_x = int(canvas_size[0] * 0.25)
341 | left_area_center_y = canvas_size[1] // 2
342 |
343 | zh_font_size = int(canvas_size[1] * 0.17 * float(zh_font_size_ratio))
344 | en_font_size = int(canvas_size[1] * 0.07 * float(en_font_size_ratio))
345 |
346 | zh_font = ImageFont.truetype(str(zh_font_path), zh_font_size)
347 | en_font = ImageFont.truetype(str(en_font_path), en_font_size)
348 |
349 | # 设置80%透明度的文字颜色 (255, 255, 255, 204) - 204是80%不透明度
350 | text_color = (255, 255, 255, 229)
351 | shadow_color = darken_color(bg_color, 0.8) + (75,) # 原始阴影透明度
352 | shadow_offset = 12
353 | shadow_alpha = 75
354 | # 计算中文标题的位置
355 | zh_bbox = draw.textbbox((0, 0), title_zh, font=zh_font)
356 | zh_text_w = zh_bbox[2] - zh_bbox[0]
357 | zh_text_h = zh_bbox[3] - zh_bbox[1]
358 | zh_x = left_area_center_x - zh_text_w // 2
359 | zh_y = left_area_center_y - zh_text_h - en_font_size // 2 - 5
360 |
361 | # 恢复原始的字体阴影效果 - 完全参考原代码
362 | for offset in range(3, shadow_offset + 1, 2):
363 | # shadow_alpha = int(210 * (1 - offset / shadow_offset))
364 | current_shadow_color = shadow_color[:3] + (shadow_alpha,)
365 | shadow_draw.text((zh_x + offset, zh_y + offset), title_zh, font=zh_font, fill=current_shadow_color)
366 |
367 | # 80%透明度的主文字
368 | draw.text((zh_x, zh_y), title_zh, font=zh_font, fill=text_color)
369 |
370 | # 计算英文标题的位置
371 | if title_en:
372 | en_bbox = draw.textbbox((0, 0), title_en, font=en_font)
373 | en_text_w = en_bbox[2] - en_bbox[0]
374 | en_text_h = en_bbox[3] - en_bbox[1]
375 | en_x = left_area_center_x - en_text_w // 2
376 | en_y = zh_y + zh_text_h + en_font_size
377 | # 恢复原始的英文标题阴影效果
378 | for offset in range(2, shadow_offset // 2 + 1):
379 | # shadow_alpha = int(210 * (1 - offset / (shadow_offset // 2)))
380 | current_shadow_color = shadow_color[:3] + (shadow_alpha,)
381 | shadow_draw.text((en_x + offset, en_y + offset), title_en, font=en_font, fill=current_shadow_color)
382 |
383 | # 80%透明度的英文主文字
384 | draw.text((en_x, en_y), title_en, font=en_font, fill=text_color)
385 |
386 | blurred_shadow = shadow_layer.filter(ImageFilter.GaussianBlur(radius=shadow_offset))
387 |
388 | combined = Image.alpha_composite(canvas_rgba, blurred_shadow)
389 | # 把 text_layer 合并到 canvas_rgba 上
390 | combined = Image.alpha_composite(combined, text_layer)
391 |
392 | def image_to_base64(image, format="auto", quality=85):
393 | buffer = BytesIO()
394 | if format.lower() == "auto":
395 | if image.mode == "RGBA" or (image.info.get('transparency') is not None):
396 | format = "PNG"
397 | else:
398 | try:
399 | image.save(buffer, format="WEBP", quality=quality, optimize=True)
400 | base64_str = base64.b64encode(buffer.getvalue()).decode('utf-8')
401 | return base64_str
402 | except Exception:
403 | format = "JPEG" # Fallback to JPEG if WebP fails
404 | if format.lower() == "png":
405 | image.save(buffer, format="PNG", optimize=True)
406 | base64_str = base64.b64encode(buffer.getvalue()).decode('utf-8')
407 | return base64_str
408 | elif format.lower() == "jpeg":
409 | image = image.convert("RGB") # Ensure RGB for JPEG
410 | image.save(buffer, format="JPEG", quality=quality, optimize=True, progressive=True)
411 | base64_str = base64.b64encode(buffer.getvalue()).decode('utf-8')
412 | return base64_str
413 | else:
414 | raise ValueError(f"Unsupported format: {format}")
415 |
416 | return image_to_base64(combined)
417 | except Exception as e:
418 | logger.error(f"创建单图封面时出错: {e}")
419 | return False
420 |
--------------------------------------------------------------------------------