def save_channel_branding(channels_dir, channel_id, save_banner=False): """ download, save and resize profile [and banner] of a channel """ channel_json = get_channel_json(channel_id) thumbnails = channel_json["snippet"]["thumbnails"] for quality in ("medium", "default"): # high:800px, medium:240px, default:88px if quality in thumbnails.keys(): thumnbail = thumbnails[quality]["url"] break channel_dir = channels_dir.joinpath(channel_id) channel_dir.mkdir(exist_ok=True) profile_path = channel_dir.joinpath("profile.jpg") if not profile_path.exists(): stream_file(thumnbail, profile_path) # resize profile as we only use up 100px/80 sq resize_image(profile_path, width=100, height=100) # currently disabled as per deprecation of the following property # without an alternative way to retrieve it (using the API) # See: https://developers.google.com/youtube/v3/revision_history#september-9,-2020 if save_banner and False: banner = channel_json["brandingSettings"]["image"]["bannerImageUrl"] banner_path = channel_dir.joinpath("banner.jpg") if not banner_path.exists(): stream_file(banner, banner_path)
def add_illustrations(self): src_illus_fpath = self.build_dir / "illustration" # if user provided a custom favicon, retrieve that if not self.conf.favicon: self.conf.favicon = Global.site["BadgeIconUrl"] handle_user_provided_file(source=self.conf.favicon, dest=src_illus_fpath) # convert to PNG (might already be PNG but it's OK) illus_fpath = src_illus_fpath.with_suffix(".png") convert_image(src_illus_fpath, illus_fpath) # resize to appropriate size (ZIM uses 48x48 so we double for retina) for size in (96, 48): resize_image(illus_fpath, width=size, height=size, method="thumbnail") with open(illus_fpath, "rb") as fh: Global.creator.add_illustration(size, fh.read()) # download and add actual favicon (ICO file) favicon_fpath = self.build_dir / "favicon.ico" handle_user_provided_file(source=Global.site["IconUrl"], dest=favicon_fpath) Global.creator.add_item_for("favicon.ico", fpath=favicon_fpath, is_front=False) # download apple-touch-icon Global.creator.add_item( URLItem(url=Global.site["BadgeIconUrl"], path="apple-touch-icon.png"))
def update_metadata(self): # we use title, description, profile and banner of channel/user # or channel of first playlist main_channel_json = get_channel_json(self.main_channel_id) save_channel_branding(self.channels_dir, self.main_channel_id, save_banner=True) self.copy_default_banner(self.main_channel_id) # if a single playlist was requested, use if for names; # otherwise, use main_channel's details. auto_title = (self.playlists[0].title if self.is_playlist and len(self.playlists) == 1 else main_channel_json["snippet"]["title"].strip()) auto_description = (clean_text(self.playlists[0].description) if self.is_playlist and len(self.playlists) == 1 else clean_text( main_channel_json["snippet"]["description"])) self.title = self.title or auto_title or "-" self.description = self.description or auto_description or "-" if self.creator is None: if self.is_single_channel: self.creator = _("Youtube Channel “{title}”").format( title=main_channel_json["snippet"]["title"]) else: self.creator = _("Youtube Channels") self.publisher = self.publisher or "Kiwix" self.tags = self.tags or ["youtube"] if "_videos:yes" not in self.tags: self.tags.append("_videos:yes") # copy our main_channel branding into /(profile|banner).jpg if not supplied if not self.profile_path.exists(): shutil.copy( self.channels_dir.joinpath(self.main_channel_id, "profile.jpg"), self.profile_path, ) if not self.banner_path.exists(): shutil.copy( self.channels_dir.joinpath(self.main_channel_id, "banner.jpg"), self.banner_path, ) # set colors from images if not supplied if self.main_color is None or self.secondary_color is None: profile_main, profile_secondary = get_colors(self.profile_path) self.main_color = self.main_color or profile_main self.secondary_color = self.secondary_color or profile_secondary resize_image( self.profile_path, width=48, height=48, method="thumbnail", dst=self.build_dir.joinpath("favicon.jpg"), )
def test_resize_height(png_image, jpg_image, tmp_path, fmt): src, dst = get_src_dst(tmp_path, fmt, png_image=png_image, jpg_image=jpg_image) width, height = 100, 50 resize_image(src, width, height, dst=dst, method="height") _, th = get_image_size(dst) assert th == height
def test_resize_width(png_image, jpg_image, tmp_path, fmt): src, dst = get_src_dst(tmp_path, fmt, png_image=png_image, jpg_image=jpg_image) width, height = 100, 50 resize_image(src, width, height, dst=dst, method="width") tw, _ = get_image_size(dst) assert tw == width
def test_resize_upscale(png_image, jpg_image, tmp_path, fmt): src, dst = get_src_dst(tmp_path, fmt, png_image=png_image, jpg_image=jpg_image) width, height = 500, 1000 resize_image(src, width, height, dst=dst, method="cover") tw, th = get_image_size(dst) assert tw == width assert th == height
def test_resize_contain(png_image, jpg_image, tmp_path, fmt): src, dst = get_src_dst(tmp_path, fmt, png_image=png_image, jpg_image=jpg_image) width, height = 5, 50 resize_image(src, width, height, dst=dst, method="contain") tw, th = get_image_size(dst) assert tw <= width assert th <= height
def test_resize_thumbnail(png_image, jpg_image, tmp_path, fmt): src, dst = get_src_dst(tmp_path, fmt, png_image=png_image, jpg_image=jpg_image) width, height = 100, 50 resize_image(src, width, height, dst=dst, method="thumbnail") tw, th = get_image_size(dst) assert tw <= width assert th <= height
def check_branding_values(self): """checks that user-supplied images and colors are valid (so to fail early) Images are checked for existence or downloaded then resized Colors are check for validity""" # skip this step if none of related values were supplied if not sum([ bool(x) for x in ( self.profile_image, self.banner_image, self.main_color, self.secondary_color, ) ]): return logger.info("checking your branding files and values") if self.profile_image: if self.profile_image.startswith("http"): stream_file(self.profile_image, self.profile_path) else: if not self.profile_image.exists(): raise IOError( f"--profile image could not be found: {self.profile_image}" ) shutil.move(self.profile_image, self.profile_path) resize_image(self.profile_path, width=100, height=100, method="thumbnail") if self.banner_image: if self.banner_image.startswith("http"): stream_file(self.banner_image, self.banner_path) else: if not self.banner_image.exists(): raise IOError( f"--banner image could not be found: {self.banner_image}" ) shutil.move(self.banner_image, self.banner_path) resize_image(self.banner_path, width=1060, height=175, method="thumbnail") if self.main_color and not is_hex_color(self.main_color): raise ValueError( f"--main-color is not a valid hex color: {self.main_color}") if self.secondary_color and not is_hex_color(self.secondary_color): raise ValueError( f"--secondary_color-color is not a valid hex color: {self.secondary_color}" )
def test_resize_small_image_error(png_image, jpg_image, tmp_path, fmt): src, dst = get_src_dst(tmp_path, fmt, png_image=png_image, jpg_image=jpg_image) width, height = 500, 1000 with pytest.raises(ImageSizeError): resize_image(src, width, height, dst=dst, method="cover", allow_upscaling=False)
def add_favicon(self): favicon_orig = self.build_dir / "favicon" # if user provided a custom favicon, retrieve that if self.favicon: handle_user_provided_file(source=self.favicon, dest=favicon_orig) # otherwise, get thumbnail from database else: # add channel thumbnail as favicon try: favicon_prefix, favicon_data = self.db.get_channel_metadata( self.channel_id)["thumbnail"].split(";base64,", 1) favicon_data = base64.standard_b64decode(favicon_data) # favicon_mime = favicon_prefix.replace("data:", "") with open(favicon_orig, "wb") as fh: fh.write(favicon_data) del favicon_data except Exception as exc: logger.warning( "Unable to extract favicon from DB; using default") logger.exception(exc) # use a default favicon handle_user_provided_file(source=self.templates_dir / "kolibri-logo.png", dest=favicon_orig) # convert to PNG (might already be PNG but it's OK) favicon_fpath = favicon_orig.with_suffix(".png") convert_image(favicon_orig, favicon_fpath) # resize to appropriate size (ZIM uses 48x48 so we double for retina) for size in (96, 48): resize_image(favicon_fpath, width=size, height=size, method="thumbnail") with open(favicon_fpath, "rb") as fh: self.creator.add_illustration(size, fh.read()) # resize to appropriate size (ZIM uses 48x48) resize_image(favicon_fpath, width=96, height=96, method="thumbnail") # generate favicon favicon_ico_path = favicon_fpath.with_suffix(".ico") create_favicon(src=favicon_fpath, dst=favicon_ico_path) self.creator.add_item_for("favicon.png", fpath=favicon_fpath) self.creator.add_item_for("favicon.ico", fpath=favicon_ico_path)
def test_resize_bytestream(png_image, jpg_image, tmp_path, fmt): src, dst = get_src_dst(tmp_path, fmt, png_image=png_image, jpg_image=jpg_image) # copy image content into a bytes stream img = io.BytesIO() with open(src, "rb") as srch: img.write(srch.read()) # resize in place (no dst) width, height = 100, 50 resize_image(img, width, height, method="thumbnail") tw, th = get_image_size(img) assert tw <= width assert th <= height
def add_illustration(self, record=None): if self.favicon_url in self.indexed_urls: return # add illustration from favicon option or in-warc favicon logger.info("Adding illustration from " + (self.favicon_url if record is None else "WARC")) favicon_fname = pathlib.Path(urlparse(self.favicon_url).path).name src_illus_fpath = pathlib.Path(".").joinpath(favicon_fname) # reusing payload from WARC record if record: with open(src_illus_fpath, "wb") as fh: if hasattr(record, "buffered_stream"): record.buffered_stream.seek(0) fh.write(record.buffered_stream.read()) else: fh.write(record.content_stream().read()) # fetching online else: try: handle_user_provided_file(source=self.favicon_url, dest=src_illus_fpath) except Exception as exc: logger.warning( "Unable to retrieve favicon. " "ZIM won't have an illustration: {exc}".format(exc=exc)) return # convert to PNG (might already be PNG but it's OK) illus_fpath = src_illus_fpath.with_suffix(".png") convert_image(src_illus_fpath, illus_fpath) # resize to appropriate size (ZIM uses 48x48 so we double for retina) for size in (96, 48): resize_image(illus_fpath, width=size, height=size, method="thumbnail") with open(illus_fpath, "rb") as fh: self.creator.add_illustration(size, fh.read()) src_illus_fpath.unlink()
def download_jpeg_image_and_convert( self, url, fpath, preset_options={}, resize=None ): """downloads a JPEG image and converts and optimizes it into desired format detected from fpath""" org_jpeg_path = pathlib.Path( tempfile.NamedTemporaryFile(delete=False, suffix=".jpg").name ) save_large_file(url, org_jpeg_path) if resize is not None: resize_image( org_jpeg_path, width=resize[0], height=resize[1], method="cover", ) optimize_image( org_jpeg_path, fpath, convert=True, delete_src=True, **preset_options ) logger.debug(f"Converted {org_jpeg_path} to {fpath} and optimized ")
def process_thumbnail(thumbnail_path, preset): # thumbnail might be WebP as .webp, JPEG as .jpg or WebP as .jpg tmp_thumbnail = thumbnail_path if not thumbnail_path.exists(): logger.debug("We don't have video.webp, thumbnail is .jpg") tmp_thumbnail = thumbnail_path.with_suffix(".jpg") # resize thumbnail. we use max width:248x187px in listing # but our posters are 480x270px resize_image( tmp_thumbnail, width=480, height=270, method="cover", allow_upscaling=True, ) optimize_image(tmp_thumbnail, thumbnail_path, delete_src=True, **preset.options) return True
def get_image_data(self, url: str, **resize_args: dict) -> io.BytesIO: """Bytes stream of an optimized, resized WebP of the source image""" src, webp = io.BytesIO(), io.BytesIO() stream_file(url=url, byte_stream=src) with Image.open(src) as img: img.save(webp, format="WEBP") del src resize_args = resize_args or {} try: resize_image( src=webp, **resize_args, allow_upscaling=False, ) except ImageSizeError as exc: logger.debug(f"Resize Error for {url}: {exc}") return optimize_webp( src=webp, lossless=False, quality=60, method=6, )