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`
|
## Setup
|
||||||
- `markdown`
|
|
||||||
- `pygments`
|
|
||||||
|
|
||||||
## Start the web app
|
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
# The application will listen on port 8000
|
$ pip install madblog
|
||||||
python -m app
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 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 |