diff --git a/ebook/__init__.py b/ebook/__init__.py index bbf8c41..0cb0dc6 100644 --- a/ebook/__init__.py +++ b/ebook/__init__.py @@ -1,6 +1,8 @@ from .epub import make_epub, EpubFile -from .cover import make_cover -from .cover import make_cover_from_url +from .cover import make_cover, make_cover_from_url +from .image import get_image_from_url +from sites import Image +from bs4 import BeautifulSoup import html import unicodedata @@ -72,7 +74,8 @@ class CoverOptions: height = 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)) - 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)) @@ -82,8 +85,18 @@ def chapter_html(story, titleprefix=None, normalize=False): title = chapter.title or f'#{i}' if hasattr(chapter, '__iter__'): # This is a Section - chapters.extend(chapter_html(chapter, titleprefix=title, normalize=normalize)) + chapters.extend(chapter_html( + chapter, titleprefix=title, normalize=normalize)) 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. for image in chapter.images: # 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: break 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 - contents = chapter.contents + contents = str(soup) if normalize: title = unicodedata.normalize('NFKC', title) contents = unicodedata.normalize('NFKC', contents) chapters.append(EpubFile( title=title, 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: - 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 @@ -127,14 +143,19 @@ def generate_epub(story, cover_options={}, output_filename=None, output_dir=None extra_metadata['Tags'] = ', '.join(story.tags) if extra_metadata: - metadata['extra'] = '\n '.join(f'
{k}
{v}
' for k, v in extra_metadata.items()) + metadata['extra'] = '\n '.join( + f'
{k}
{v}
' for k, v in extra_metadata.items()) - valid_cover_options = ('fontname', 'fontsize', 'width', 'height', 'wrapat', 'bgcolor', 'textcolor', 'cover_url') - 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) + valid_cover_options = ('fontname', 'fontsize', 'width', + 'height', 'wrapat', 'bgcolor', 'textcolor', 'cover_url') + 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: - 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: image = make_cover_from_url(story.cover_url, story.title, story.author) 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 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), - 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(path='images/cover.png', contents=image.read(), filetype='image/png'), + 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(path='images/cover.png', + contents=image.read(), filetype='image/png'), ], metadata, output_dir=output_dir diff --git a/ebook/image.py b/ebook/image.py new file mode 100644 index 0000000..14f8e61 --- /dev/null +++ b/ebook/image.py @@ -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())