|
|
|
@ -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[^>]*><!\\[CDATA\\[(.*)]]><\\/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[^>]*>.*<\\/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}; |
|
|
|
|