纯文本
交互的前提下,优雅展示 markdown 文档中的图片。公司内网有一套基于 markdown
的文档系统,方便同事查阅资料,现希望能够在移动端进行浏览。
目前我们已在集团移动办公 APP 发布有 H5 小程序
,实现了互联网与内网的数据通信,但存在以下限制:
POST
loading
弹窗**方案一:将图片编码进 markdown 文本 **
识别出 markdown 内的图片,转换为 BASE64 编码并替换原文本,终端解析后渲染。本文采用此方案✅。
方案二:延迟加载图片
终端渲染后,监听页面滚动,按需加载图片(传递 url 或图片编号,后端返回 BASE64 编码)。此方案可通过自定义指令实现,前后端均需要代码改造。
同时,当文档被修改后,监听事件,删除对应的缓存文件。
@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, "")
else
it
}
//暂不支持互联网资源
if(fileUrl.startsWith("http")) return@replace match.value
val 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()
}
else
it
}
val out = ByteArrayOutputStream()
val mout = MemoryCacheImageOutputStream(out)
ImageIO.getImageWritersByMIMEType("image/webp").next().let { writer->
writer.output = mout
writer.write(
null,
IIOImage(img, null, null),
WebPWriteParam(writer.locale).also {
it.compressionMode = ImageWriteParam.MODE_EXPLICIT
it.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()
}
}
}
}