MARKDOWN 文档图片编码嵌入方案

#1 写在前面

  • 开始写这篇文章时,标题怎么定困扰我良久,缘于不晓得如何给接下来要做的事定个简单明了的标题:在📱终端只能纯文本交互的前提下,优雅展示 markdown 文档中的图片。
  • 这也许比问题本身还要棘手😄。

#2 背景说明

公司内网有一套基于 markdown 的文档系统,方便同事查阅资料,现希望能够在移动端进行浏览。

目前我们已在集团移动办公 APP 发布有 H5 小程序,实现了互联网与内网的数据通信,但存在以下限制:

  • 请求方式为 POST
  • 后端返回内容限定为纯文本
  • 每次发起请求终端都有 loading 弹窗
  • 无法加载互联网资源

#3 思路阐述

**方案一:将图片编码进 markdown 文本 **

识别出 markdown 内的图片,转换为 BASE64 编码并替换原文本,终端解析后渲染。本文采用此方案✅。

方案二:延迟加载图片

终端渲染后,监听页面滚动,按需加载图片(传递 url 或图片编号,后端返回 BASE64 编码)。此方案可通过自定义指令实现,前后端均需要代码改造。

#3.1 处理流程

  1. 用户请求指定 ID 的 MARKDOWN 资源
  2. 从数据库读取原始文本,调用 MarkdownFunc.embedImages 方法
  3. 若该 ID 的缓存文件存在,则直接使用,跳转到⑥
  4. 用正则表达式匹配全部图片标签,对符合后缀规范的本地文件,进行以下操作
    a. 原始图片宽度超出阈值,则先缩放
    b. 转换为 WEBP 格式(节流😄)
    c. 进一步转换为 BASE64 编码
    d. 替换到原标签文本
  5. 将处理完成的文本写入缓存文件
  6. 返回内容到客户端

同时,当文档被修改后,监听事件,删除对应的缓存文件。

#3.2 代码实现

@Configuration
@ConfigurationProperties(prefix = "page.markdown")
class MarkdownConfig {var maxWidth        = 900       //图片宽度超出此值则进行压缩var quality         = 0.1F      //转换为 webp 时质量阈值var resizeQuality   = 0.8f      //裁剪图片的质量阈值var exts            = listOf("jpg","jpeg","bmp","png")var dir             = "markdown"
}@Component
class MarkdownFunc(private val fileStore: FileStore,private val config: MarkdownConfig) {@Value("\${server.servlet.context-path}")private val contextPath = ""private val logger = LoggerFactory.getLogger(javaClass)/*** 转换为 Base64 编码*/private fun base64(bytes:ByteArray) = "![](data:image/webp;base64,${Base64.getEncoder().encodeToString(bytes)})"private fun txtFile(id: Long) = fileStore.buildPathWithoutDate("${id}.txt", config.dir)/**** @param id    文档唯一编号* @param text  markdown 源文本*/fun embedImages(id:Long, text:String):String = txtFile(id).let { file->if(file.exists()) return@let Files.readString(file)Regex("!\\[.*?\\]\\((.*?)\\)").replace(text) { match->val fileUrl = match.groupValues.last().let {if(it.startsWith(contextPath))it.replaceFirst(contextPath, "")elseit}//暂不支持互联网资源if(fileUrl.startsWith("http"))  return@replace match.valueval imgPath = Paths.get(".", fileUrl)val ext = imgPath.extension.lowercase()logger.info("${imgPath.toAbsolutePath() }  ${imgPath.isRegularFile()}")if(imgPath.exists() && imgPath.isRegularFile()){if(config.exts.contains(ext)){var img = ImageIO.read(imgPath.toFile()).let {if(it.width > config.maxWidth){if(logger.isDebugEnabled)   logger.debug("图片 $imgPath 宽度超出阈值 ${config.maxWidth} 即将裁剪...")//对图片进行缩放,如需水印可以调用 watermark 方法Thumbnails.of(it).width(config.maxWidth).outputQuality(config.resizeQuality).asBufferedImage()}elseit}val out = ByteArrayOutputStream()val mout = MemoryCacheImageOutputStream(out)ImageIO.getImageWritersByMIMEType("image/webp").next().let { writer->writer.output = moutwriter.write(null,IIOImage(img, null, null),WebPWriteParam(writer.locale).also {it.compressionMode = ImageWriteParam.MODE_EXPLICITit.compressionType = it.compressionTypes[WebPWriteParam.LOSSY_COMPRESSION]it.compressionQuality = config.quality})if(logger.isDebugEnabled)   logger.debug("图片 $imgPath 转 webp 完成...")}mout.flush()base64(out.toByteArray())}//对于 webp 格式不作缩放处理直接编码else if(ext == "webp"){base64(Files.readAllBytes(imgPath))}else{if(logger.isDebugEnabled)   logger.debug("图片 $imgPath 不是支持的格式...")match.value}}else {logger.error("图片 $imgPath 不存在或不是一个有效文件...")match.value}}.also {file.parent.also { p->if(!p.exists())Files.createDirectories(p)}Files.writeString(file, it, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)logger.info("缓存 $file 写入成功(SIZE = ${file.fileSize()} B)")}}@Async@EventListener(PageContentUpdateEvent::class)fun onPageUpdate(event: PageContentUpdateEvent) {event.page.also {if(it.template == Page.MARKDOWN){logger.info("检测到 #${it.id} 的内容变更,即将删除其缓存文件(若存在)...")txtFile(it.id).deleteIfExists()}}}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/102629.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Node与ES6模块

模块化的作用主要体现在封装和隐藏私有实现细节,以及保证全局命名空间清洁上,因而模块之间不会意外修改各自定义的变量、函数和类。 1 模块 1.1 代码打包工具基本工作原理 在函数中声明的局部变量和嵌套函数都是函数私有的。这意味着我们可以使用立即…

林沛满---一个面试建议

在应聘一个技术职位之前,做好充分的准备无疑能大大提高成功率。这里所说的准备并不是指押题,因为有经验的面试官往往准备了海量的题库,押中的概率太低。比如我有位同事的题库里有上百道题,内容涵盖了编程、操作系统、网络、存储……

Unity MRTK Hololens2眼动交互

/** ** UnityVersion : 2021.3.6f1* Description : 眼部交互基类* Author: * CreateTime : 2023-10-11 09:43:20* Version : V1.0.0* * */using System.Collections.Generic; using Microsoft.MixedReality.Toolkit.Input; using UnityEngine;namespace MRTKExtend.EyeTrackin…

神秘的锦衣卫

在看明朝电视剧经常听到的一句台词:锦衣卫办案,闲杂人等速速离开。锦衣卫是明朝特务机构,直接听命于皇帝,是亲军卫之一,也是最重要的一卫。 1、卫所制 卫所制是明代最主要的军事制度,其目标是寓兵于农、屯…

RISC-V 架构寄存器规范

a0 - a7( x10 - x17 ) 调用者保存,用来传递输入参数。其中的 a0 和 a1 还用来保存返回值 t0 - 6( x5 - x7, x28 - x31 ) 调用者保存,作为临时寄存器使用,在被调函数中可以随意使用无需保存 s0 - s11( x8 - x9, x18…

React 状态管理 - Context API 前世今生(下)

New Context API Provider【context的生产者组件】 createContext 创建一个Context对象,订阅了整个Context对象的组件,会从组件树中离自身最近的那个匹配的Provider中读取到当前的context值。Context.Provider 父Context对象返回的Provider组件&#x…

22字符串-简单反转

目录 BM(Boyer-Moore) 坏字符 好后缀 什么情况用哪个规则? LeetCode之路——151. 反转字符串中的单词 分析: 字符串匹配中除了简单的BF(Brute Force)、RK(Rabin-Karp)算法,还有…

PHP Discord获取频道消息功能实现

PHP Discord获取频道消息功能实现 1. 关注对应频道2. 添加机器人3. 配置机器人权限4. 使用 DiscordPHP 类库5. 代码示例 (Laravel 框架)6. 服务器部署 1. 关注对应频道 首先要创建自己的频道, 然后到对应的公告频道中关注这个频道(这时 Discord 会让你选择频道, 选择之前创建的…

(n, 0)维NumPy数组 和 (n, 1)维NumPy数组 有什么区别?

(n, 0) 维NumPy数组和 (n, 1) 维NumPy数组之间有一个重要的区别,这涉及到数组的维度和形状: (n, 0) 维NumPy数组: 这是一个具有 n 行和 0 列的数组。在 NumPy 中,这实际上是一个一维数组,但它有 n 个元素,其…

区块链游戏的开发框架

链游(Blockchain Games)是基于区块链技术构建的游戏。它们与传统游戏有一些显著不同之处,因此需要特定的开发框架和工具。以下是一些用于链游开发的开发框架及其特点,希望对大家有所帮助。北京木奇移动技术有限公司,专…

基于STM32_DS18B20单总线传感器驱动

基于STM32_DS18B20单总线传感器驱动 文章目录 基于STM32_DS18B20单总线传感器驱动前言一、BS18B20?二、原理1.复位与检验2.基本命令3.唯一ROM识别码4.温度转换 三、驱动代码四、注意事项 前言 本文以一款典型的单总线传感器及其驱动——DS18B20为例,简单…

vue学习-15vue全局路由守卫

在vue.js中,提供三种全局守卫,英文称作(Global Navigation Guards),这些守卫允许你在路由发生变化的时候执行的一些操作,例如身份验证,日志纪录或者路由跳转控制,全局路由守卫包含以…

【DevOps】DevOps—基本概念

文章目录 1. DevOps2. CI/CD 1. DevOps 维基百科定义: DevOps是一组过程、方法与系统的统称,用于促进 开发、技术运营 和 质量保障(QA) 部门之间的沟通、协作与整合。我理解DevOps是一种软件管理思维模式。 为什么会有DevOps呢&…

《UnityShader入门精要》学习2

UnityShader 基础 UnityShader 概述 一对好兄弟:材质和UnityShader 总体来说,在Unity中我们需要配合使用材质(Material)和Unity Shader才能达到需要的效果。一个最常见的流程是: (1)创建一个…

ts内置对象有哪些有什么作用

TS内置对象是指在TypeScript中自带的一些对象,可以直接使用而不需要导入或安装。一些常见的TS内置对象包括: Math:提供了一些常用的数学函数,例如sin、cos、sqrt等。 Date:提供了日期和时间的相关功能,例如…

(5)SpringMVC处理携带JSON格式(“key“:value)请求数据的Ajax请求

SpringMVC处理Ajax 参考文章数据交换的常见格式,如JSON格式和XML格式 请求参数的携带方式 浏览器发送到服务器的请求参数有namevalue&...(键值对)和{key:value,...}(json对象)两种格式 URL请求和表单的GET请求会将请求参数以键值对的格式拼接到请求地址后面form表单的P…

【深度学习】UniControl 一个统一的扩散模型用于可控的野外视觉生成

论文:https://arxiv.org/abs/2305.11147 代码:https://github.com/salesforce/UniControl#data-preparation docker快速部署:https://qq742971636.blog.csdn.net/article/details/133129146 文章目录 AbstractIntroductionRelated WorksUniCo…

【Linux】HTTPS协议

文章目录 📖 前言1. 引入https协议2. 常见的加密方式2.1 对称加密:2.2 非对称加密:2.3 数据摘要&&数据指纹: 3. 对加密方式的探究3.1 只使用对称加密:3.2 只使用非对称加密:3.3 双方都使用非对称加…

SQL和Python,哪个更容易自学?哪个更适合数据工作的编程新手?

如果你想从事数据工作,比如数据分析、数据开发、数据科学等,你可能会遇到这样的问题:SQL和Python哪个更容易自学?哪个更有用?哪个更有前途?其实这两种语言都是数据工作的重要技能,但它们的特点和…

vue拦截器是什么,如何使用

Vue拦截器是一种用来拦截并处理HTTP请求和响应的机制,它可以在请求或响应发送前或后进行一些预处理或处理。在Vue中,可以使用axios库来实现拦截器,axios库是一个基于Promise的HTTP客户端,可以用于浏览器和Node.js平台。 使用axio…