From 76abeb1ead4a61f624265e8434c83daef818cd31 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Wed, 25 Sep 2024 16:08:45 +0300 Subject: [PATCH] Improve image thumbnails generation. --- dao/pom.xml | 8 +- .../server/dao/util/ImageUtils.java | 159 ++++++++++-------- pom.xml | 19 +-- 3 files changed, 93 insertions(+), 93 deletions(-) diff --git a/dao/pom.xml b/dao/pom.xml index 6f5ec41672..a5a0719ae7 100644 --- a/dao/pom.xml +++ b/dao/pom.xml @@ -246,12 +246,8 @@ hypersistence-utils-hibernate-63 - org.apache.xmlgraphics - batik-transcoder - - - org.apache.xmlgraphics - batik-codec + com.github.weisj + jsvg com.drewnoakes diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/ImageUtils.java b/dao/src/main/java/org/thingsboard/server/dao/util/ImageUtils.java index 19649655a3..9903fe44da 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/util/ImageUtils.java +++ b/dao/src/main/java/org/thingsboard/server/dao/util/ImageUtils.java @@ -21,37 +21,36 @@ import com.drew.metadata.Metadata; import com.drew.metadata.Tag; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; +import com.github.weisj.jsvg.SVGDocument; +import com.github.weisj.jsvg.attributes.ViewBox; +import com.github.weisj.jsvg.geometry.size.FloatSize; +import com.github.weisj.jsvg.parser.DefaultParserProvider; +import com.github.weisj.jsvg.parser.LoaderContext; +import com.github.weisj.jsvg.parser.SVGLoader; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import lombok.With; import lombok.extern.slf4j.Slf4j; -import org.apache.batik.anim.dom.SAXSVGDocumentFactory; -import org.apache.batik.bridge.BridgeContext; -import org.apache.batik.bridge.DocumentLoader; -import org.apache.batik.bridge.GVTBuilder; -import org.apache.batik.bridge.UserAgent; -import org.apache.batik.bridge.UserAgentAdapter; -import org.apache.batik.gvt.GraphicsNode; -import org.apache.batik.transcoder.TranscoderInput; -import org.apache.batik.transcoder.TranscoderOutput; -import org.apache.batik.transcoder.image.PNGTranscoder; -import org.apache.batik.util.XMLResourceDescriptor; import org.apache.commons.lang3.exception.ExceptionUtils; import org.springframework.util.MimeType; import org.springframework.util.MimeTypeUtils; import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.server.common.data.StringUtils; -import org.w3c.dom.Document; +import javax.imageio.IIOImage; import javax.imageio.ImageIO; -import java.awt.Color; +import javax.imageio.ImageTypeSpecifier; +import javax.imageio.ImageWriteParam; +import javax.imageio.ImageWriter; +import java.awt.*; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.Map; +import java.util.regex.Pattern; @NoArgsConstructor(access = AccessLevel.PRIVATE) @Slf4j @@ -144,52 +143,32 @@ public class ImageUtils { BufferedImage thumbnail = new BufferedImage(preview.getWidth(), preview.getHeight(), BufferedImage.TYPE_INT_ARGB); thumbnail.getGraphics().drawImage(bufferedImage, 0, 0, preview.getWidth(), preview.getHeight(), null); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - ImageIO.write(thumbnail, "png", out); + + byte[] pngThumbnail = toCompressedPngData(thumbnail); preview.setMediaType("image/png"); - preview.setData(out.toByteArray()); - preview.setSize(preview.getData().length); + preview.setData(pngThumbnail); + preview.setSize(pngThumbnail.length); image.setPreview(preview); return image; } public static ProcessedImage processSvgImage(byte[] data, String mediaType, int thumbnailMaxDimension) throws Exception { - SAXSVGDocumentFactory factory = new SAXSVGDocumentFactory(XMLResourceDescriptor.getXMLParserClassName()); - Document document = factory.createDocument(null, new ByteArrayInputStream(data)); + var imageData = removeScadaSymbolMetadata(data); + + SVGLoader loader = new SVGLoader(); + SVGDocument svgDocument = loader.load(new ByteArrayInputStream(imageData), null, LoaderContext.builder() + .parserProvider(new DefaultParserProvider()) + .build()); + Integer width = null; Integer height = null; - String strWidth = document.getDocumentElement().getAttribute("width"); - String strHeight = document.getDocumentElement().getAttribute("height"); - if (StringUtils.isNotEmpty(strWidth) && StringUtils.isNotEmpty(strHeight)) { - try { - width = (int) Double.parseDouble(strWidth); - height = (int) Double.parseDouble(strHeight); - } catch (NumberFormatException ignored) {} // in case width and height are in %, mm, etc. - } - if (width == null || height == null) { - String viewBox = document.getDocumentElement().getAttribute("viewBox"); - if (StringUtils.isNotEmpty(viewBox)) { - String[] viewBoxValues = viewBox.split(" "); - if (viewBoxValues.length > 3) { - width = (int) Double.parseDouble(viewBoxValues[2]); - height = (int) Double.parseDouble(viewBoxValues[3]); - } - } - } - if (width == null) { - UserAgent agent = new UserAgentAdapter(); - DocumentLoader loader = new DocumentLoader(agent); - BridgeContext context = new BridgeContext(agent, loader); - context.setDynamic(true); - GVTBuilder builder = new GVTBuilder(); - GraphicsNode root = builder.build(context, document); - var bounds = root.getPrimitiveBounds(); - if (bounds != null) { - width = (int) bounds.getWidth(); - height = (int) bounds.getHeight(); - } + if (svgDocument != null) { + FloatSize imageSize = svgDocument.size(); + width = (int) imageSize.width; + height = (int) imageSize.height; } + ProcessedImage image = new ProcessedImage(); image.setMediaType(mediaType); image.setWidth(width == null ? 0 : width); @@ -197,26 +176,28 @@ public class ImageUtils { image.setData(data); image.setSize(data.length); - PNGTranscoder transcoder = new PNGTranscoder(); - if (image.getSize() < 10240) { // if SVG is smaller than 10kB (average 250x250 PNG preview size) - return withPreviewAsOriginalImage(image); + if (imageData.length < 10240 || svgDocument == null) { // if SVG is smaller than 10kB (average 250x250 PNG preview size) + return withPreviewAsOriginalImage(image, imageData); } else if (image.getSize() > 102400 && image.getWidth() != 0) { // considering SVG image detailed after 100kB - // increasing preview dimensions thumbnailMaxDimension = 512; - int[] thumbnailDimensions = getThumbnailDimensions(image.getWidth(), image.getHeight(), thumbnailMaxDimension, false); - transcoder.addTranscodingHint(PNGTranscoder.KEY_WIDTH, (float) thumbnailDimensions[0]); - transcoder.addTranscodingHint(PNGTranscoder.KEY_HEIGHT, (float) thumbnailDimensions[1]); - } else { - transcoder.addTranscodingHint(PNGTranscoder.KEY_MAX_WIDTH, (float) thumbnailMaxDimension); - transcoder.addTranscodingHint(PNGTranscoder.KEY_MAX_HEIGHT, (float) thumbnailMaxDimension); } - ByteArrayOutputStream out = new ByteArrayOutputStream(); - transcoder.transcode(new TranscoderInput(new ByteArrayInputStream(data)), new TranscoderOutput(out)); - byte[] pngThumbnail = out.toByteArray(); - ProcessedImage preview = new ProcessedImage(); - preview.setWidth(thumbnailMaxDimension); - preview.setHeight(thumbnailMaxDimension); + int[] thumbnailDimensions = getThumbnailDimensions(image.getWidth(), image.getHeight(), thumbnailMaxDimension, false); + preview.setWidth(thumbnailDimensions[0]); + preview.setHeight(thumbnailDimensions[1]); + + BufferedImage thumbnail = new BufferedImage(preview.getWidth(), preview.getHeight(), BufferedImage.TYPE_INT_ARGB); + Graphics2D graphics = thumbnail.createGraphics(); + graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + svgDocument.render((Component)null,graphics, new ViewBox(0, 0, preview.getWidth(), preview.getHeight())); + graphics.dispose(); + + byte[] pngThumbnail = toCompressedPngData(thumbnail); + + if (imageData.length < pngThumbnail.length) { // set preview as original SVG if PNG thumbnail size is greater + return withPreviewAsOriginalImage(image, imageData); + } + preview.setMediaType("image/png"); preview.setData(pngThumbnail); preview.setSize(pngThumbnail.length); @@ -224,6 +205,27 @@ public class ImageUtils { return image; } + public static byte[] toCompressedPngData(BufferedImage image) throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ImageTypeSpecifier type = ImageTypeSpecifier.createFromRenderedImage(image); + ImageWriter writer = ImageIO.getImageWriters(type, "png").next(); + ImageWriteParam param = writer.getDefaultWriteParam(); + if (param.canWriteCompressed()) { + param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); + param.setCompressionQuality(0.0f); + } + var output = ImageIO.createImageOutputStream(out); + writer.setOutput(output); + try { + writer.write(null, new IIOImage(image, null, null), param); + } finally { + writer.dispose(); + output.flush(); + } + return out.toByteArray(); + } + + private static ProcessedImage previewAsOriginalImage(byte[] data, String mediaType) { ProcessedImage image = new ProcessedImage(); image.setMediaType(mediaType); @@ -235,21 +237,34 @@ public class ImageUtils { } public static ProcessedImage withPreviewAsOriginalImage(ProcessedImage originalImage) { - originalImage.setPreview(originalImage.withData(null)); + return withPreviewAsOriginalImage(originalImage, null); + } + + public static ProcessedImage withPreviewAsOriginalImage(ProcessedImage originalImage, byte[] previewData) { + originalImage.setPreview(originalImage.withData(previewData)); + if (previewData != null) { + originalImage.getPreview().setSize(previewData.length); + } return originalImage; } public static ScadaSymbolMetadataInfo processScadaSymbolMetadata(String fileName, byte[] data) throws Exception { - SAXSVGDocumentFactory factory = new SAXSVGDocumentFactory(XMLResourceDescriptor.getXMLParserClassName()); - Document document = factory.createDocument(null, new ByteArrayInputStream(data)); - var metaElements = document.getElementsByTagName("tb:metadata"); JsonNode metaData = null; - if (metaElements.getLength() > 0) { - metaData = JacksonUtil.toJsonNode(metaElements.item(0).getTextContent()); + String contents = new String(data, StandardCharsets.UTF_8); + var matcher = Pattern.compile("(?s)]*><\\/tb:metadata>").matcher(contents); + if (matcher.find()) { + var metadataContent = matcher.group(1); + metaData = JacksonUtil.toJsonNode(metadataContent); } return new ScadaSymbolMetadataInfo(fileName, metaData); } + public static byte[] removeScadaSymbolMetadata(byte[] data) { + String contents = new String(data, StandardCharsets.UTF_8); + contents = contents.replaceFirst("(?s)]*>.*<\\/tb:metadata>", ""); + return contents.getBytes(StandardCharsets.UTF_8); + } + private static int[] getThumbnailDimensions(int originalWidth, int originalHeight, int maxDimension, boolean originalIfSmaller) { if (originalWidth <= maxDimension && originalHeight <= maxDimension && originalIfSmaller) { return new int[]{originalWidth, originalHeight}; diff --git a/pom.xml b/pom.xml index 6d0fa6f090..4ea6491eb2 100755 --- a/pom.xml +++ b/pom.xml @@ -163,7 +163,7 @@ 1.39.0 6.6.0 1.35.0 - 1.17 + 1.6.1 2.19.0 9.2.0 @@ -2262,20 +2262,9 @@ ${google-oauth-client.version} - org.apache.xmlgraphics - batik-transcoder - ${apache-xmlgraphics.version} - - - commons-logging - commons-logging - - - - - org.apache.xmlgraphics - batik-codec - ${apache-xmlgraphics.version} + com.github.weisj + jsvg + ${weisj-jsvg.version} com.drewnoakes