mirror of
https://gitflic.ru/project/openide/openide.git
synced 2025-12-13 06:59:44 +07:00
IJPL-155932 SVG Image support via ImageReaderSpi (detect SVG)
closes #2850 GitOrigin-RevId: 1674cb6711fa8e2744c09dcdd58963051d331f3c
This commit is contained in:
committed by
intellij-monorepo-bot
parent
783bbde096
commit
f9f1a0aa07
@@ -42,5 +42,6 @@
|
||||
<orderEntry type="library" name="kotlinx-serialization-json" level="project" />
|
||||
<orderEntry type="module" module-name="intellij.xml.frontback" />
|
||||
<orderEntry type="module" module-name="intellij.platform.ui.jcef" />
|
||||
<orderEntry type="library" name="jsvg" level="project" />
|
||||
</component>
|
||||
</module>
|
||||
@@ -1 +1,2 @@
|
||||
org.intellij.images.util.imageio.CommonsImagingImageReaderSpi
|
||||
org.intellij.images.util.imageio.CommonsImagingImageReaderSpi
|
||||
org.intellij.images.util.imageio.svg.SvgImageReaderSpi
|
||||
@@ -0,0 +1,144 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package org.intellij.images.util.imageio.svg
|
||||
|
||||
import com.github.weisj.jsvg.attributes.font.SVGFont
|
||||
import com.github.weisj.jsvg.geometry.size.Length
|
||||
import com.github.weisj.jsvg.nodes.SVG
|
||||
import com.intellij.ui.paint.PaintUtil
|
||||
import com.intellij.ui.scale.ScaleContext
|
||||
import com.intellij.ui.svg.createJSvgDocument
|
||||
import com.intellij.util.ui.ImageUtil
|
||||
import java.awt.image.BufferedImage
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import javax.imageio.ImageReadParam
|
||||
import javax.imageio.ImageReader
|
||||
import javax.imageio.ImageTypeSpecifier
|
||||
import javax.imageio.stream.ImageInputStream
|
||||
import kotlin.math.max
|
||||
|
||||
class SvgImageReader(svgImageReaderSpi: SvgImageReaderSpi) : ImageReader(svgImageReaderSpi) {
|
||||
private var height: Length? = null
|
||||
private var width: Length? = null
|
||||
private var jSvgDocument: SVG? = null
|
||||
override fun setInput(input: Any?, seekForwardOnly: Boolean, ignoreMetadata: Boolean) {
|
||||
super.setInput(input, seekForwardOnly, ignoreMetadata)
|
||||
reset()
|
||||
}
|
||||
|
||||
override fun getWidth(imageIndex: Int): Int {
|
||||
loadInfoIfNeeded()
|
||||
return width?.raw()?.toInt() ?: throw IOException("SVG document not loaded")
|
||||
}
|
||||
|
||||
override fun getHeight(imageIndex: Int): Int {
|
||||
loadInfoIfNeeded()
|
||||
return height?.raw()?.toInt() ?: throw IOException("SVG document not loaded")
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun loadInfoIfNeeded() {
|
||||
val input = input
|
||||
if (jSvgDocument == null && input is ImageInputStream) {
|
||||
|
||||
try {
|
||||
// Not using ByteArray to avoid potential humongous allocation
|
||||
ImageInputStreamAdapter(input).buffered().use {
|
||||
jSvgDocument = createJSvgDocument(it).also { svg ->
|
||||
width = svg.width
|
||||
height = svg.height
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
// could not read the SVG document
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun read(imageIndex: Int, param: ImageReadParam?): BufferedImage {
|
||||
loadInfoIfNeeded()
|
||||
jSvgDocument ?: throw IOException("SVG document not loaded")
|
||||
|
||||
val sourceRenderSize = param?.sourceRenderSize
|
||||
|
||||
val width = sourceRenderSize?.width?.toDouble() ?: width!!.raw().toDouble()
|
||||
val height = sourceRenderSize?.height?.toDouble() ?: height!!.raw().toDouble()
|
||||
|
||||
// how to have an hidpi aware image?
|
||||
val bi = ImageUtil.createImage(
|
||||
ScaleContext.create(),
|
||||
width,
|
||||
height,
|
||||
BufferedImage.TYPE_INT_ARGB,
|
||||
PaintUtil.RoundingMode.ROUND
|
||||
)
|
||||
val g = bi.createGraphics()
|
||||
|
||||
ImageUtil.applyQualityRenderingHints(g)
|
||||
|
||||
jSvgDocument?.renderWithSize(
|
||||
width.toFloat(),
|
||||
height.toFloat(),
|
||||
SVGFont.defaultFontSize(),
|
||||
g,
|
||||
)
|
||||
return bi
|
||||
}
|
||||
|
||||
override fun reset() {
|
||||
jSvgDocument = null
|
||||
height = null
|
||||
width = null
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
reset()
|
||||
}
|
||||
|
||||
override fun getNumImages(allowSearch: Boolean): Int {
|
||||
return if (jSvgDocument != null) 1 else 0
|
||||
}
|
||||
|
||||
override fun getImageTypes(imageIndex: Int): Iterator<ImageTypeSpecifier> {
|
||||
return listOf<ImageTypeSpecifier>(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_ARGB)).iterator()
|
||||
}
|
||||
|
||||
override fun getStreamMetadata() = null
|
||||
|
||||
override fun getImageMetadata(imageIndex: Int) = null
|
||||
|
||||
}
|
||||
|
||||
private class ImageInputStreamAdapter(private val imageInputStream: ImageInputStream) : InputStream() {
|
||||
private var closed = false
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun close() {
|
||||
closed = true
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun read(): Int {
|
||||
if(closed) throw IOException("stream closed")
|
||||
return imageInputStream.read()
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun read(b: ByteArray, off: Int, len: Int): Int {
|
||||
if(closed) throw IOException("stream closed")
|
||||
if (len <= 0) {
|
||||
return 0
|
||||
}
|
||||
return imageInputStream.read(b, off, max(len.toLong(), 0).toInt())
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun skip(n: Long): Long {
|
||||
if(closed) throw IOException("stream closed")
|
||||
if (n <= 0) {
|
||||
return 0
|
||||
}
|
||||
return imageInputStream.skipBytes(n)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package org.intellij.images.util.imageio.svg
|
||||
|
||||
import java.io.EOFException
|
||||
import java.util.*
|
||||
import javax.imageio.ImageReader
|
||||
import javax.imageio.spi.ImageReaderSpi
|
||||
import javax.imageio.stream.ImageInputStream
|
||||
import kotlin.experimental.and
|
||||
|
||||
/**
|
||||
* ImageReaderSpi for SVG images.
|
||||
*
|
||||
* @author Brice Dutheil
|
||||
*/
|
||||
class SvgImageReaderSpi : ImageReaderSpi() {
|
||||
init {
|
||||
vendorName = "weisj/jsvg & JetBrains"
|
||||
suffixes = arrayOf("svg")
|
||||
MIMETypes = arrayOf("image/svg+xml")
|
||||
names = arrayOf("SVG Image Reader")
|
||||
pluginClassName = SvgImageReaderSpi::class.java.name
|
||||
inputTypes = arrayOf<Class<*>>(ImageInputStream::class.java)
|
||||
}
|
||||
|
||||
override fun getDescription(locale: Locale?) = "SVG Image Reader"
|
||||
|
||||
override fun canDecodeInput(source: Any?): Boolean {
|
||||
if (source !is ImageInputStream) return false
|
||||
return canDecode(source)
|
||||
}
|
||||
|
||||
private fun canDecode(imageInputStream: ImageInputStream): Boolean {
|
||||
// NOTE: This test is quite quick as it does not involve any parsing,
|
||||
// however, it may not recognize all kinds of SVG documents.
|
||||
|
||||
try {
|
||||
// We need to read the first few bytes to determine if this is an SVG file,
|
||||
// then reset the stream at the marked position
|
||||
imageInputStream.mark()
|
||||
|
||||
// SVG file can starts with an XML declaration, then possibly followed by comments, whitespaces
|
||||
// \w.*(<?xml version="1.0" encoding="UTF-8" ?>)?
|
||||
// (\w|(<!--.*-->))*
|
||||
|
||||
// Then either the doctype gives the hint possibly surrounded by comments and/or whitespaces
|
||||
// <!DOCTYPE svg ...
|
||||
// (\w|(<!--.*-->))*
|
||||
|
||||
// Or the root tag is svg, possibly preceding comments and/or whitespaces
|
||||
// <svg ...
|
||||
|
||||
// Handles first whitespaces if any
|
||||
val lastReadByte = imageInputStream.readFirstAfterWhitespaces()
|
||||
|
||||
// The next byte should be '<' (comments, doctype XML declaration or the root tag)
|
||||
if (lastReadByte != '<'.code) {
|
||||
return false
|
||||
}
|
||||
|
||||
val window = ByteArray(4)
|
||||
while (true) {
|
||||
imageInputStream.readFully(window)
|
||||
|
||||
when {
|
||||
// `<?` Handles the XML declaration
|
||||
window.startsWith('?') -> imageInputStream.skipUntil('?', '>')
|
||||
//buffer[0] == '?'.code.toByte() -> imageInputStream.skipUntil('?', '>')
|
||||
|
||||
// `<!--` Handles a comment
|
||||
window.startsWith('!', '-', '-') -> imageInputStream.skipUntil('-', '-', '>')
|
||||
|
||||
// `<!DOCTYPE` Handles the DOCTYPE declaration
|
||||
window.startsWith('!', 'D', 'O', 'C') && imageInputStream.readNextEquals(charArrayOf('T', 'Y', 'P', 'E')) -> {
|
||||
val lastReadChar = imageInputStream.readFirstAfterWhitespaces()
|
||||
return lastReadChar == 's'.code && imageInputStream.readNextEquals(charArrayOf('v', 'g'))
|
||||
}
|
||||
|
||||
// `<svg` Handles the root tag
|
||||
window.startsWith('s', 'v', 'g') && (Char(window[3].toUShort()).isWhitespace() || window[3] == ':'.code.toByte()) -> {
|
||||
return true
|
||||
}
|
||||
|
||||
// not an SVG file or not handled
|
||||
else -> return false
|
||||
}
|
||||
|
||||
//while ((imageInputStream.readByte() and 0xFF.toByte()).toInt().toChar() != '<') {
|
||||
// // Skip over, until next begin tag or EOF
|
||||
//}
|
||||
// Skip over, until next begin tag or EOF
|
||||
imageInputStream.skipUntil('<')
|
||||
}
|
||||
} catch (ignore: EOFException) {
|
||||
// Possible for small files...
|
||||
ignore.printStackTrace()
|
||||
return false
|
||||
} finally {
|
||||
imageInputStream.reset()
|
||||
}
|
||||
}
|
||||
|
||||
private fun ImageInputStream.readFirstAfterWhitespaces(): Int {
|
||||
var lastReadByte: Int
|
||||
while ((read().also { lastReadByte = it }).toChar().isWhitespace()) {
|
||||
// skip whitespaces if any
|
||||
}
|
||||
return lastReadByte
|
||||
}
|
||||
|
||||
private fun ImageInputStream.skipUntil(vararg expected: Char) {
|
||||
while (!readNextEquals(expected)) {
|
||||
// skip until expected chars or EOF
|
||||
}
|
||||
}
|
||||
|
||||
private fun ImageInputStream.readNextEquals(expected: CharArray): Boolean {
|
||||
// first char is read with readByte()
|
||||
require(expected.isNotEmpty())
|
||||
val first = (readByte() and 0xFF.toByte()).toInt().toChar()
|
||||
if (first != expected[0]) return false
|
||||
|
||||
// next can be read with read()
|
||||
expected.forEachIndexed { index, it ->
|
||||
if (index == 0) return@forEachIndexed
|
||||
val read = read()
|
||||
if (read != it.code) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun ByteArray.startsWith(vararg expected: Char): Boolean {
|
||||
expected.forEachIndexed { index, c ->
|
||||
if (this[index] != c.code.toByte()) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun createReaderInstance(extension: Any?): ImageReader {
|
||||
return SvgImageReader(this)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
package org.intellij.images.util.imageio.svg
|
||||
|
||||
import org.junit.jupiter.api.Assertions.*
|
||||
import org.junit.jupiter.api.Test
|
||||
import javax.imageio.stream.MemoryCacheImageInputStream
|
||||
|
||||
class SvgImageReaderSpiTest {
|
||||
@Test
|
||||
fun `can read svg with xml header`() {
|
||||
val svg = """
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"> </svg>
|
||||
""".trimIndent()
|
||||
|
||||
MemoryCacheImageInputStream(svg.byteInputStream()).use { stream ->
|
||||
assertTrue(SvgImageReaderSpi().canDecodeInput(stream))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `cannot read svg with broken xml header`() {
|
||||
val svg = """
|
||||
<?xml version="1.0" encoding="UTF-8" // broken
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"> </svg>
|
||||
""".trimIndent()
|
||||
|
||||
MemoryCacheImageInputStream(svg.byteInputStream()).use { stream ->
|
||||
assertFalse(SvgImageReaderSpi().canDecodeInput(stream))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `cannot read svg with broken truncated file`() {
|
||||
val svg = """
|
||||
<?xml version="1.0" encoding="UTF-8" // broken
|
||||
""".trimIndent()
|
||||
|
||||
MemoryCacheImageInputStream(svg.byteInputStream()).use { stream ->
|
||||
assertFalse(SvgImageReaderSpi().canDecodeInput(stream))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `can read svg with whitespace before xml header`() {
|
||||
val svg = """
|
||||
|
||||
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"> </svg>
|
||||
""".trimIndent()
|
||||
|
||||
MemoryCacheImageInputStream(svg.byteInputStream()).use { stream ->
|
||||
assertTrue(SvgImageReaderSpi().canDecodeInput(stream))
|
||||
}
|
||||
}
|
||||
@Test
|
||||
fun `can read svg with comment after xml header`() {
|
||||
val svg = """
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!-- comment -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"> </svg>
|
||||
""".trimIndent()
|
||||
|
||||
MemoryCacheImageInputStream(svg.byteInputStream()).use { stream ->
|
||||
assertTrue(SvgImageReaderSpi().canDecodeInput(stream))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `can read svg with DOCTYPE after xml header`() {
|
||||
val svg = """
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<!-- comment -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"> </svg>
|
||||
""".trimIndent()
|
||||
|
||||
MemoryCacheImageInputStream(svg.byteInputStream()).use { stream ->
|
||||
assertTrue(SvgImageReaderSpi().canDecodeInput(stream))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `can read svg with comment before DOCTYPE`() {
|
||||
val svg = """
|
||||
<!-- comment -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"> </svg>
|
||||
""".trimIndent()
|
||||
|
||||
MemoryCacheImageInputStream(svg.byteInputStream()).use { stream ->
|
||||
assertTrue(SvgImageReaderSpi().canDecodeInput(stream))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `can read svg starting with DOCTYPE`() {
|
||||
val svg = """
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"> </svg>
|
||||
""".trimIndent()
|
||||
|
||||
MemoryCacheImageInputStream(svg.byteInputStream()).use { stream ->
|
||||
assertTrue(SvgImageReaderSpi().canDecodeInput(stream))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `can read svg starting with DOCTYPE having spaces`() {
|
||||
val svg = """
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<!-- comment -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"> </svg>
|
||||
""".trimIndent()
|
||||
|
||||
MemoryCacheImageInputStream(svg.byteInputStream()).use { stream ->
|
||||
assertTrue(SvgImageReaderSpi().canDecodeInput(stream))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `can read svg starting with SVG root tag`() {
|
||||
val svg = """
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"> </svg>
|
||||
""".trimIndent()
|
||||
|
||||
MemoryCacheImageInputStream(svg.byteInputStream()).use { stream ->
|
||||
assertTrue(SvgImageReaderSpi().canDecodeInput(stream))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `can read svg starting with comment then SVG root tag`() {
|
||||
val svg = """
|
||||
<!-- comment -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"> </svg>
|
||||
""".trimIndent()
|
||||
|
||||
MemoryCacheImageInputStream(svg.byteInputStream()).use { stream ->
|
||||
assertTrue(SvgImageReaderSpi().canDecodeInput(stream))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `cannot read svg with broken comment`() {
|
||||
val svg = """
|
||||
<!-- comment -- // broken
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"> </svg>
|
||||
""".trimIndent()
|
||||
|
||||
MemoryCacheImageInputStream(svg.byteInputStream()).use { stream ->
|
||||
assertFalse(SvgImageReaderSpi().canDecodeInput(stream))
|
||||
}
|
||||
}
|
||||
@Test
|
||||
fun `cannot read svg with broken root tag`() {
|
||||
val svg = """
|
||||
<!-- comment -->
|
||||
<sv xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"> </svg>
|
||||
""".trimIndent()
|
||||
|
||||
MemoryCacheImageInputStream(svg.byteInputStream()).use { stream ->
|
||||
assertFalse(SvgImageReaderSpi().canDecodeInput(stream))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user