diff --git a/.gitignore b/.gitignore index 85e61a6..b9bc505 100644 --- a/.gitignore +++ b/.gitignore @@ -163,4 +163,6 @@ cython_debug/ test/.static test/.thumbnails -test/**/index.html \ No newline at end of file +test/**/index.html +test/**/.sizelist.json +test/manifest.json \ No newline at end of file diff --git a/README.md b/README.md index 499ce9c..20c597b 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ - **Selective File Extensions:** Ability to specify which file extensions to include in the gallery. - **Ignore Other Files:** Option to ignore files other than those specified by the included extensions. - **Info Tooltips:** Display additional information as tooltips for images if an `info` file is present in the directory. +- **Generate Web Manifest:** Ability to generate a web manifest file for PWA (Progressive Web App) support. ## Requirements @@ -25,13 +26,14 @@ - `Jinja2` library - `Pillow` library - `rich_argparse` library +- `cairosvg` library (for SVG to PNG icon conversion) ## Installation Install the required libraries using pip: ```sh -pip install numpy tqdm Jinja2 Pillow rich-argparse +pip install numpy tqdm Jinja2 Pillow rich-argparse cairosvg ``` ## Usage @@ -56,6 +58,7 @@ The script supports several command-line options to customize its behavior. Belo - `--ignore-other-files`: Ignore files that do not match the specified extensions. - `--exclude-folder FOLDER`: Specify folders to exclude from processing. This option can be specified multiple times. - `--version`: Show the version number of the script and exit. +- `-m, --web-manifest`: Generate a web manifest file. ### Examples @@ -89,6 +92,12 @@ To exclude specific folders and specify file extensions: ./builder.py -p /data/pictures -w https://pictures.example.com -t "My Photo Gallery" --exclude-folder Archives --exclude-folder Temp -e .jpg -e .jpeg -e .png ``` +To generate a web manifest file: + +```sh +./builder.py -p /data/pictures -w https://pictures.example.com -t "My Photo Gallery" -m +``` + ## Notes - The root and web root paths must point to the same folder, one on the filesystem and one on the web server. Use absolute paths. diff --git a/StaticGalleryBuilder.code-workspace b/StaticGalleryBuilder.code-workspace index 6f8144a..b9f8634 100644 --- a/StaticGalleryBuilder.code-workspace +++ b/StaticGalleryBuilder.code-workspace @@ -29,6 +29,30 @@ "gitblame.inlineMessageEnabled": true, "gitblame.inlineMessageFormat": "${author.name}, ${time.ago} • ${commit.summary}", "gitblame.statusBarMessageFormat": "${author.name} (${time.ago})", + "html.format.templating": true, + "html.format.wrapAttributes": "preserve", + "html.format.wrapLineLength": 200, + "html.format.indentHandlebars": true, + "html.suggest.html5": true, + "html.hover.documentation": true, + "html.validate.scripts": true, + "html.validate.styles": true, + "prettier.htmlWhitespaceSensitivity": "css", + "json.schemaDownload.enable": true, + "json.schemas": [ + { + "fileMatch": [ + "manifest.json.j2" + ], + "url": "https://json.schemastore.org/web-manifest-combined.json" + } + ], + "emmet.includeLanguages": { + "jinja-html": "html", + "jinja-css": "css", + "jinja-js": "javascript", + "jinja-json": "json" + }, "[python]": { "editor.defaultFormatter": "ms-python.black-formatter" }, @@ -41,10 +65,9 @@ "[jinja-css]": { "editor.defaultFormatter": "vscode.css-language-features" }, - "html.format.templating": true, - "html.format.wrapAttributes": "preserve", - "html.format.wrapLineLength": 200, - "html.format.indentHandlebars": true + "[jinja-js]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, }, "extensions": { "recommendations": [ @@ -76,8 +99,9 @@ "-t", "Pictures", "--theme", - "themes/kjoe.css", + "themes/default.css", "--use-fancy-folders", + "--web-manifest", "-n" ], "postDebugTask": "Delete Lockfile" @@ -98,6 +122,7 @@ "--theme", "themes/kjoe.css", "--use-fancy-folders", + "--web-manifest", "-n" ], "postDebugTask": "Delete Lockfile 2" diff --git a/builder.py b/builder.py index b260656..d1feb78 100755 --- a/builder.py +++ b/builder.py @@ -14,6 +14,14 @@ from tqdm.auto import tqdm from PIL import Image from rich_argparse import RichHelpFormatter, HelpPreviewAction +try: + import cairosvg + from io import BytesIO + + svgsupport = True +except: + svgsupport = False + import cclicense # fmt: off @@ -23,11 +31,12 @@ FAVICON_PATH = ".static/favicon.ico" GLOBAL_CSS_PATH = ".static/global.css" DEFAULT_THEME_PATH = os.path.join(os.path.abspath(os.path.dirname(__file__)), "themes", "default.css") DEFAULT_AUTHOR = "Author" -VERSION = "1.8.1" +VERSION = "1.9.0" RAW_EXTENSIONS = [".3fr", ".ari", ".arw", ".bay", ".braw", ".crw", ".cr2", ".cr3", ".cap", ".data", ".dcs", ".dcr", ".dng", ".drf", ".eip", ".erf", ".fff", ".gpr", ".iiq", ".k25", ".kdc", ".mdc", ".mef", ".mos", ".mrw", ".nef", ".nrw", ".obm", ".orf", ".pef", ".ptx", ".pxn", ".r3d", ".raf", ".raw", ".rwl", ".rw2", ".rwz", ".sr2", ".srf", ".srw", ".tif", ".tiff", ".x3f"] IMG_EXTENSIONS = [".jpg", ".jpeg"] -EXCLUDES = [".lock", "index.html", ".sizelist.json", ".thumbnails", ".static"] +EXCLUDES = [".lock", "index.html", "manifest.json", ".sizelist.json", ".thumbnails", ".static"] NOT_LIST = ["*/Galleries/*", "Archives"] +ICON_SIZES = ["36x36", "48x48", "72x72", "96x96", "144x144", "192x192", "512x512"] # fmt: on # Initialize Jinja2 environment @@ -36,19 +45,26 @@ thumbnails: List[Tuple[str, str]] = [] info: Dict[str, str] = {} +class Icon: + src: str + type: str + sizes: str + + class Args: - root_directory: str - web_root_url: str - site_title: str - regenerate_thumbnails: bool - non_interactive_mode: bool - use_fancy_folders: bool - license_type: Optional[str] author_name: str - file_extensions: List[str] - theme_path: str - ignore_other_files: bool exclude_folders: List[str] + file_extensions: List[str] + generate_webmanifest: bool + ignore_other_files: bool + license_type: Optional[str] + non_interactive_mode: bool + regenerate_thumbnails: bool + root_directory: str + site_title: str + theme_path: str + use_fancy_folders: bool + web_root_url: str # fmt: off @@ -62,6 +78,7 @@ def parse_arguments() -> Args: parser.add_argument("-l", "--license-type", help="Specify the license type for the images.", choices=["cc-zero", "cc-by", "cc-by-sa", "cc-by-nd", "cc-by-nc", "cc-by-nc-sa", "cc-by-nc-nd"], default=None, dest="license_type", metavar="LICENSE") parser.add_argument("-a", "--author-name", help="Name of the author of the images.", default=DEFAULT_AUTHOR, type=str, dest="author_name", metavar="AUTHOR") parser.add_argument("-e", "--file-extensions", help="File extensions to include (can be specified multiple times).", action="append", dest="file_extensions", metavar="EXTENSION") + parser.add_argument("-m", "--web-manifest", help="Generate a web manifest file.", action="store_true", default=False, dest="generate_webmanifest") parser.add_argument("--theme-path", help="Path to the CSS theme file.", default=DEFAULT_THEME_PATH, type=str, dest="theme_path", metavar="PATH") parser.add_argument("--use-fancy-folders", help="Enable fancy folder view instead of the default Apache directory listing.", action="store_true", default=False, dest="use_fancy_folders") parser.add_argument("--ignore-other-files", help="Ignore files that do not match the specified extensions.", action="store_true", default=False, dest="ignore_other_files") @@ -70,18 +87,19 @@ def parse_arguments() -> Args: parser.add_argument("--generate-help-preview", action=HelpPreviewAction, path="help.svg") parsed_args = parser.parse_args() _args = Args() - _args.root_directory = parsed_args.root_directory - _args.web_root_url = parsed_args.web_root_url - _args.site_title = parsed_args.site_title - _args.regenerate_thumbnails = parsed_args.regenerate_thumbnails - _args.non_interactive_mode = parsed_args.non_interactive_mode - _args.use_fancy_folders = parsed_args.use_fancy_folders - _args.license_type = parsed_args.license_type _args.author_name = parsed_args.author_name - _args.file_extensions = parsed_args.file_extensions - _args.theme_path = parsed_args.theme_path - _args.ignore_other_files = parsed_args.ignore_other_files _args.exclude_folders = parsed_args.exclude_folders + _args.file_extensions = parsed_args.file_extensions + _args.generate_webmanifest = parsed_args.generate_webmanifest + _args.ignore_other_files = parsed_args.ignore_other_files + _args.license_type = parsed_args.license_type + _args.non_interactive_mode = parsed_args.non_interactive_mode + _args.regenerate_thumbnails = parsed_args.regenerate_thumbnails + _args.root_directory = parsed_args.root_directory + _args.site_title = parsed_args.site_title + _args.theme_path = parsed_args.theme_path + _args.use_fancy_folders = parsed_args.use_fancy_folders + _args.web_root_url = parsed_args.web_root_url return _args # fmt: on @@ -105,6 +123,58 @@ def copy_static_files(_args: Args) -> None: shutil.copyfile(_args.theme_path, os.path.join(_args.root_directory, ".static", "theme.css")) +def webmanifest(_args: Args) -> None: + icons: List[Icon] = [] + files = os.listdir(os.path.join(STATIC_FILES_DIR, "icons")) + if svgsupport and any(file.endswith(".svg") for file in files): + svg = [file for file in files if file.endswith(".svg")][0] + icons.append({"src": f"/icons/svg", "type": "image/svg+xml", "sizes": "any"}) + for size in ICON_SIZES: + tmpimg = BytesIO() + sizes = size.split("x") + iconpath = os.path.join(_args.root_directory, ".static", "icons", os.path.splitext(svg)[0] + "-" + size + ".png") + cairosvg.svg2png( + url=os.path.join(STATIC_FILES_DIR, "icons", svg), + write_to=tmpimg, + output_width=int(sizes[0]), + output_height=int(sizes[1]), + scale=1, + ) + with Image.open(tmpimg) as iconfile: + iconfile.save(iconpath, format="PNG") + icons.append({"src": iconpath, "sizes": size, "type": "image/png"}) + else: + for icon in os.listdir(os.path.join(STATIC_FILES_DIR, "icons")): + if not icon.endswith(".png"): + continue + with Image.open(os.path.join(STATIC_FILES_DIR, "icons", icon)) as iconfile: + iconsize = f"{iconfile.size[0]}x{iconfile.size[1]}" + icons.append({"src": f".static/pwa/icons/{icon}", "sizes": iconsize, "type": "image/png"}) + if len(icons) == 0: + print("No icons found in the static/icons folder!") + return + + site_id = urllib.parse.quote(_args.web_root_url.replace("https://", "").replace("http://", "").replace("/", "")) + with open(os.path.join(_args.root_directory, ".static", "theme.css")) as f: + content = f.read() + background_color = ( + content.replace("body{", "body {").split("body {")[1].split("}")[0].split("background-color:")[1].split(";")[0].strip() + ) + theme_color = ( + content.replace(".navbar{", "navbar {").split(".navbar {")[1].split("}")[0].split("background-color:")[1].split(";")[0].strip() + ) + with open(os.path.join(_args.root_directory, "manifest.json"), "w", encoding="utf-8") as f: + manifest = env.get_template("manifest.json.j2") + content = manifest.render( + name=_args.site_title, + icons=icons, + id=site_id, + background_color=background_color, + theme_color=theme_color, + ) + f.write(content) + + def generate_thumbnail(arguments: Tuple[str, str]) -> None: folder, item = arguments path = os.path.join(args.root_directory, ".thumbnails", folder.removeprefix(args.root_directory), os.path.splitext(item)[0]) + ".jpg" @@ -250,6 +320,7 @@ def list_folder(folder: str, title: str) -> None: images=image_chunks, info=_info, allimages=images, + webmanifest=args.generate_webmanifest, ) f.write(content) else: @@ -277,6 +348,10 @@ def main() -> None: print("Copying static files...") copy_static_files(args) + if args.generate_webmanifest: + print("Generating webmanifest...") + webmanifest(args) + if args.non_interactive_mode: print("Generating HTML files...") list_folder(args.root_directory, args.site_title) diff --git a/files/icons/icon.svg b/files/icons/icon.svg new file mode 100644 index 0000000..c2152ab --- /dev/null +++ b/files/icons/icon.svg @@ -0,0 +1,27 @@ + + + + \ No newline at end of file diff --git a/help.svg b/help.svg index d018641..bfc9ace 100644 --- a/help.svg +++ b/help.svg @@ -1,287 +1,182 @@ - diff --git a/requirements.txt b/requirements.txt index ddd9eb0..6b41c3e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ +CairoSVG==2.7.1 Jinja2==3.1.4 numpy==2.0.0 pillow==10.4.0 rich-argparse==1.5.2 -tqdm==4.66.4 +tqdm==4.66.4 \ No newline at end of file diff --git a/templates/index.html.j2 b/templates/index.html.j2 index acad6e3..11da4fe 100644 --- a/templates/index.html.j2 +++ b/templates/index.html.j2 @@ -5,6 +5,9 @@