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 '/'

posted @ 2022-05-03 19:32  yangwen0228  阅读(201)  评论(0编辑  收藏  举报