Fixes and improvements for images processing

This commit is contained in:
ViacheslavKlimov 2023-11-30 20:49:09 +02:00
parent ddfa55ba6e
commit 2d26fdcb95
8 changed files with 139 additions and 109 deletions

View File

@ -18,15 +18,11 @@ package org.thingsboard.server.service.install.update;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.HasImage;
import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.WidgetTypeId;
import org.thingsboard.server.common.data.page.PageDataIterable; import org.thingsboard.server.common.data.page.PageDataIterable;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.dao.Dao;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
import org.thingsboard.server.dao.asset.AssetProfileDao; import org.thingsboard.server.dao.asset.AssetProfileDao;
import org.thingsboard.server.dao.dashboard.DashboardDao; import org.thingsboard.server.dao.dashboard.DashboardDao;
import org.thingsboard.server.dao.device.DeviceProfileDao; import org.thingsboard.server.dao.device.DeviceProfileDao;
@ -34,6 +30,9 @@ import org.thingsboard.server.dao.resource.ImageService;
import org.thingsboard.server.dao.widget.WidgetTypeDao; import org.thingsboard.server.dao.widget.WidgetTypeDao;
import org.thingsboard.server.dao.widget.WidgetsBundleDao; import org.thingsboard.server.dao.widget.WidgetsBundleDao;
import java.util.function.BiFunction;
import java.util.function.Function;
@Component @Component
@RequiredArgsConstructor @RequiredArgsConstructor
@Slf4j @Slf4j
@ -48,108 +47,78 @@ public class ImagesUpdater {
public void updateWidgetsBundlesImages() { public void updateWidgetsBundlesImages() {
log.info("Updating widgets bundles images..."); log.info("Updating widgets bundles images...");
var widgetsBundles = new PageDataIterable<>(widgetsBundleDao::findAllWidgetsBundles, 128); var widgetsBundles = new PageDataIterable<>(widgetsBundleDao::findAllWidgetsBundles, 128);
int updatedCount = 0; updateImages(widgetsBundles, "bundle", imageService::replaceBase64WithImageUrl, widgetsBundleDao);
int totalCount = 0;
for (WidgetsBundle widgetsBundle : widgetsBundles) {
totalCount++;
try {
boolean updated = imageService.replaceBase64WithImageUrl(widgetsBundle, "bundle");
if (updated) {
widgetsBundleDao.save(widgetsBundle.getTenantId(), widgetsBundle);
log.debug("[{}][{}][{}] Updated widgets bundle images", widgetsBundle.getTenantId(), widgetsBundle.getId(), widgetsBundle.getTitle());
updatedCount++;
}
} catch (Exception e) {
log.error("[{}][{}][{}] Failed to update widgets bundle images", widgetsBundle.getTenantId(), widgetsBundle.getId(), widgetsBundle.getTitle(), e);
}
}
log.info("Updated {} widgets bundles out of {}", updatedCount, totalCount);
} }
public void updateWidgetTypesImages() { public void updateWidgetTypesImages() {
log.info("Updating widget types images..."); log.info("Updating widget types images...");
var widgetTypes = new PageDataIterable<>(widgetTypeDao::findAllWidgetTypesIds, 1024); var widgetTypesIds = new PageDataIterable<>(widgetTypeDao::findAllWidgetTypesIds, 1024);
int updatedCount = 0; updateImages(widgetTypesIds, "widget type", imageService::replaceBase64WithImageUrl, widgetTypeDao);
int totalCount = 0;
for (WidgetTypeId widgetTypeId : widgetTypes) {
totalCount++;
WidgetTypeDetails widgetTypeDetails = widgetTypeDao.findById(TenantId.SYS_TENANT_ID, widgetTypeId.getId());
try {
boolean updated = imageService.replaceBase64WithImageUrl(widgetTypeDetails);
if (updated) {
widgetTypeDao.save(widgetTypeDetails.getTenantId(), widgetTypeDetails);
log.debug("[{}][{}][{}] Updated widget type images", widgetTypeDetails.getTenantId(), widgetTypeDetails.getId(), widgetTypeDetails.getName());
updatedCount++;
}
} catch (Exception e) {
log.error("[{}][{}][{}] Failed to update widget type images", widgetTypeDetails.getTenantId(), widgetTypeDetails.getId(), widgetTypeDetails.getName(), e);
}
}
log.info("Updated {} widget types out of {}", updatedCount, totalCount);
} }
public void updateDashboardsImages() { public void updateDashboardsImages() {
log.info("Updating dashboards images..."); log.info("Updating dashboards images...");
var dashboards = new PageDataIterable<>(dashboardDao::findAllIds, 1024); var dashboardsIds = new PageDataIterable<>(dashboardDao::findAllIds, 1024);
int updatedCount = 0; updateImages(dashboardsIds, "dashboard", imageService::replaceBase64WithImageUrl, dashboardDao);
int totalCount = 0;
for (DashboardId dashboardId : dashboards) {
totalCount++;
Dashboard dashboard = dashboardDao.findById(TenantId.SYS_TENANT_ID, dashboardId.getId());
try {
boolean updated = imageService.replaceBase64WithImageUrl(dashboard);
if (updated) {
dashboardDao.save(dashboard.getTenantId(), dashboard);
log.info("[{}][{}][{}] Updated dashboard images", dashboard.getTenantId(), dashboardId, dashboard.getTitle());
updatedCount++;
}
} catch (Exception e) {
log.error("[{}][{}][{}] Failed to update dashboard images", dashboard.getTenantId(), dashboardId, dashboard.getTitle(), e);
}
}
log.info("Updated {} dashboards out of {}", updatedCount, totalCount);
} }
public void updateDeviceProfilesImages() { public void updateDeviceProfilesImages() {
log.info("Updating device profiles images..."); log.info("Updating device profiles images...");
var deviceProfiles = new PageDataIterable<>(deviceProfileDao::findAll, 256); var deviceProfiles = new PageDataIterable<>(deviceProfileDao::findAll, 256);
int updatedCount = 0; updateImages(deviceProfiles, "device profile", imageService::replaceBase64WithImageUrl, deviceProfileDao);
int totalCount = 0;
for (DeviceProfile deviceProfile : deviceProfiles) {
totalCount++;
try {
boolean updated = imageService.replaceBase64WithImageUrl(deviceProfile, "device profile");
if (updated) {
deviceProfileDao.save(deviceProfile.getTenantId(), deviceProfile);
log.debug("[{}][{}][{}] Updated device profile images", deviceProfile.getTenantId(), deviceProfile.getId(), deviceProfile.getName());
updatedCount++;
}
} catch (Exception e) {
log.error("[{}][{}][{}] Failed to update device profile images", deviceProfile.getTenantId(), deviceProfile.getId(), deviceProfile.getName(), e);
}
}
log.info("Updated {} device profiles out of {}", updatedCount, totalCount);
} }
public void updateAssetProfilesImages() { public void updateAssetProfilesImages() {
log.info("Updating asset profiles images..."); log.info("Updating asset profiles images...");
var assetProfiles = new PageDataIterable<>(assetProfileDao::findAll, 256); var assetProfiles = new PageDataIterable<>(assetProfileDao::findAll, 256);
updateImages(assetProfiles, "asset profile", imageService::replaceBase64WithImageUrl, assetProfileDao);
}
private <E extends HasImage> void updateImages(Iterable<E> entities, String type,
BiFunction<E, String, Boolean> updater, Dao<E> dao) {
int updatedCount = 0; int updatedCount = 0;
int totalCount = 0; int totalCount = 0;
for (AssetProfile assetProfile : assetProfiles) { for (E entity : entities) {
totalCount++; totalCount++;
try { try {
boolean updated = imageService.replaceBase64WithImageUrl(assetProfile, "asset profile"); boolean updated = updater.apply(entity, type);
if (updated) { if (updated) {
assetProfileDao.save(assetProfile.getTenantId(), assetProfile); dao.save(entity.getTenantId(), entity);
log.debug("[{}][{}][{}] Updated asset profile images", assetProfile.getTenantId(), assetProfile.getId(), assetProfile.getName()); log.debug("[{}][{}] Updated {} images", entity.getTenantId(), entity.getName(), type);
updatedCount++; updatedCount++;
} }
} catch (Exception e) { } catch (Exception e) {
log.error("[{}][{}][{}] Failed to update asset profile images", assetProfile.getTenantId(), assetProfile.getId(), assetProfile.getName(), e); log.error("[{}][{}] Failed to update {} images", entity.getTenantId(), entity.getName(), type, e);
}
if (totalCount % 100 == 0) {
log.info("Processed {} {}s so far", totalCount, type);
} }
} }
log.info("Updated {} asset profiles out of {}", updatedCount, totalCount); log.info("Updated {} {}s out of {}", updatedCount, type, totalCount);
}
private <E extends HasImage> void updateImages(Iterable<? extends EntityId> entitiesIds, String type,
Function<E, Boolean> updater, Dao<E> dao) {
int updatedCount = 0;
int totalCount = 0;
for (EntityId id : entitiesIds) {
totalCount++;
E entity = dao.findById(TenantId.SYS_TENANT_ID, id.getId());
try {
boolean updated = updater.apply(entity);
if (updated) {
dao.save(entity.getTenantId(), entity);
log.debug("[{}][{}] Updated {} images", entity.getTenantId(), entity.getName(), type);
updatedCount++;
}
} catch (Exception e) {
log.error("[{}][{}] Failed to update {} images", entity.getTenantId(), entity.getName(), type, e);
}
if (totalCount % 100 == 0) {
log.info("Processed {} {}s so far", totalCount, type);
}
}
log.info("Updated {} {}s out of {}", updatedCount, type, totalCount);
} }
} }

View File

@ -242,6 +242,10 @@
<groupId>org.apache.xmlgraphics</groupId> <groupId>org.apache.xmlgraphics</groupId>
<artifactId>batik-codec</artifactId> <artifactId>batik-codec</artifactId>
</dependency> </dependency>
<dependency>
<groupId>com.drewnoakes</groupId>
<artifactId>metadata-extractor</artifactId>
</dependency>
</dependencies> </dependencies>
<build> <build>
<plugins> <plugins>

View File

@ -19,6 +19,7 @@ import com.fasterxml.jackson.core.JsonProcessingException;
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.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.base.Strings;
import lombok.Data; import lombok.Data;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -285,9 +286,11 @@ public class BaseImageService extends BaseResourceService implements ImageServic
if (entity.getDescriptor().isObject()) { if (entity.getDescriptor().isObject()) {
ObjectNode descriptor = (ObjectNode) entity.getDescriptor(); ObjectNode descriptor = (ObjectNode) entity.getDescriptor();
JsonNode defaultConfig = JacksonUtil.toJsonNode(descriptor.get("defaultConfig").asText()); JsonNode defaultConfig = JacksonUtil.toJsonNode(descriptor.get("defaultConfig").asText());
if (defaultConfig != null && defaultConfig.isObject()) {
updated |= base64ToImageUrlUsingMapping(entity.getTenantId(), WIDGET_TYPE_BASE64_MAPPING, Collections.singletonMap("prefix", prefix), defaultConfig); updated |= base64ToImageUrlUsingMapping(entity.getTenantId(), WIDGET_TYPE_BASE64_MAPPING, Collections.singletonMap("prefix", prefix), defaultConfig);
descriptor.put("defaultConfig", defaultConfig.toString()); descriptor.put("defaultConfig", defaultConfig.toString());
} }
}
updated |= base64ToImageUrlRecursively(entity.getTenantId(), prefix, entity.getDescriptor()); updated |= base64ToImageUrlRecursively(entity.getTenantId(), prefix, entity.getDescriptor());
return updated; return updated;
} }
@ -313,6 +316,9 @@ public class BaseImageService extends BaseResourceService implements ImageServic
JsonPathProcessingTask task = tasks.poll(); JsonPathProcessingTask task = tasks.poll();
String token = task.currentToken(); String token = task.currentToken();
JsonNode node = task.getNode(); JsonNode node = task.getNode();
if (node == null) {
continue;
}
if (token.equals("*") || token.startsWith("$")) { if (token.equals("*") || token.startsWith("$")) {
String variableName = token.startsWith("$") ? token.substring(1) : null; String variableName = token.startsWith("$") ? token.substring(1) : null;
if (node.isArray()) { if (node.isArray()) {
@ -345,8 +351,8 @@ public class BaseImageService extends BaseResourceService implements ImageServic
} }
if (task.isLast()) { if (task.isLast()) {
String name = expression; String name = expression;
for (var replacements : task.getVariables().entrySet()) { for (var replacement : task.getVariables().entrySet()) {
name = name.replace("$" + replacements.getKey(), replacements.getValue()); name = name.replace("$" + replacement.getKey(), Strings.nullToEmpty(replacement.getValue()));
} }
if (node.isObject() && value.isTextual()) { if (node.isObject() && value.isTextual()) {
var result = base64ToImageUrl(tenantId, name, value.asText()); var result = base64ToImageUrl(tenantId, name, value.asText());
@ -362,7 +368,7 @@ public class BaseImageService extends BaseResourceService implements ImageServic
} }
} }
} else { } else {
if (StringUtils.isNotEmpty(variableName) && StringUtils.isNotEmpty(variableValue)) { if (StringUtils.isNotEmpty(variableName)) {
tasks.add(task.next(value, variableName, variableValue)); tasks.add(task.next(value, variableName, variableValue));
} else { } else {
tasks.add(task.next(value)); tasks.add(task.next(value));
@ -404,6 +410,9 @@ public class BaseImageService extends BaseResourceService implements ImageServic
byte[] imageData = Base64.getDecoder().decode(base64Data); byte[] imageData = Base64.getDecoder().decode(base64Data);
String etag = calculateEtag(imageData); String etag = calculateEtag(imageData);
var imageInfo = findImageByTenantIdAndEtag(tenantId, etag); var imageInfo = findImageByTenantIdAndEtag(tenantId, etag);
if (imageInfo == null && !tenantId.isSysTenantId()) {
imageInfo = findImageByTenantIdAndEtag(TenantId.SYS_TENANT_ID, etag);
}
if (imageInfo == null) { if (imageInfo == null) {
TbResource image = new TbResource(); TbResource image = new TbResource();
image.setTenantId(tenantId); image.setTenantId(tenantId);
@ -415,9 +424,9 @@ public class BaseImageService extends BaseResourceService implements ImageServic
String fileName; String fileName;
if (StringUtils.isBlank(mdResourceKey)) { if (StringUtils.isBlank(mdResourceKey)) {
fileName = mdResourceName.toLowerCase() fileName = StringUtils.strip(mdResourceName.toLowerCase()
.replace("'", "").replace("\"", "") .replaceAll("['\"]", "")
.replace(" ", "_").replace("/", "_") .replaceAll("[^\\pL\\d]+", "_"), "_") // leaving only letters and numbers
+ "." + extension; + "." + extension;
} else { } else {
fileName = mdResourceKey; fileName = mdResourceKey;

View File

@ -43,7 +43,7 @@ public interface DashboardRepository extends JpaRepository<DashboardEntity, UUID
@Query("SELECT d.id FROM DashboardEntity d WHERE d.tenantId = :tenantId") @Query("SELECT d.id FROM DashboardEntity d WHERE d.tenantId = :tenantId")
Page<UUID> findIdsByTenantId(@Param("tenantId") UUID tenantId, Pageable pageable); Page<UUID> findIdsByTenantId(@Param("tenantId") UUID tenantId, Pageable pageable);
@Query("SELECT id FROM DashboardEntity") @Query("SELECT d.id FROM DashboardEntity d")
Page<UUID> findAllIds(Pageable pageable); Page<UUID> findAllIds(Pageable pageable);
} }

View File

@ -91,7 +91,7 @@ public interface TbResourceRepository extends JpaRepository<TbResourceEntity, UU
@Query("SELECT externalId FROM TbResourceInfoEntity WHERE id = :id") @Query("SELECT externalId FROM TbResourceInfoEntity WHERE id = :id")
UUID getExternalIdByInternal(@Param("id") UUID internalId); UUID getExternalIdByInternal(@Param("id") UUID internalId);
@Query("SELECT id FROM TbResourceInfoEntity WHERE tenantId = :tenantId") @Query("SELECT r.id FROM TbResourceInfoEntity r WHERE r.tenantId = :tenantId")
Page<UUID> findIdsByTenantId(@Param("tenantId") UUID tenantId, Pageable pageable); Page<UUID> findIdsByTenantId(@Param("tenantId") UUID tenantId, Pageable pageable);
} }

View File

@ -76,10 +76,10 @@ public interface WidgetTypeRepository extends JpaRepository<WidgetTypeDetailsEnt
@Query("SELECT externalId FROM WidgetTypeDetailsEntity WHERE id = :id") @Query("SELECT externalId FROM WidgetTypeDetailsEntity WHERE id = :id")
UUID getExternalIdById(@Param("id") UUID id); UUID getExternalIdById(@Param("id") UUID id);
@Query("SELECT id FROM WidgetTypeDetailsEntity") @Query("SELECT w.id FROM WidgetTypeDetailsEntity w")
Page<UUID> findAllIds(Pageable pageable); Page<UUID> findAllIds(Pageable pageable);
@Query("SELECT id FROM WidgetTypeDetailsEntity WHERE tenantId = :tenantId") @Query("SELECT w.id FROM WidgetTypeDetailsEntity w WHERE w.tenantId = :tenantId")
Page<UUID> findIdsByTenantId(@Param("tenantId") UUID tenantId, Pageable pageable); Page<UUID> findIdsByTenantId(@Param("tenantId") UUID tenantId, Pageable pageable);
} }

View File

@ -15,9 +15,14 @@
*/ */
package org.thingsboard.server.dao.util; package org.thingsboard.server.dao.util;
import com.drew.imaging.ImageMetadataReader;
import com.drew.metadata.Directory;
import com.drew.metadata.Metadata;
import com.drew.metadata.Tag;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.batik.anim.dom.SAXSVGDocumentFactory; import org.apache.batik.anim.dom.SAXSVGDocumentFactory;
import org.apache.batik.bridge.BridgeContext; import org.apache.batik.bridge.BridgeContext;
import org.apache.batik.bridge.DocumentLoader; import org.apache.batik.bridge.DocumentLoader;
@ -41,11 +46,13 @@ import java.io.ByteArrayOutputStream;
import java.util.Map; import java.util.Map;
@NoArgsConstructor(access = AccessLevel.PRIVATE) @NoArgsConstructor(access = AccessLevel.PRIVATE)
@Slf4j
public class ImageUtils { public class ImageUtils {
private static final Map<String, String> mediaTypeMappings = Map.of( private static final Map<String, String> mediaTypeMappings = Map.of(
"jpeg", "jpg", "jpeg", "jpg",
"svg+xml", "svg" "svg+xml", "svg",
"x-icon", "ico"
); );
public static String mediaTypeToFileExtension(String mimeType) { public static String mediaTypeToFileExtension(String mimeType) {
@ -64,15 +71,49 @@ public class ImageUtils {
if (mediaTypeToFileExtension(mediaType).equals("svg")) { if (mediaTypeToFileExtension(mediaType).equals("svg")) {
return processSvgImage(data, mediaType, thumbnailMaxDimension); return processSvgImage(data, mediaType, thumbnailMaxDimension);
} }
BufferedImage bufferedImage = ImageIO.read(new ByteArrayInputStream(data));
ProcessedImage image = new ProcessedImage(); ProcessedImage image = new ProcessedImage();
image.setMediaType(mediaType); image.setMediaType(mediaType);
image.setWidth(bufferedImage.getWidth());
image.setHeight(bufferedImage.getHeight());
image.setData(data); image.setData(data);
image.setSize(data.length); image.setSize(data.length);
BufferedImage bufferedImage = null;
try {
bufferedImage = ImageIO.read(new ByteArrayInputStream(data));
} catch (Exception ignored) {
}
if (bufferedImage == null) { // means that media type is not supported by ImageIO; extracting width and height from metadata and leaving preview as original image
Metadata metadata = ImageMetadataReader.readMetadata(new ByteArrayInputStream(data));
for (Directory dir : metadata.getDirectories()) {
Tag widthTag = dir.getTags().stream()
.filter(tag -> tag.getTagName().toLowerCase().contains("width"))
.findFirst().orElse(null);
Tag heightTag = dir.getTags().stream()
.filter(tag -> tag.getTagName().toLowerCase().contains("height"))
.findFirst().orElse(null);
if (widthTag == null || heightTag == null) {
continue;
}
int width = Integer.parseInt(dir.getObject(widthTag.getTagType()).toString());
int height = Integer.parseInt(dir.getObject(widthTag.getTagType()).toString());
image.setWidth(width);
image.setHeight(height);
ProcessedImage preview = new ProcessedImage();
preview.setWidth(image.getWidth());
preview.setHeight(image.getHeight());
preview.setMediaType(mediaType);
preview.setData(null);
preview.setSize(data.length);
image.setPreview(preview);
log.debug("Couldn't parse {} ({}) with ImageIO, got width {} and height {} from metadata", mediaType, dir.getName(), width, height);
return image;
}
throw new IllegalArgumentException("Media type " + mediaType + " not supported");
}
image.setWidth(bufferedImage.getWidth());
image.setHeight(bufferedImage.getHeight());
ProcessedImage preview = new ProcessedImage(); ProcessedImage preview = new ProcessedImage();
int[] thumbnailDimensions = getThumbnailDimensions(image.getWidth(), image.getHeight(), thumbnailMaxDimension); int[] thumbnailDimensions = getThumbnailDimensions(image.getWidth(), image.getHeight(), thumbnailMaxDimension);
preview.setWidth(thumbnailDimensions[0]); preview.setWidth(thumbnailDimensions[0]);
@ -101,18 +142,19 @@ public class ImageUtils {
} }
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( SAXSVGDocumentFactory factory = new SAXSVGDocumentFactory(XMLResourceDescriptor.getXMLParserClassName());
XMLResourceDescriptor.getXMLParserClassName()); Document document = factory.createDocument(null, new ByteArrayInputStream(data));
Document document = factory.createDocument(
null, new ByteArrayInputStream(data));
Integer width = null; Integer width = null;
Integer height = null; Integer height = null;
String strWidth = document.getDocumentElement().getAttribute("width"); String strWidth = document.getDocumentElement().getAttribute("width");
String strHeight = document.getDocumentElement().getAttribute("height"); String strHeight = document.getDocumentElement().getAttribute("height");
if (StringUtils.isNotEmpty(strWidth) && StringUtils.isNotEmpty(strHeight)) { if (StringUtils.isNotEmpty(strWidth) && StringUtils.isNotEmpty(strHeight)) {
try {
width = (int) Double.parseDouble(strWidth); width = (int) Double.parseDouble(strWidth);
height = (int) Double.parseDouble(strHeight);; height = (int) Double.parseDouble(strHeight);
} else { } catch (NumberFormatException ignored) {} // in case width and height are in %, mm, etc.
}
if (width == null || height == null) {
String viewBox = document.getDocumentElement().getAttribute("viewBox"); String viewBox = document.getDocumentElement().getAttribute("viewBox");
if (StringUtils.isNotEmpty(viewBox)) { if (StringUtils.isNotEmpty(viewBox)) {
String[] viewBoxValues = viewBox.split(" "); String[] viewBoxValues = viewBox.split(" ");

View File

@ -154,6 +154,7 @@
<oshi.version>6.4.2</oshi.version> <oshi.version>6.4.2</oshi.version>
<google-oauth-client.version>1.34.1</google-oauth-client.version> <google-oauth-client.version>1.34.1</google-oauth-client.version>
<apache-xmlgraphics.version>1.17</apache-xmlgraphics.version> <apache-xmlgraphics.version>1.17</apache-xmlgraphics.version>
<drewnoakes-metadata-extractor.version>2.19.0</drewnoakes-metadata-extractor.version>
</properties> </properties>
<modules> <modules>
@ -2040,6 +2041,11 @@
<artifactId>batik-codec</artifactId> <artifactId>batik-codec</artifactId>
<version>${apache-xmlgraphics.version}</version> <version>${apache-xmlgraphics.version}</version>
</dependency> </dependency>
<dependency>
<groupId>com.drewnoakes</groupId>
<artifactId>metadata-extractor</artifactId>
<version>${drewnoakes-metadata-extractor.version}</version>
</dependency>
</dependencies> </dependencies>
</dependencyManagement> </dependencyManagement>