mirror of
https://github.com/kemayo/leech
synced 2025-12-06 08:22:56 +01:00
fix(Partial-Fix-to-Issue-#2): Leech can now download images however there is no way of disabling this option and this was only tested with stories from fiction.live
BREAKING CHANGE:
This commit is contained in:
parent
1edde92a9d
commit
71345b2658
2 changed files with 148 additions and 16 deletions
|
|
@ -1,6 +1,8 @@
|
||||||
from .epub import make_epub, EpubFile
|
from .epub import make_epub, EpubFile
|
||||||
from .cover import make_cover
|
from .cover import make_cover, make_cover_from_url
|
||||||
from .cover import make_cover_from_url
|
from .image import get_image_from_url
|
||||||
|
from sites import Image
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
import html
|
import html
|
||||||
import unicodedata
|
import unicodedata
|
||||||
|
|
@ -72,7 +74,8 @@ class CoverOptions:
|
||||||
height = attr.ib(default=None, converter=attr.converters.optional(int))
|
height = attr.ib(default=None, converter=attr.converters.optional(int))
|
||||||
wrapat = attr.ib(default=None, converter=attr.converters.optional(int))
|
wrapat = attr.ib(default=None, converter=attr.converters.optional(int))
|
||||||
bgcolor = attr.ib(default=None, converter=attr.converters.optional(tuple))
|
bgcolor = attr.ib(default=None, converter=attr.converters.optional(tuple))
|
||||||
textcolor = attr.ib(default=None, converter=attr.converters.optional(tuple))
|
textcolor = attr.ib(
|
||||||
|
default=None, converter=attr.converters.optional(tuple))
|
||||||
cover_url = attr.ib(default=None, converter=attr.converters.optional(str))
|
cover_url = attr.ib(default=None, converter=attr.converters.optional(str))
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -82,8 +85,18 @@ def chapter_html(story, titleprefix=None, normalize=False):
|
||||||
title = chapter.title or f'#{i}'
|
title = chapter.title or f'#{i}'
|
||||||
if hasattr(chapter, '__iter__'):
|
if hasattr(chapter, '__iter__'):
|
||||||
# This is a Section
|
# This is a Section
|
||||||
chapters.extend(chapter_html(chapter, titleprefix=title, normalize=normalize))
|
chapters.extend(chapter_html(
|
||||||
|
chapter, titleprefix=title, normalize=normalize))
|
||||||
else:
|
else:
|
||||||
|
soup = BeautifulSoup(chapter.contents, 'html5lib')
|
||||||
|
for count, img in enumerate(soup.find_all('img')):
|
||||||
|
img_contents = get_image_from_url(img['src']).read()
|
||||||
|
chapter.images.append(Image(
|
||||||
|
path=f"images/ch{i}_leechimage_{count}.png",
|
||||||
|
contents=img_contents,
|
||||||
|
content_type='image/png'
|
||||||
|
))
|
||||||
|
img['src'] = f"../images/ch{i}_leechimage_{count}.png"
|
||||||
# Add all pictures on this chapter as well.
|
# Add all pictures on this chapter as well.
|
||||||
for image in chapter.images:
|
for image in chapter.images:
|
||||||
# For/else syntax, check if the image path already exists, if it doesn't add the image.
|
# For/else syntax, check if the image path already exists, if it doesn't add the image.
|
||||||
|
|
@ -92,20 +105,23 @@ def chapter_html(story, titleprefix=None, normalize=False):
|
||||||
if other_file.path == image.path:
|
if other_file.path == image.path:
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
chapters.append(EpubFile(path=image.path, contents=image.contents, filetype=image.content_type))
|
chapters.append(EpubFile(
|
||||||
|
path=image.path, contents=image.contents, filetype=image.content_type))
|
||||||
|
|
||||||
title = titleprefix and f'{titleprefix}: {title}' or title
|
title = titleprefix and f'{titleprefix}: {title}' or title
|
||||||
contents = chapter.contents
|
contents = str(soup)
|
||||||
if normalize:
|
if normalize:
|
||||||
title = unicodedata.normalize('NFKC', title)
|
title = unicodedata.normalize('NFKC', title)
|
||||||
contents = unicodedata.normalize('NFKC', contents)
|
contents = unicodedata.normalize('NFKC', contents)
|
||||||
chapters.append(EpubFile(
|
chapters.append(EpubFile(
|
||||||
title=title,
|
title=title,
|
||||||
path=f'{story.id}/chapter{i + 1}.html',
|
path=f'{story.id}/chapter{i + 1}.html',
|
||||||
contents=html_template.format(title=html.escape(title), text=contents)
|
contents=html_template.format(
|
||||||
|
title=html.escape(title), text=contents)
|
||||||
))
|
))
|
||||||
if story.footnotes:
|
if story.footnotes:
|
||||||
chapters.append(EpubFile(title="Footnotes", path=f'{story.id}/footnotes.html', contents=html_template.format(title="Footnotes", text='\n\n'.join(story.footnotes))))
|
chapters.append(EpubFile(title="Footnotes", path=f'{story.id}/footnotes.html', contents=html_template.format(
|
||||||
|
title="Footnotes", text='\n\n'.join(story.footnotes))))
|
||||||
return chapters
|
return chapters
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -127,14 +143,19 @@ def generate_epub(story, cover_options={}, output_filename=None, output_dir=None
|
||||||
extra_metadata['Tags'] = ', '.join(story.tags)
|
extra_metadata['Tags'] = ', '.join(story.tags)
|
||||||
|
|
||||||
if extra_metadata:
|
if extra_metadata:
|
||||||
metadata['extra'] = '\n '.join(f'<dt>{k}</dt><dd>{v}</dd>' for k, v in extra_metadata.items())
|
metadata['extra'] = '\n '.join(
|
||||||
|
f'<dt>{k}</dt><dd>{v}</dd>' for k, v in extra_metadata.items())
|
||||||
|
|
||||||
valid_cover_options = ('fontname', 'fontsize', 'width', 'height', 'wrapat', 'bgcolor', 'textcolor', 'cover_url')
|
valid_cover_options = ('fontname', 'fontsize', 'width',
|
||||||
cover_options = CoverOptions(**{k: v for k, v in cover_options.items() if k in valid_cover_options})
|
'height', 'wrapat', 'bgcolor', 'textcolor', 'cover_url')
|
||||||
cover_options = attr.asdict(cover_options, filter=lambda k, v: v is not None, retain_collection_types=True)
|
cover_options = CoverOptions(
|
||||||
|
**{k: v for k, v in cover_options.items() if k in valid_cover_options})
|
||||||
|
cover_options = attr.asdict(
|
||||||
|
cover_options, filter=lambda k, v: v is not None, retain_collection_types=True)
|
||||||
|
|
||||||
if cover_options and "cover_url" in cover_options:
|
if cover_options and "cover_url" in cover_options:
|
||||||
image = make_cover_from_url(cover_options["cover_url"], story.title, story.author)
|
image = make_cover_from_url(
|
||||||
|
cover_options["cover_url"], story.title, story.author)
|
||||||
elif story.cover_url:
|
elif story.cover_url:
|
||||||
image = make_cover_from_url(story.cover_url, story.title, story.author)
|
image = make_cover_from_url(story.cover_url, story.title, story.author)
|
||||||
else:
|
else:
|
||||||
|
|
@ -145,10 +166,17 @@ def generate_epub(story, cover_options={}, output_filename=None, output_dir=None
|
||||||
[
|
[
|
||||||
# The cover is static, and the only change comes from the image which we generate
|
# The cover is static, and the only change comes from the image which we generate
|
||||||
EpubFile(title='Cover', path='cover.html', contents=cover_template),
|
EpubFile(title='Cover', path='cover.html', contents=cover_template),
|
||||||
EpubFile(title='Front Matter', path='frontmatter.html', contents=frontmatter_template.format(now=datetime.datetime.now(), **metadata)),
|
EpubFile(title='Front Matter', path='frontmatter.html', contents=frontmatter_template.format(
|
||||||
|
now=datetime.datetime.now(), **metadata)),
|
||||||
*chapter_html(story, normalize=normalize),
|
*chapter_html(story, normalize=normalize),
|
||||||
EpubFile(path='Styles/base.css', contents=requests.Session().get('https://raw.githubusercontent.com/mattharrison/epub-css-starter-kit/master/css/base.css').text, filetype='text/css'),
|
EpubFile(
|
||||||
EpubFile(path='images/cover.png', contents=image.read(), filetype='image/png'),
|
path='Styles/base.css',
|
||||||
|
contents=requests.Session().get(
|
||||||
|
'https://raw.githubusercontent.com/mattharrison/epub-css-starter-kit/master/css/base.css').text,
|
||||||
|
filetype='text/css'
|
||||||
|
),
|
||||||
|
EpubFile(path='images/cover.png',
|
||||||
|
contents=image.read(), filetype='image/png'),
|
||||||
],
|
],
|
||||||
metadata,
|
metadata,
|
||||||
output_dir=output_dir
|
output_dir=output_dir
|
||||||
|
|
|
||||||
104
ebook/image.py
Normal file
104
ebook/image.py
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
# Basically the same as cover.py with some minor differences
|
||||||
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
|
from io import BytesIO
|
||||||
|
import textwrap
|
||||||
|
import requests
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def make_image(
|
||||||
|
message: str,
|
||||||
|
width=600,
|
||||||
|
height=300,
|
||||||
|
fontname="Helvetica",
|
||||||
|
font_size=40,
|
||||||
|
bg_color=(0, 0, 0),
|
||||||
|
textcolor=(255, 255, 255),
|
||||||
|
wrap_at=30
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
This function should only be called if get_image_from_url() fails
|
||||||
|
"""
|
||||||
|
img = Image.new("RGBA", (width, height), bg_color)
|
||||||
|
draw = ImageDraw.Draw(img)
|
||||||
|
|
||||||
|
message = textwrap.fill(message, wrap_at)
|
||||||
|
|
||||||
|
font = _safe_font(fontname, size=font_size)
|
||||||
|
message_size = draw.textsize(message, font=font)
|
||||||
|
draw_text_outlined(
|
||||||
|
draw, ((width - message_size[0]) / 2, 100), message, textcolor, font=font)
|
||||||
|
# draw.text(((width - title_size[0]) / 2, 100), title, textcolor, font=font)
|
||||||
|
|
||||||
|
output = BytesIO()
|
||||||
|
img.save(output, "PNG")
|
||||||
|
output.name = 'cover.png'
|
||||||
|
# writing left the cursor at the end of the file, so reset it
|
||||||
|
output.seek(0)
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
def get_image_from_url(url: str):
|
||||||
|
"""
|
||||||
|
Basically the same as make_cover_from_url()
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
logger.info("Downloading image from " + url)
|
||||||
|
img = requests.Session().get(url)
|
||||||
|
cover = BytesIO(img.content)
|
||||||
|
|
||||||
|
img_format = Image.open(cover).format
|
||||||
|
# The `Image.open` read a few bytes from the stream to work out the
|
||||||
|
# format, so reset it:
|
||||||
|
cover.seek(0)
|
||||||
|
|
||||||
|
if img_format != "PNG":
|
||||||
|
cover = _convert_to_png(cover)
|
||||||
|
except Exception as e:
|
||||||
|
logger.info("Encountered an error downloading cover: " + str(e))
|
||||||
|
cover = make_image("There was a problem downloading this image.")
|
||||||
|
|
||||||
|
return cover
|
||||||
|
|
||||||
|
|
||||||
|
def _convert_to_png(image_bytestream):
|
||||||
|
png_image = BytesIO()
|
||||||
|
Image.open(image_bytestream).save(png_image, format="PNG")
|
||||||
|
png_image.name = 'cover.png'
|
||||||
|
png_image.seek(0)
|
||||||
|
|
||||||
|
return png_image
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_font(preferred, *args, **kwargs):
|
||||||
|
for font in (preferred, "Helvetica", "FreeSans", "Arial"):
|
||||||
|
try:
|
||||||
|
return ImageFont.truetype(*args, font=font, **kwargs)
|
||||||
|
except IOError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# This is pretty terrible, but it'll work regardless of what fonts the
|
||||||
|
# system has. Worst issue: can't set the size.
|
||||||
|
return ImageFont.load_default()
|
||||||
|
|
||||||
|
|
||||||
|
def draw_text_outlined(draw, xy, text, fill=None, font=None, anchor=None):
|
||||||
|
x, y = xy
|
||||||
|
|
||||||
|
# Outline
|
||||||
|
draw.text((x - 1, y), text=text, fill=(0, 0, 0), font=font, anchor=anchor)
|
||||||
|
draw.text((x + 1, y), text=text, fill=(0, 0, 0), font=font, anchor=anchor)
|
||||||
|
draw.text((x, y - 1), text=text, fill=(0, 0, 0), font=font, anchor=anchor)
|
||||||
|
draw.text((x, y + 1), text=text, fill=(0, 0, 0), font=font, anchor=anchor)
|
||||||
|
|
||||||
|
# Fill
|
||||||
|
draw.text(xy, text=text, fill=fill, font=font, anchor=anchor)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
f = make_image(
|
||||||
|
'Test of a Title which is quite long and will require multiple lines')
|
||||||
|
with open('output.png', 'wb') as out:
|
||||||
|
out.write(f.read())
|
||||||
Loading…
Reference in a new issue