Refactored Platypush blog repo.
Removed all the Python logic + templates and styles. Those have now been moved to a stand-alone project (madblog), therefore this repo should only contain the static blog pages and images.
25
README.md
|
@ -1,16 +1,21 @@
|
|||
# Platypush blog pages and engine
|
||||
# Platypush blog pages
|
||||
|
||||
This project provides the pages and the webapp needed by the Platypush blog.
|
||||
This project provides the content of the Platypush blog.
|
||||
|
||||
## Dependencies
|
||||
It uses [`madblog`](https://git.platypush.tech/blacklight/madblog)
|
||||
as a Markdown-based blogging micro-framework.
|
||||
|
||||
- `flask`
|
||||
- `markdown`
|
||||
- `pygments`
|
||||
|
||||
## Start the web app
|
||||
## Setup
|
||||
|
||||
```shell
|
||||
# The application will listen on port 8000
|
||||
python -m app
|
||||
$ pip install madblog
|
||||
```
|
||||
|
||||
## Run
|
||||
|
||||
```shell
|
||||
$ git clone https://git.platypush.tech/platypush/blog.git
|
||||
$ cd blog
|
||||
$ madblog
|
||||
```
|
||||
|
||||
|
|
157
app/__init__.py
|
@ -1,157 +0,0 @@
|
|||
import datetime
|
||||
import os
|
||||
import re
|
||||
|
||||
from glob import glob
|
||||
from typing import Optional
|
||||
|
||||
from flask import Flask, Response, abort, send_from_directory, render_template
|
||||
from markdown import markdown
|
||||
|
||||
from .latex import MarkdownLatex
|
||||
|
||||
basedir = os.path.abspath(os.path.join(os.path.realpath(__file__), '..', '..'))
|
||||
templates_dir = os.path.join(basedir, 'templates')
|
||||
static_dir = os.path.join(basedir, 'static')
|
||||
pages_dir = os.path.join(static_dir, 'pages')
|
||||
img_dir = os.path.join(static_dir, 'img')
|
||||
css_dir = os.path.join(static_dir, 'css')
|
||||
fonts_dir = os.path.join(static_dir, 'fonts')
|
||||
|
||||
app = Flask(__name__, template_folder=templates_dir)
|
||||
|
||||
|
||||
def parse_page_title(page: str) -> str:
|
||||
if page.endswith('.md'):
|
||||
page = page[:-3]
|
||||
|
||||
return page.replace('-', ' ')
|
||||
|
||||
|
||||
def get_page_metadata(page: str) -> dict:
|
||||
if not page.endswith('.md'):
|
||||
page = page + '.md'
|
||||
|
||||
if not os.path.isfile(os.path.join(pages_dir, page)):
|
||||
abort(404)
|
||||
|
||||
metadata = {}
|
||||
with open(os.path.join(pages_dir, page), 'r') as f:
|
||||
metadata['uri'] = '/article/' + page[:-3]
|
||||
|
||||
for line in f.readlines():
|
||||
if not line:
|
||||
continue
|
||||
|
||||
if not (m := re.match(r'^\[//]: # \(([^:]+):\s*([^)]+)\)\s*$', line)):
|
||||
break
|
||||
|
||||
if m.group(1) == 'published':
|
||||
metadata[m.group(1)] = datetime.date.fromisoformat(m.group(2))
|
||||
else:
|
||||
metadata[m.group(1)] = m.group(2)
|
||||
|
||||
return metadata
|
||||
|
||||
|
||||
def get_page(page: str, title: Optional[str] = None, skip_header: bool = False):
|
||||
if not page.endswith('.md'):
|
||||
page = page + '.md'
|
||||
|
||||
metadata = get_page_metadata(page)
|
||||
with open(os.path.join(pages_dir, page), 'r') as f:
|
||||
return render_template('article.html',
|
||||
title=title if title else metadata.get('title', 'Platypush - Blog'),
|
||||
image=metadata.get('image'),
|
||||
description=metadata.get('description'),
|
||||
author=re.match(r'(.+?)\s+<([^>]+>)', metadata['author'])[1] if 'author' in metadata else None,
|
||||
author_email=re.match(r'(.+?)\s+<([^>]+)>', metadata['author'])[2] if 'author' in metadata else None,
|
||||
published=(metadata['published'].strftime('%b %d, %Y')
|
||||
if metadata.get('published') else None),
|
||||
content=markdown(f.read(), extensions=['fenced_code', 'codehilite', MarkdownLatex()]),
|
||||
skip_header=skip_header)
|
||||
|
||||
|
||||
def get_pages(with_content: bool = False, skip_header: bool = False) -> list:
|
||||
return sorted([
|
||||
{
|
||||
'path': path,
|
||||
'content': get_page(path, skip_header=skip_header) if with_content else '',
|
||||
**get_page_metadata(os.path.basename(path)),
|
||||
}
|
||||
for path in glob(os.path.join(pages_dir, '*.md'))
|
||||
], key=lambda page: page.get('published'), reverse=True)
|
||||
|
||||
|
||||
@app.route('/', methods=['GET'])
|
||||
def home_route():
|
||||
return render_template('index.html', pages=get_pages())
|
||||
|
||||
|
||||
@app.route('/favicon.ico', methods=['GET'])
|
||||
def favicon_route():
|
||||
return send_from_directory(img_dir, 'favicon.ico')
|
||||
|
||||
|
||||
@app.route('/img/<img>', methods=['GET'])
|
||||
def img_route(img: str):
|
||||
return send_from_directory(img_dir, img)
|
||||
|
||||
|
||||
@app.route('/css/<style>', methods=['GET'])
|
||||
def css_route(style: str):
|
||||
return send_from_directory(css_dir, style)
|
||||
|
||||
|
||||
@app.route('/fonts/<file>', methods=['GET'])
|
||||
def fonts_route(file: str):
|
||||
return send_from_directory(fonts_dir, file)
|
||||
|
||||
|
||||
@app.route('/article/<article>', methods=['GET'])
|
||||
def article_route(article: str):
|
||||
return get_page(article)
|
||||
|
||||
|
||||
@app.route('/rss', methods=['GET'])
|
||||
def rss_route():
|
||||
pages = get_pages(with_content=True, skip_header=True)
|
||||
|
||||
return Response('''<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<rss version="2.0" xmlns:media="http://search.yahoo.com/mrss/">
|
||||
<channel>
|
||||
<title>Platypush blog feeds</title>
|
||||
<link>http://blog.platypush.tech</link>
|
||||
<description>Insights and inspirational projects using Platypush as an automation platform</description>
|
||||
<category>Programming, automation, Python, machine learning, IoT, smart home</category>
|
||||
<image>
|
||||
<url>https://git.platypush.tech/uploads/-/system/appearance/header_logo/1/icon-256.png</url>
|
||||
<title>Platypush</title>
|
||||
<link>https://git.platypush.tech</link>
|
||||
</image>
|
||||
<pubDate>{last_pub_date}</pubDate>
|
||||
<language>en-us</language>
|
||||
|
||||
{items}
|
||||
</channel>
|
||||
</rss>'''.format(
|
||||
last_pub_date=pages[0]['published'].strftime('%a, %d %b %Y %H:%M:%S GMT'),
|
||||
items='\n\n'.join([
|
||||
'''
|
||||
<item>
|
||||
<title>{title}</title>
|
||||
<link>https://blog.platypush.tech{link}</link>
|
||||
<pubDate>{published}</pubDate>
|
||||
<description><![CDATA[{content}]]></description>
|
||||
<media:content medium="image" url="https://blog.platypush.tech{image}" width="200" height="150" />
|
||||
</item>
|
||||
'''.format(
|
||||
title=page.get('title', '[No Title]'),
|
||||
link=page.get('uri', ''),
|
||||
published=page['published'].strftime('%a, %d %b %Y %H:%M:%S GMT') if 'published' in page else '',
|
||||
content=page.get('content', ''),
|
||||
image=page.get('image', ''),
|
||||
)
|
||||
for page in pages
|
||||
]),
|
||||
), mimetype='application/rss+xml')
|
|
@ -1,18 +0,0 @@
|
|||
import argparse
|
||||
import sys
|
||||
|
||||
from . import app
|
||||
|
||||
|
||||
def get_args():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--host', dest='host', required=False, default='0.0.0.0', help='Bind host/address')
|
||||
parser.add_argument('--port', dest='port', required=False, type=int, default=8000, help='Bind port (default: 8000)')
|
||||
parser.add_argument('--debug', dest='debug', required=False, action='store_true', default=False,
|
||||
help='Enable debug mode (default: False)')
|
||||
|
||||
return parser.parse_known_args(sys.argv[1:])
|
||||
|
||||
|
||||
opts = get_args()[0]
|
||||
app.run(host=opts.host, port=opts.port, debug=opts.debug)
|
248
app/latex.py
|
@ -1,248 +0,0 @@
|
|||
"""
|
||||
Licensed under Public Domain Mark 1.0.
|
||||
See https://creativecommons.org/publicdomain/mark/1.0/
|
||||
Author: Justin Bruce Van Horne <justinvh@gmail.com>
|
||||
|
||||
Python-Markdown LaTeX Extension
|
||||
Adds support for $math mode$ and %text mode%. This plugin supports
|
||||
multiline equations/text.
|
||||
The actual image generation is done via LaTeX/DVI output.
|
||||
It encodes data as base64 so there is no need for images directly.
|
||||
All the work is done in the preprocessor.
|
||||
"""
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
from subprocess import call as rawcall, PIPE
|
||||
|
||||
import markdown
|
||||
|
||||
|
||||
def call(*args, **kwargs):
|
||||
"""
|
||||
Proxy to subprocess.call(), removes timeout argument in case of
|
||||
Python2 because that was only implemented in Python3.
|
||||
"""
|
||||
return rawcall(*args, **kwargs)
|
||||
|
||||
|
||||
# Defines our basic inline image
|
||||
img_expr = '<img class="latex inline math-%s" alt="%s" id="%s" src="data:image/png;base64,%s">'
|
||||
|
||||
# Defines multiline expression image
|
||||
multiline_img_expr = '''<div class="multiline-wrapper">
|
||||
<img class="latex multiline math-%s" alt="%s" id="%s" src="data:image/png;base64,%s"></div>'''
|
||||
|
||||
# Base CSS template
|
||||
img_css = """<style scoped>
|
||||
.multiline-wrapper {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
img.latex.multiline {
|
||||
height: 65%;
|
||||
}
|
||||
|
||||
img.latex.inline {
|
||||
height: .9em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
</style>"""
|
||||
|
||||
# Cache and temp file paths
|
||||
tmpdir = tempfile.gettempdir() + '/markdown-latex'
|
||||
cache_file = tmpdir + '/latex.cache'
|
||||
|
||||
|
||||
class LaTeXPreprocessor(markdown.preprocessors.Preprocessor):
|
||||
# These are our cached expressions that are stored in latex.cache
|
||||
cached = {}
|
||||
|
||||
# Basic LaTex Setup as well as our list of expressions to parse
|
||||
tex_preamble = r"""\documentclass[14pt]{article}
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amsthm}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{bm}
|
||||
\usepackage{graphicx}
|
||||
\usepackage[usenames,dvipsnames]{color}
|
||||
\pagestyle{empty}
|
||||
"""
|
||||
|
||||
# Math TeX extraction regex
|
||||
math_extract_regex = re.compile(r'(.+?)((\\\(.+?\\\))|(\$\$\n.+?\n\$\$\n))(.*)', re.MULTILINE | re.DOTALL)
|
||||
|
||||
# Math TeX matching regex
|
||||
math_match_regex = re.compile(r'\s*(\\\(.+?\\\))|(\$\$\n.+?\n\$\$\n)\s*', re.MULTILINE | re.DOTALL)
|
||||
|
||||
def __init__(self, *_, **__):
|
||||
if not os.path.isdir(tmpdir):
|
||||
os.makedirs(tmpdir)
|
||||
try:
|
||||
with open(cache_file, 'r') as f:
|
||||
self.cached = json.load(f)
|
||||
except (IOError, json.JSONDecodeError):
|
||||
self.cached = {}
|
||||
|
||||
self.config = {
|
||||
("general", "preamble"): "",
|
||||
("dvipng", "args"): "-q -T tight -bg Transparent -z 9 -D 200",
|
||||
("delimiters", "text"): "%",
|
||||
("delimiters", "math"): "$",
|
||||
("delimiters", "preamble"): "%%"}
|
||||
|
||||
def _latex_to_base64(self, tex):
|
||||
"""Generates a base64 representation of TeX string"""
|
||||
|
||||
# Generate the temporary file
|
||||
tmp_file_fd, path = tempfile.mkstemp(dir=tmpdir)
|
||||
with os.fdopen(tmp_file_fd, "w") as tmp_file:
|
||||
tmp_file.write(self.tex_preamble)
|
||||
tmp_file.write(tex)
|
||||
tmp_file.write('\n\\end{document}')
|
||||
|
||||
# compile LaTeX document. A DVI file is created
|
||||
status = call(('latex -halt-on-error -output-directory={:s} {:s}'
|
||||
.format(tmpdir, path)).split(),
|
||||
stdout=PIPE, timeout=10)
|
||||
|
||||
# clean up if the above failed
|
||||
if status:
|
||||
self._cleanup(path, err=True)
|
||||
raise Exception("Couldn't compile LaTeX document." +
|
||||
"Please read '%s.log' for more detail." % path)
|
||||
|
||||
# Run dvipng on the generated DVI file. Use tight bounding box.
|
||||
# Magnification is set to 1200
|
||||
dvi = "%s.dvi" % path
|
||||
png = "%s.png" % path
|
||||
|
||||
# Extract the image
|
||||
cmd = "dvipng %s %s -o %s" % (self.config[("dvipng", "args")], dvi, png)
|
||||
status = call(cmd.split(), stdout=PIPE)
|
||||
|
||||
# clean up if we couldn't make the above work
|
||||
if status:
|
||||
self._cleanup(path, err=True)
|
||||
raise Exception("Couldn't convert LaTeX to image." +
|
||||
"Please read '%s.log' for more detail." % path)
|
||||
|
||||
# Read the png and encode the data
|
||||
try:
|
||||
with open(png, "rb") as png:
|
||||
data = png.read()
|
||||
return base64.b64encode(data)
|
||||
finally:
|
||||
self._cleanup(path)
|
||||
|
||||
@staticmethod
|
||||
def _cleanup(path, err=False):
|
||||
# don't clean up the log if there's an error
|
||||
extensions = ["", ".aux", ".dvi", ".png", ".log"]
|
||||
if err:
|
||||
extensions.pop()
|
||||
|
||||
# now do the actual cleanup, passing on non-existent files
|
||||
for extension in extensions:
|
||||
try:
|
||||
os.remove("%s%s" % (path, extension))
|
||||
except (IOError, OSError):
|
||||
pass
|
||||
|
||||
def run(self, lines):
|
||||
"""Parses the actual page"""
|
||||
# Checks for the LaTeX header
|
||||
use_latex = any(line == '[//]: # (latex: 1)' for line in lines)
|
||||
if not use_latex:
|
||||
return lines
|
||||
|
||||
# Re-creates the entire page so we can parse in a multiline env.
|
||||
page = "\n".join(lines)
|
||||
|
||||
# Adds a preamble mode
|
||||
self.tex_preamble += self.config[("general", "preamble")] + "\n\\begin{document}\n"
|
||||
|
||||
# Figure out our text strings and math-mode strings
|
||||
tex_expr = self.math_extract_regex.findall(page)
|
||||
|
||||
# No sense in doing the extra work
|
||||
if not len(tex_expr):
|
||||
return page.split("\n")
|
||||
|
||||
# Parse the expressions
|
||||
new_cache = {}
|
||||
new_page = ''
|
||||
n_multiline_expressions = 0
|
||||
|
||||
while page:
|
||||
m = self.math_extract_regex.match(page)
|
||||
if not m:
|
||||
new_page += page
|
||||
break
|
||||
|
||||
new_page += m.group(1)
|
||||
math_match = self.math_match_regex.match(m.group(2))
|
||||
if not math_match:
|
||||
new_page += m.group(2)
|
||||
else:
|
||||
expr = m.group(2)
|
||||
is_multiline = math_match.group(2) is not None
|
||||
tex_hash = self.hash(expr)
|
||||
if tex_hash in self.cached:
|
||||
data = self.cached[tex_hash]
|
||||
else:
|
||||
data = self._latex_to_base64(expr).decode()
|
||||
new_cache[tex_hash] = data
|
||||
|
||||
if is_multiline and n_multiline_expressions > 0:
|
||||
new_page += '</p>'
|
||||
new_page += (multiline_img_expr if is_multiline else img_expr) % ('true', expr, tex_hash, data)
|
||||
|
||||
if is_multiline:
|
||||
new_page += '<p>'
|
||||
n_multiline_expressions += 1
|
||||
|
||||
page = m.group(5)
|
||||
|
||||
if n_multiline_expressions > 0:
|
||||
new_page += '</p>'
|
||||
|
||||
# Cache our data
|
||||
self.cached.update(new_cache)
|
||||
with open(cache_file, 'w') as f:
|
||||
json.dump(self.cached, f)
|
||||
|
||||
# Make sure to re-split the lines
|
||||
return new_page.split("\n")
|
||||
|
||||
@staticmethod
|
||||
def hash(tex: str) -> str:
|
||||
return hashlib.sha1(tex.encode()).hexdigest()
|
||||
|
||||
|
||||
class LaTeXPostprocessor(markdown.postprocessors.Postprocessor):
|
||||
"""This post processor extension just allows us to further
|
||||
refine, if necessary, the document after it has been parsed."""
|
||||
|
||||
# noinspection PyMethodMayBeStatic
|
||||
def run(self, text):
|
||||
# Inline a style for default behavior
|
||||
text = img_css + text
|
||||
return text
|
||||
|
||||
|
||||
class MarkdownLatex(markdown.Extension):
|
||||
"""Wrapper for LaTeXPreprocessor"""
|
||||
|
||||
def extendMarkdown(self, md):
|
||||
# Our base LaTeX extension
|
||||
md.preprocessors.add('latex',
|
||||
LaTeXPreprocessor(self), ">html_block")
|
||||
# Our cleanup postprocessing extension
|
||||
md.postprocessors.add('latex',
|
||||
LaTeXPostprocessor(self), ">amp_substitute")
|
10
config.yaml
Normal file
|
@ -0,0 +1,10 @@
|
|||
title: Platypush
|
||||
description: The Platypush blog
|
||||
link: https://blog.platypush.tech
|
||||
home_link: https://platypush.tech
|
||||
categories:
|
||||
- IoT
|
||||
- automation
|
||||
- python
|
||||
- programming
|
||||
- machine learning
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 74 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 206 KiB After Width: | Height: | Size: 206 KiB |
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 85 KiB |
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 86 KiB |
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 86 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 727 KiB After Width: | Height: | Size: 727 KiB |
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 122 KiB |
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 8.7 KiB |
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
Before Width: | Height: | Size: 161 KiB After Width: | Height: | Size: 161 KiB |
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 66 KiB |
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 9.3 KiB |
Before Width: | Height: | Size: 211 KiB After Width: | Height: | Size: 211 KiB |
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 90 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 156 KiB After Width: | Height: | Size: 156 KiB |
Before Width: | Height: | Size: 114 KiB After Width: | Height: | Size: 114 KiB |
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 101 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 57 KiB |
Before Width: | Height: | Size: 158 KiB After Width: | Height: | Size: 158 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 8.6 KiB |
Before Width: | Height: | Size: 140 KiB After Width: | Height: | Size: 140 KiB |
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 109 KiB |
Before Width: | Height: | Size: 110 KiB After Width: | Height: | Size: 110 KiB |
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 87 KiB |
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 84 KiB |
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 221 KiB After Width: | Height: | Size: 221 KiB |
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 7.5 KiB |
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 109 KiB |
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 78 KiB |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 231 KiB After Width: | Height: | Size: 231 KiB |
Before Width: | Height: | Size: 4.4 MiB After Width: | Height: | Size: 4.4 MiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 57 KiB |
Before Width: | Height: | Size: 175 KiB After Width: | Height: | Size: 175 KiB |
Before Width: | Height: | Size: 134 KiB After Width: | Height: | Size: 134 KiB |
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 65 KiB |
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 186 KiB After Width: | Height: | Size: 186 KiB |
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 128 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 72 KiB |
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 63 KiB |
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |