sublime中Markdown文档中的图片预览
sublime中Markdown文档中的图片预览
MarkdownImages(废弃)
把这个插件打开的时候,用一段时间,sublime中的图片就会不断的闪动,鼠标也跳来跳去,然后,僵死,然后,不可用。看了一下原因,可能是这个插件里面的图片是不断删掉又重新插入,和sublime自己的输入触发更新图片,重新计算图片的大小和文字高低比较频繁有关系。
另外,里面有一个很重要的误用是,phantom
里面的key,在sublime中,这个key是可以多个图片共用的,而作者把这个key理解为每个图片要生成一个key,这样,就需要自己用全局变量去保存这个key,一旦这个key丢失了,就图片都清理不掉了。实际上,我们用view.id()
来标识这个文件里面的所有的图片就可以了。这样,就可以很方便的把本文件的图片都清掉,也不担心全局变量丢失之后清理不掉图片的问题了。
修改方案
解决方案就是在插入图片的时候进行预览,因为大多数时候,在写作时,我们只关心插入图片的时候,图片是否是正确的,一旦按了保存键,就把所以的图片清空掉,这样,既不需要缓存大量的图片,也不会在写作过程中,显示图片,导致大量的鼠标跳动和卡顿。
这样真是非常顺畅,如果想要预览一下图片,那手动执行markdown_image_show
显示所有的图片,再保存一下就清掉了所有图片。这样就比较符合高效实用的目的了。
代码如下
import sublime
import sublime_plugin
import os
import sys
import subprocess
import struct
import imghdr
import base64
import urllib.request
import urllib.parse
import io
import re
DEBUG = False
def debug(*args, **kwargs):
if DEBUG:
print(*args, **kwargs)
class MarkdownImagePasteObject(object):
settings_file = 'MarkdownImagePaste.sublime-settings'
def __init__(self, *args, **kwgs):
super(MarkdownImagePasteObject, self).__init__(*args, **kwgs)
self.settings = sublime.load_settings(self.settings_file)
self.image_dir = self.settings.get("image_dir", ".images")
self.project_dir = self.settings.get("project_dir")
if not self.project_dir:
self.project_dir = "~/md"
self.project_dir = os.path.normpath(os.path.expanduser(self.project_dir))
class MarkdownImagePasteCommand(MarkdownImagePasteObject, sublime_plugin.TextCommand):
def run(self, edit):
filename = self.get_filename()
if filename is None:
sublime.error_message('Please save the file first!')
return
size = self.paste_image(filename)
# print("size:", size)
if size:
for pos in self.view.sel():
if 'text.html.markdown' in self.view.scope_name(pos.begin()):
width = int(size[0])
hight = int(size[1])
if sys.platform == 'darwin': # Retina screen
width = width // 2
hight = hight // 2
max_width = int(self.settings.get("image_max_width", 900))
if width > max_width:
ratio = max_width / width
width = max_width
hight = int(hight * ratio)
self.view.insert(edit, pos.begin(), '![](%s){width="%d" height="%d"}' % (filename, width, hight))
else:
self.view.insert(edit, pos.begin(), "%s" % filename)
break
# show the pasted image
ImageHandler.show_images_for_filename(self.view, filename)
else:
self.view.run_command("paste")
def get_filename(self):
view = self.view
filename = view.file_name()
if filename is None:
# raise RuntimeError("Please save the file first!")
return None
else:
filename = os.path.normpath(os.path.expanduser(filename))
# create dir in current path with the name of current filename
dirname, _ = os.path.splitext(filename)
sub_dir = dirname[len(self.project_dir) + 1:]
# print("sub_dir", sub_dir)
# create new image file under currentdir/filename_without_ext/filename_without_ext%d.png
fn_without_ext = os.path.basename(dirname)
full_image_dir = os.path.join(self.project_dir, self.image_dir, sub_dir)
# print("full_image_dir", full_image_dir)
if not os.path.lexists(full_image_dir):
os.makedirs(full_image_dir)
i = 0
while True:
# absolute file path
abs_filename = os.path.join(full_image_dir, "%s%d.png" % (fn_without_ext, i))
if not os.path.exists(abs_filename):
break
i += 1
# print("save file: " + abs_filename)
return abs_filename
def paste_image(self, filename):
'''
成功:返回格式为 (width, height),失败:返回 None
'''
# 内部没有pillow的lib,用外包python执行
command = 'python3 "%s" save "%s"' % (os.path.join(os.path.dirname(__file__), 'bin/imageutils.py'), filename)
# print(command)
out = self.run_command(command)
# print("out:" + out + ":end")
if out and out[-2:-1] == "0":
return out.split("\n")[0].split(",")
else:
return None
def run_command(self, cmd):
filename = self.view.file_name()
if filename is None:
cwd = "~"
else:
cwd = os.path.dirname(filename)
# print("cmd %r" % cmd)
proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=cwd, env=os.environ)
try:
outs, errs = proc.communicate(timeout=15)
# print("outs %r %r" % (outs, proc))
except Exception:
proc.kill()
outs, errs = proc.communicate()
print("outs %r, errs %r" % (b'\n'.join(outs.split(b'\r\n')), errs))
if errs is None or len(errs) == 0:
return outs.decode()
class MarkdownImageShowCommand(MarkdownImagePasteObject, sublime_plugin.TextCommand):
"""
Show local images inline.
"""
def run(self, edit, **kwargs):
show_local = kwargs.get('show_local', True)
show_remote = kwargs.get('show_remote', True)
max_width = self.settings.get("image_max_width", 900)
ImageHandler.hide_images(self.view)
ImageHandler.show_images_for_custome(self.view)
class MarkdownImageHideCommand(MarkdownImagePasteObject, sublime_plugin.TextCommand):
"""
Hide all shown images.
"""
def run(self, edit):
ImageHandler.hide_images(self.view)
class MarkdownImageListener(sublime_plugin.EventListener):
def on_post_save(self, view):
print("close images")
ImageHandler.hide_images(view)
def on_close(self, view):
print("close images")
ImageHandler.hide_images(view)
class ImageHandler:
"""
Static class to bundle image handling.
"""
@staticmethod
def show_images_for_standard(view, max_width=None, show_local=True, show_remote=False, base_path=""):
selector = 'markup.underline.link.image.markdown'
ImageHandler.show_images_for_selector(view, selector, max_width, show_local, show_remote, base_path)
@staticmethod
def show_images_for_selector(view, selector, max_width=None, show_local=True, show_remote=False, base_path=""):
img_regs = view.find_by_selector(selector)
ImageHandler.show_images(view, img_regs, max_width, show_local, show_remote, base_path)
@staticmethod
def show_images_for_custome(view, max_width=None, base_path=""):
selector = 'markup.underline.link.image.markdown'
img_regs = view.find_by_selector(selector)
pattern = r'<!-- !\[[^\]]*?\]\(([^\\)]*?)\).*? -->'
uploaded_images = view.find_all(pattern)
for img_reg in uploaded_images:
m = re.match(pattern, view.substr(img_reg))
if m:
img_regs.append(view.find(m.groups()[0], start_pt=img_reg.a))
ImageHandler.show_images(view, img_regs, max_width, True, True, base_path)
@staticmethod
def show_images_for_filename(view, filename, max_width=None, base_path=""):
img_regs = view.find_all(filename)
ImageHandler.show_images(view, img_regs, max_width, True, True, base_path)
@staticmethod
def show_images(view, img_regs, max_width=None, show_local=True, show_remote=False, base_path=""):
'''
img_regs: [(0, 10), (20, 25)] # 图片的起始位置列表
'''
debug("show_images")
if not show_local and not show_remote and not img_regs:
debug("doing nothing")
return
# Note: Excessive size will cause the ST3 window to become blank
# unrecoverably. 900 apperas to be a safe limit,
# but can possibly go higher.
if not max_width or max_width < 0:
max_width = 900
debug("img_regs", img_regs)
# Handling space characters in image links
# Image links not enclosed in <> that contain spaces
# are parsed by sublime as multiple links instead of one.
# Example: "<!-- ![](my file.png) -->" gets parsed as two links: "my" and "file.png".
# We detect when two links are separated only by spaces and merge them
indexes_to_merge = []
for i, (left_reg, right_reg) in enumerate(zip(img_regs, img_regs[1:])):
inter_region = sublime.Region(left_reg.end(), right_reg.begin())
if (view.substr(inter_region)).isspace():
# the inter_region is all spaces
# Noting that left and right regions must be merged
indexes_to_merge += [i+1]
new_img_regs = []
for i in range(len(img_regs)):
if i in indexes_to_merge:
new_img_regs[-1] = new_img_regs[-1].cover(img_regs[i])
else:
new_img_regs += [img_regs[i]]
img_regs = new_img_regs
for region in reversed(img_regs):
ttype = None
rel_p = view.substr(region)
# If an image link is enclosed in <> to tolerate spaces in it,
# then the > appears at the end of rel_p for some reason.
# This character makes the link invalid, so it must be removed
if rel_p[-1] == '>':
rel_p = rel_p[0:-1]
# (Windows) cutting the drive letter from the path,
# otherwise urlparse interprets it as a scheme (like 'file' or 'http')
# and generates a bogus url object like:
# url= ParseResult(scheme='c', netloc='', path='/path/image.png', params='', query='', fragment='')
drive_letter, rel_p = os.path.splitdrive(rel_p)
url = urllib.parse.urlparse(rel_p)
if url.scheme and url.scheme != 'file':
if not show_remote:
continue
# We can't render SVG images, so skip the request
# Note: not all URLs that return SVG end with .svg
# We could use a HEAD request to check the Content-Type before
# downloading the image, but the size of an SVG is typically
# so small to not be worth the extra request
if url.path.endswith('.svg'):
continue
debug("image url", rel_p)
try:
data = urllib.request.urlopen(rel_p)
except Exception as e:
debug("Failed to open URL {}:".format(rel_p), e)
continue
try:
data = data.read()
except Exception as e:
debug("Failed to read data from URL {}:".format(rel_p), e)
continue
try:
w, h, ttype = get_image_size(io.BytesIO(data))
except Exception as e:
msg = "Failed to get_image_size for data from URL {}"
debug(msg.format(rel_p), e)
continue
FMT = u'''
<img src="data:image/{}" class="centerImage" {}>
'''
b64_data = base64.encodebytes(data).decode('ascii')
b64_data = b64_data.replace('\n', '')
img = "{};base64,{}".format(ttype, b64_data)
else:
if not show_local:
continue
# Convert relative paths to be relative to the current file
# or project folder.
# NOTE: if the current file or project folder can't be
# determined (e.g. if the view content is not in a project and
# hasn't been saved), then it will anchor to /.
path = url.path
# Force paths to be prefixed with base_path if it was provided
# in settings.
if base_path:
path = os.path.join(base_path, path)
if not os.path.isabs(path):
folder = get_path_for(view)
path = os.path.join(folder, path)
path = os.path.normpath(path)
# (Windows) Adding back the drive letter that was cut from the path before
path = drive_letter + path
url = url._replace(scheme='file', path=path)
FMT = '''
<img src="{}" class="centerImage" {}>
'''
try:
w, h, ttype = get_file_image_size(path)
debug(w,h,ttype)
except Exception as e:
debug("Failed to load {}:".format(path), e)
continue
img = urllib.parse.urlunparse(url)
# On Windows, urlunparse adds a third slash after 'file://' for some reason
# This breaks the image url, so it must be removed
# splitdrive() detects windows because it only returns something if the
# path contains a drive letter
if os.path.splitdrive(path)[0]:
img = img.replace('file:///', 'file://', 1)
if not ttype:
debug("unknown ttype")
continue
# If only width or height are provided, scale the other dimension
# properly
# Width defined in custom size should override max_width
line_region = view.line(region)
imgattr = check_imgattr(view, line_region, region)
assert w is not None and h is not None
if not imgattr and w > 0 and h > 0:
if max_width and w > max_width:
m = max_width / w
h = int(m * h)
w = max_width
imgattr = 'width="{}" height="{}"'.format(w, h)
# Force the phantom image view to append past the end of the line
# Otherwise, the phantom image view interlaces in between
# word-wrapped lines
line_region.a = line_region.b
debug("region", region)
debug("line_region", line_region)
key = 'view-' + str(view.id())
html_img = FMT.format(img, imgattr)
debug("Creating phantom", url)
view.add_phantom(key, sublime.Region(line_region.b), html_img, sublime.LAYOUT_BLOCK)
@staticmethod
def hide_images(view):
key = 'view-' + str(view.id())
view.erase_phantoms(key)
def check_imgattr(view, line_region, link_region):
# find attrs for this link
full_line = view.substr(line_region)
link_till_eol = full_line[link_region.a - line_region.a:]
# find attr if present
m = re.match(r'.*\)\{(.*)\}', link_till_eol)
if m:
return m.groups()[0]
return ''
def get_file_image_size(img):
with open(img, 'rb') as f:
return get_image_size(f)
def get_image_size(f):
"""
Determine the image type of img and return its size.
"""
head = f.read(24)
ttype = None
debug(str(head))
debug(str(head[:4]))
debug(head[:4] == b'<svg')
if imghdr.what('', head) == 'png':
debug('detected png')
ttype = "png"
check = struct.unpack('>i', head[4:8])[0]
if check != 0x0d0a1a0a:
return None, None, ttype
width, height = struct.unpack('>ii', head[16:24])
elif imghdr.what('', head) == 'gif':
debug('detected gif')
ttype = "gif"
width, height = struct.unpack('<HH', head[6:10])
elif imghdr.what('', head) == 'jpeg':
debug('detected jpeg')
ttype = "jpeg"
try:
f.seek(0) # Read 0xff next
size = 2
ftype = 0
while not 0xc0 <= ftype <= 0xcf:
f.seek(size, 1)
byte = f.read(1)
while ord(byte) == 0xff:
byte = f.read(1)
ftype = ord(byte)
size = struct.unpack('>H', f.read(2))[0] - 2
# SOFn block
f.seek(1, 1) # skip precision byte.
height, width = struct.unpack('>HH', f.read(4))
except Exception as e:
debug("determining jpeg image size failed", e)
return None, None, ttype
elif head[:4] == b'<svg':
debug('detected svg')
# SVG is not rendered by ST3 in phantoms.
# The SVG would need to be rendered as png/jpg separately, and its data
# placed into the phantom
return None, None, None
else:
debug('unable to detect image')
return None, None, None
return width, height, ttype
def get_path_for(view):
"""
Returns the path of the current file in view.
Returns / if no path is found
"""
if view.file_name():
return os.path.dirname(view.file_name())
if view.window().project_file_name():
return os.path.dirname(view.window().project_file_name())
return '/'