Improve image thumbnails generation.
This commit is contained in:
parent
9bbaab7221
commit
76abeb1ead
@ -246,12 +246,8 @@
|
|||||||
<artifactId>hypersistence-utils-hibernate-63</artifactId>
|
<artifactId>hypersistence-utils-hibernate-63</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.xmlgraphics</groupId>
|
<groupId>com.github.weisj</groupId>
|
||||||
<artifactId>batik-transcoder</artifactId>
|
<artifactId>jsvg</artifactId>
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.apache.xmlgraphics</groupId>
|
|
||||||
<artifactId>batik-codec</artifactId>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.drewnoakes</groupId>
|
<groupId>com.drewnoakes</groupId>
|
||||||
|
|||||||
@ -21,37 +21,36 @@ import com.drew.metadata.Metadata;
|
|||||||
import com.drew.metadata.Tag;
|
import com.drew.metadata.Tag;
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
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.AccessLevel;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import lombok.With;
|
import lombok.With;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
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.apache.commons.lang3.exception.ExceptionUtils;
|
||||||
import org.springframework.util.MimeType;
|
import org.springframework.util.MimeType;
|
||||||
import org.springframework.util.MimeTypeUtils;
|
import org.springframework.util.MimeTypeUtils;
|
||||||
import org.thingsboard.common.util.JacksonUtil;
|
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 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.awt.image.BufferedImage;
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@ -144,52 +143,32 @@ public class ImageUtils {
|
|||||||
|
|
||||||
BufferedImage thumbnail = new BufferedImage(preview.getWidth(), preview.getHeight(), BufferedImage.TYPE_INT_ARGB);
|
BufferedImage thumbnail = new BufferedImage(preview.getWidth(), preview.getHeight(), BufferedImage.TYPE_INT_ARGB);
|
||||||
thumbnail.getGraphics().drawImage(bufferedImage, 0, 0, preview.getWidth(), preview.getHeight(), null);
|
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.setMediaType("image/png");
|
||||||
preview.setData(out.toByteArray());
|
preview.setData(pngThumbnail);
|
||||||
preview.setSize(preview.getData().length);
|
preview.setSize(pngThumbnail.length);
|
||||||
image.setPreview(preview);
|
image.setPreview(preview);
|
||||||
return image;
|
return image;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ProcessedImage processSvgImage(byte[] data, String mediaType, int thumbnailMaxDimension) throws Exception {
|
public static ProcessedImage processSvgImage(byte[] data, String mediaType, int thumbnailMaxDimension) throws Exception {
|
||||||
SAXSVGDocumentFactory factory = new SAXSVGDocumentFactory(XMLResourceDescriptor.getXMLParserClassName());
|
var imageData = removeScadaSymbolMetadata(data);
|
||||||
Document document = factory.createDocument(null, new ByteArrayInputStream(data));
|
|
||||||
|
SVGLoader loader = new SVGLoader();
|
||||||
|
SVGDocument svgDocument = loader.load(new ByteArrayInputStream(imageData), null, LoaderContext.builder()
|
||||||
|
.parserProvider(new DefaultParserProvider())
|
||||||
|
.build());
|
||||||
|
|
||||||
Integer width = null;
|
Integer width = null;
|
||||||
Integer height = null;
|
Integer height = null;
|
||||||
String strWidth = document.getDocumentElement().getAttribute("width");
|
if (svgDocument != null) {
|
||||||
String strHeight = document.getDocumentElement().getAttribute("height");
|
FloatSize imageSize = svgDocument.size();
|
||||||
if (StringUtils.isNotEmpty(strWidth) && StringUtils.isNotEmpty(strHeight)) {
|
width = (int) imageSize.width;
|
||||||
try {
|
height = (int) imageSize.height;
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ProcessedImage image = new ProcessedImage();
|
ProcessedImage image = new ProcessedImage();
|
||||||
image.setMediaType(mediaType);
|
image.setMediaType(mediaType);
|
||||||
image.setWidth(width == null ? 0 : width);
|
image.setWidth(width == null ? 0 : width);
|
||||||
@ -197,26 +176,28 @@ public class ImageUtils {
|
|||||||
image.setData(data);
|
image.setData(data);
|
||||||
image.setSize(data.length);
|
image.setSize(data.length);
|
||||||
|
|
||||||
PNGTranscoder transcoder = new PNGTranscoder();
|
if (imageData.length < 10240 || svgDocument == null) { // if SVG is smaller than 10kB (average 250x250 PNG preview size)
|
||||||
if (image.getSize() < 10240) { // if SVG is smaller than 10kB (average 250x250 PNG preview size)
|
return withPreviewAsOriginalImage(image, imageData);
|
||||||
return withPreviewAsOriginalImage(image);
|
|
||||||
} else if (image.getSize() > 102400 && image.getWidth() != 0) { // considering SVG image detailed after 100kB
|
} else if (image.getSize() > 102400 && image.getWidth() != 0) { // considering SVG image detailed after 100kB
|
||||||
// increasing preview dimensions
|
|
||||||
thumbnailMaxDimension = 512;
|
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();
|
ProcessedImage preview = new ProcessedImage();
|
||||||
preview.setWidth(thumbnailMaxDimension);
|
int[] thumbnailDimensions = getThumbnailDimensions(image.getWidth(), image.getHeight(), thumbnailMaxDimension, false);
|
||||||
preview.setHeight(thumbnailMaxDimension);
|
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.setMediaType("image/png");
|
||||||
preview.setData(pngThumbnail);
|
preview.setData(pngThumbnail);
|
||||||
preview.setSize(pngThumbnail.length);
|
preview.setSize(pngThumbnail.length);
|
||||||
@ -224,6 +205,27 @@ public class ImageUtils {
|
|||||||
return image;
|
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) {
|
private static ProcessedImage previewAsOriginalImage(byte[] data, String mediaType) {
|
||||||
ProcessedImage image = new ProcessedImage();
|
ProcessedImage image = new ProcessedImage();
|
||||||
image.setMediaType(mediaType);
|
image.setMediaType(mediaType);
|
||||||
@ -235,21 +237,34 @@ public class ImageUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static ProcessedImage withPreviewAsOriginalImage(ProcessedImage originalImage) {
|
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;
|
return originalImage;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ScadaSymbolMetadataInfo processScadaSymbolMetadata(String fileName, byte[] data) throws Exception {
|
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;
|
JsonNode metaData = null;
|
||||||
if (metaElements.getLength() > 0) {
|
String contents = new String(data, StandardCharsets.UTF_8);
|
||||||
metaData = JacksonUtil.toJsonNode(metaElements.item(0).getTextContent());
|
var matcher = Pattern.compile("(?s)<tb:metadata[^>]*><!\\[CDATA\\[(.*)]]><\\/tb:metadata>").matcher(contents);
|
||||||
|
if (matcher.find()) {
|
||||||
|
var metadataContent = matcher.group(1);
|
||||||
|
metaData = JacksonUtil.toJsonNode(metadataContent);
|
||||||
}
|
}
|
||||||
return new ScadaSymbolMetadataInfo(fileName, metaData);
|
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[^>]*>.*<\\/tb:metadata>", "");
|
||||||
|
return contents.getBytes(StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
|
||||||
private static int[] getThumbnailDimensions(int originalWidth, int originalHeight, int maxDimension, boolean originalIfSmaller) {
|
private static int[] getThumbnailDimensions(int originalWidth, int originalHeight, int maxDimension, boolean originalIfSmaller) {
|
||||||
if (originalWidth <= maxDimension && originalHeight <= maxDimension && originalIfSmaller) {
|
if (originalWidth <= maxDimension && originalHeight <= maxDimension && originalIfSmaller) {
|
||||||
return new int[]{originalWidth, originalHeight};
|
return new int[]{originalWidth, originalHeight};
|
||||||
|
|||||||
19
pom.xml
19
pom.xml
@ -163,7 +163,7 @@
|
|||||||
<slack-api.version>1.39.0</slack-api.version>
|
<slack-api.version>1.39.0</slack-api.version>
|
||||||
<oshi.version>6.6.0</oshi.version>
|
<oshi.version>6.6.0</oshi.version>
|
||||||
<google-oauth-client.version>1.35.0</google-oauth-client.version>
|
<google-oauth-client.version>1.35.0</google-oauth-client.version>
|
||||||
<apache-xmlgraphics.version>1.17</apache-xmlgraphics.version>
|
<weisj-jsvg.version>1.6.1</weisj-jsvg.version>
|
||||||
<drewnoakes-metadata-extractor.version>2.19.0</drewnoakes-metadata-extractor.version>
|
<drewnoakes-metadata-extractor.version>2.19.0</drewnoakes-metadata-extractor.version>
|
||||||
<firebase-admin.version>9.2.0</firebase-admin.version>
|
<firebase-admin.version>9.2.0</firebase-admin.version>
|
||||||
</properties>
|
</properties>
|
||||||
@ -2262,20 +2262,9 @@
|
|||||||
<version>${google-oauth-client.version}</version>
|
<version>${google-oauth-client.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.xmlgraphics</groupId>
|
<groupId>com.github.weisj</groupId>
|
||||||
<artifactId>batik-transcoder</artifactId>
|
<artifactId>jsvg</artifactId>
|
||||||
<version>${apache-xmlgraphics.version}</version>
|
<version>${weisj-jsvg.version}</version>
|
||||||
<exclusions>
|
|
||||||
<exclusion>
|
|
||||||
<groupId>commons-logging</groupId>
|
|
||||||
<artifactId>commons-logging</artifactId>
|
|
||||||
</exclusion>
|
|
||||||
</exclusions>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.apache.xmlgraphics</groupId>
|
|
||||||
<artifactId>batik-codec</artifactId>
|
|
||||||
<version>${apache-xmlgraphics.version}</version>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.drewnoakes</groupId>
|
<groupId>com.drewnoakes</groupId>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user