├── .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 | ![插件界面](https://raw.githubusercontent.com/justzerock/MoviePilot-Plugins/main/images/plugin.webp) -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------