Set the maxwidth for images (fix #48)

It didn't look pretty when images where larger than the viewport, and it
"broke" word wrap (because it stretched the phantom, and hence lines
wrapped further, see #34)

Sublime Text's minihtml only supports width and height attributes on
img tags, therefore, we have to determine the aspect ratio of the images
ourselves if we want to set a maxsize (so that we can set the height).

We use a hacky function copy pasted from stackoverflow to determine the
size of common format of images using std lib python.
This commit is contained in:
Mathieu PATUREL
2019-11-16 14:56:03 +11:00
parent c10bc95e54
commit cf68b2c202
10 changed files with 192 additions and 24 deletions

4
.gitattributes vendored
View File

@ -1,3 +1,3 @@
docs/ export-ignore
resources/*.png export-ignore
resources/*.py export-ignore
resources/
!resources/*.base64

View File

@ -7,7 +7,9 @@ original_window: the regular window
preview_window: the window with the markdown file and the preview
"""
import time
import os.path
import struct
import sublime
import sublime_plugin
@ -24,8 +26,10 @@ resources = {}
def plugin_loaded():
global DELAY
resources["base64_404_image"] = get_resource("404.base64")
resources["base64_loading_image"] = get_resource("loading.base64")
resources["base64_404_image"] = parse_image_resource(get_resource("404.base64"))
resources["base64_loading_image"] = parse_image_resource(
get_resource("loading.base64")
)
resources["stylesheet"] = get_resource("stylesheet.css")
# FIXME: how could we make this setting update without restarting sublime text
# and not loading it every update as well
@ -90,7 +94,7 @@ class OpenMarkdownPreviewCommand(sublime_plugin.TextCommand):
markdown_view.set_syntax_file(syntax_file)
markdown_view.settings().set(
MARKDOWN_VIEW_INFOS, {"original_window_id": original_window_id}
MARKDOWN_VIEW_INFOS, {"original_window_id": original_window_id,},
)
def is_enabled(self):
@ -208,11 +212,17 @@ class MarkdownLivePreviewListener(sublime_plugin.EventListener):
total_region = sublime.Region(0, markdown_view.size())
markdown = markdown_view.substr(total_region)
preview_view = markdown_view.window().active_view_in_group(1)
viewport_width = preview_view.viewport_extent()[0]
basepath = os.path.dirname(markdown_view.file_name())
html = markdown2html(
markdown, basepath, partial(self._update_preview, markdown_view), resources,
markdown,
basepath,
partial(self._update_preview, markdown_view),
resources,
viewport_width,
)
print(html)
self.phantom_sets[markdown_view.id()].update(
[
@ -239,6 +249,11 @@ def get_resource(resource):
return sublime.load_resource(path)
def parse_image_resource(text):
width, height, base64_image = text.splitlines()
return base64_image, (int(width), int(height))
# try to reload the resources if we save this file
try:
plugin_loaded()

View File

@ -1 +1,52 @@
# hello world
# Why?
Why do you want to use fancy symbols in your standard monospace font? Obviously to have a fancy prompt like mine :-)
![prompt](https://github.com/gabrielelana/awesome-terminal-fonts/raw/master/why.png)
And because when you live in a terminal a symbol can convey more informations in less space creating a dense and beautiful (for those who have a certain aesthetic taste) informative workspace
Heavily inspired by <https://github.com/Lokaltog/vim-powerline> and the relative patch script from **Kim Silkebækken** (kim.silkebaekken+vim@gmail.com)
## Patching vs Fallback
There are two strategies that could be used to have symbols in a terminal
* you can take a bunch of symbol fonts, your favourite monospace font and merge them together (patching strategy)
* you can use a feature of `freetype2` font engine, basically you can say that whenever the current font doesn't have a glyph for a certain codepoint then fallback and go look into other fonts (fallback strategy)
Initially I used the first strategy, later I switched to the second. The patching strategy it's more reliable and portable, the problem is that you need to patch every monospace font you want to use and patching a single font it's a lot of manual fine tuning. If you want you can find all previous patched fonts in [patching-strategy branch](https://github.com/gabrielelana/awesome-terminal-fonts/tree/patching-strategy)
## Font Maps
Referring to glyphs by codepints (eg. `\uf00c`) in your scripts or shell configuration it's not recommended because icon fonts like [Font Awesome](http://fontawesome.io/) use [code points ranges](https://en.wikipedia.org/wiki/Private_Use_Areas) those ranges are not disciplined by the unicode consortium, every font can associate every glyphs to those codepoints. This means that [Font Awesome](http://fontawesome.io/) can choose to move glyphs around freely, today `\uf00c` is associated to the `check` symbol, tomorrow it can be associated to something else. Moreover, more than one icon font can use the same codepoint for different glyphs and if we want to use them both we need to move one of them. So, if you use a codepoint to refer to a glyph after an update that codepoint can point to another glyph. To avoid this situation you can use the font maps in the `./build` directory, font maps are scripts which define shell variables that give names to glyphs, by sourcing those files in your shell you can refer to glyphs by name (eg. `$CODEPOINT_OF_AWESOME_CHECK`).
TLDR: don't refer to glyphs by codepoints (eg. `\uf00c`) but by name (eg. `$CODEPOINT_OF_AWESOME_CHECK`) to make your scripts and shell configurations resilient to future updates. To do that don't forget to copy font maps (`*.sh` files) in the `./build` directory in your home directory and to source them in your shell startup
## Included Fonts
In this repository you can find a bunch of fonts that I use as symbol fonts with the relative font maps
* **Font Awesome 4.7.0**: `./fonts/fontawesome-regular.ttf`, for further informations and license see http://fortawesome.github.io/Font-Awesome
* **Devicons 1.8.0**: `./fonts/devicons-regular.ttf`, for further informations and license see https://github.com/vorillaz/devicons
* **Octicons 1.0.0**: `./fonts/octicons-regular.ttf`, for further informations and license see https://github.com/blog/1135-the-making-of-octicons
* **Pomicons 1.0.0**: `./fonts/pomicons-regular.ttf`, for further informations and license see https://github.com/gabrielelana/pomicons
## How to install (Linux)
* copy all the fonts from `./build` directory to `~/.fonts` directory
* copy all the font maps (all `*.sh` files) from `./build` directory to `~/.fonts` directory
* run `fc-cache -fv ~/.fonts` to let freetype2 know of those fonts
* customize the configuration file `./config/10-symbols.conf` replacing `PragmataPro` with the name of the font you want to use in the terminal (I will add more fonts in the future so that this step could be skippable)
* copy the above configuration file to `~/.config/fontconfig/conf.d` directory
* source the font maps (`source ~/.fonts/*.sh`) in your shell startup script (eg. `~/.bashrc` or `~/.zshrc`)
### Arch Linux
We have been included in the [official repositories](https://www.archlinux.org/packages/community/any/awesome-terminal-fonts/), so if you are running an Arch Linux
* run `pacman -Syu awesome-terminal-fonts`
## How to install (OSX)
* follow [this detailed instructions](https://github.com/gabrielelana/awesome-terminal-fonts/wiki/OS-X) contributed by [@inkrement](https://github.com/inkrement)
* copy all the fonts maps (all `*.sh` files) from `./build` directory to `~/.fonts` directory
* source the font maps (`source ~/.fonts/*.sh`) in your shell startup script (eg. `~/.bashrc` or `~/.zshrc`)
* If it still doesn't work, consider to use the [patching strategy](#patching-vs-fallback)
## How to install (Windows)
* make sure you have permissions to execute Powershell scripts in your machine. To do so, open Windows Powershell as Administrator and paste & run the following command `Set-ExecutionPolicy RemoteSigned`
* then run the install script `./install.ps1`
## License
[MIT](https://github.com/gabrielelana/awesome-terminal-fonts/blob/master/LICENSE)

View File

@ -5,6 +5,8 @@ and makes it a lot easier to think about, and for anyone who would want to, test
markdown2html is just a pure function
"""
import io
import struct
import os.path
import concurrent.futures
import urllib.request
@ -30,7 +32,7 @@ class LoadingError(Exception):
pass
def markdown2html(markdown, basepath, re_render, resources):
def markdown2html(markdown, basepath, re_render, resources, viewport_width):
""" converts the markdown to html, loads the images and puts in base64 for sublime
to understand them correctly. That means that we are responsible for loading the
images from the internet. Hence, we take in re_render, which is just a function we
@ -58,13 +60,16 @@ def markdown2html(markdown, basepath, re_render, resources):
path = os.path.realpath(os.path.expanduser(os.path.join(basepath, src)))
try:
base64 = get_base64_image(path, re_render)
base64, (width, height) = get_base64_image(path, re_render)
except FileNotFoundError as e:
base64 = resources["base64_404_image"]
base64, (width, height) = resources["base64_404_image"]
except LoadingError:
base64 = resources["base64_loading_image"]
base64, (width, height) = resources["base64_loading_image"]
img_element["src"] = base64
if width > viewport_width:
img_element["width"] = viewport_width
img_element["height"] = viewport_width * (height / width)
# remove comments, because they pollute the console with error messages
for comment_element in soup.find_all(
@ -100,10 +105,12 @@ def get_base64_image(path, re_render):
""" Gets the base64 for the image (local and remote images). re_render is a
callback which is called when we finish loading an image from the internet
to trigger an update of the preview (the image will then be loaded from the cache)
return base64_data, (width, height)
"""
def callback(path, future):
# altering image_cache is "safe" to do because callback is called in the same
# altering images_cache is "safe" to do because callback is called in the same
# thread as add_done_callback:
# > Added callables are called in the order that they were added and are always
# > called in a thread belonging to the process that added them
@ -120,14 +127,23 @@ def get_base64_image(path, re_render):
executor.submit(load_image, path).add_done_callback(partial(callback, path))
raise LoadingError()
with open(path, "rb") as fp:
image = "data:image/png;base64," + base64.b64encode(fp.read()).decode("utf-8")
images_cache[path] = image
return image
with open(path, "rb") as fhandle:
image_content = fhandle.read()
width, height = get_image_size(io.BytesIO(image_content), path)
image = "data:image/png;base64," + base64.b64encode(image_content).decode(
"utf-8"
)
images_cache[path] = image, (width, height)
return images_cache[path]
def load_image(url):
with urllib.request.urlopen(url, timeout=60) as conn:
image_content = conn.read()
width, height = get_image_size(io.BytesIO(image_content), url)
content_type = conn.info().get_content_type()
if "image" not in content_type:
raise ValueError(
@ -135,7 +151,60 @@ def load_image(url):
url, content_type
)
)
return "data:image/png;base64," + base64.b64encode(conn.read()).decode("utf-8")
return (
"data:image/png;base64," + base64.b64encode(image_content).decode("utf-8"),
(width, height),
)
def get_image_size(fhandle, pathlike):
""" Thanks to https://stackoverflow.com/a/20380514/6164984 for providing the basis
of a working solution.
fhandle should be a seekable stream. It's not the best for non-seekable streams,
but in our case, we have to load the whole stream into memory anyway because base64
library only accepts bytes-like objects, and not streams.
pathlike is the filename/path/url of the image so that we can guess the file format
"""
format_ = os.path.splitext(os.path.basename(pathlike))[1][1:]
head = fhandle.read(24)
if len(head) != 24:
return "invalid head"
if format_ == "png":
check = struct.unpack(">i", head[4:8])[0]
if check != 0x0D0A1A0A:
return
width, height = struct.unpack(">ii", head[16:24])
elif format_ == "gif":
width, height = struct.unpack("<HH", head[6:10])
elif format_ == "jpeg":
try:
fhandle.seek(0) # Read 0xff next
size = 2
ftype = 0
while not 0xC0 <= ftype <= 0xCF:
fhandle.seek(size, 1)
byte = fhandle.read(1)
if byte == b"":
fhandle = end
byte = fhandle.read(1)
while ord(byte) == 0xFF:
byte = fhandle.read(1)
ftype = ord(byte)
size = struct.unpack(">H", fhandle.read(2))[0] - 2
# We are at a SOFn block
fhandle.seek(1, 1) # Skip `precision' byte.
height, width = struct.unpack(">HH", fhandle.read(4))
except Exception as e: # IGNORE:W0703
raise e
else:
return "unknown format {!r}".format(format_)
return width, height
def independent_markdown2html(markdown):
@ -143,5 +212,10 @@ def independent_markdown2html(markdown):
markdown,
".",
lambda: None,
{"base64_404_image": "", "base64_loading_image": "", "stylesheet": ""},
{
"base64_404_image": ("", (0, 0)),
"base64_loading_image": ("", (0, 0)),
"stylesheet": "",
},
960,
)

File diff suppressed because one or more lines are too long

View File

@ -1,9 +1,33 @@
""" A small script to convert the images into base64 data """
import struct
from base64 import b64encode
with open("404.png", "rb") as png, open("404.base64", "wb") as base64:
base64.write(b64encode(png.read()))
with open("loading.png", "rb") as png, open("loading.base64", "wb") as base64:
base64.write(b64encode(png.read()))
def get_image_size(fhandle):
"""https://stackoverflow.com/a/20380514/6164984"""
head = fhandle.read(24)
if len(head) != 24:
return
# always going to be png
check = struct.unpack(">i", head[4:8])[0]
if check != 0x0D0A1A0A:
raise ValueError("invalid check (?)")
width, height = struct.unpack(">ii", head[16:24])
return width, height
def make_cache(image_name):
with open("{}.png".format(image_name), "rb") as png, open(
"{}.base64".format(image_name), "wb"
) as base64:
width, height = get_image_size(png)
png.seek(0)
base64.write(bytes("{}\n{}\n".format(width, height), encoding="utf-8"))
base64.write(b64encode(png.read()))
make_cache("404")
make_cache("loading")

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 953 B

After

Width:  |  Height:  |  Size: 7.2 KiB

BIN
resources/loading.xcf Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 953 B