001/* 002 * This library is part of OpenCms - 003 * the Open Source Content Management System 004 * 005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com) 006 * 007 * This library is free software; you can redistribute it and/or 008 * modify it under the terms of the GNU Lesser General Public 009 * License as published by the Free Software Foundation; either 010 * version 2.1 of the License, or (at your option) any later version. 011 * 012 * This library is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 015 * Lesser General Public License for more details. 016 * 017 * For further information about Alkacon Software GmbH & Co. KG, please see the 018 * company website: http://www.alkacon.com 019 * 020 * For further information about OpenCms, please see the 021 * project website: http://www.opencms.org 022 * 023 * You should have received a copy of the GNU Lesser General Public 024 * License along with this library; if not, write to the Free Software 025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 026 */ 027 028package org.opencms.file.types; 029 030import org.opencms.configuration.CmsConfigurationException; 031import org.opencms.db.CmsSecurityManager; 032import org.opencms.file.CmsFile; 033import org.opencms.file.CmsObject; 034import org.opencms.file.CmsProperty; 035import org.opencms.file.CmsPropertyDefinition; 036import org.opencms.file.CmsResource; 037import org.opencms.file.CmsResourceFilter; 038import org.opencms.file.CmsVfsException; 039import org.opencms.loader.CmsDumpLoader; 040import org.opencms.loader.CmsImageLoader; 041import org.opencms.loader.CmsImageScaler; 042import org.opencms.main.CmsException; 043import org.opencms.main.CmsLog; 044import org.opencms.main.OpenCms; 045import org.opencms.security.CmsPermissionSet; 046import org.opencms.security.CmsSecurityException; 047import org.opencms.util.CmsStringUtil; 048 049import java.io.ByteArrayInputStream; 050import java.util.ArrayList; 051import java.util.HashMap; 052import java.util.List; 053import java.util.Map; 054import java.util.Objects; 055 056import org.apache.commons.logging.Log; 057 058import org.dom4j.Document; 059import org.dom4j.Element; 060import org.dom4j.io.SAXReader; 061 062/** 063 * Resource type descriptor for the type "image".<p> 064 * 065 * @since 6.0.0 066 */ 067public class CmsResourceTypeImage extends A_CmsResourceType { 068 069 /** 070 * A data container for image size and scale operations.<p> 071 */ 072 protected static class CmsImageAdjuster { 073 074 /** The image byte content. */ 075 private byte[] m_content; 076 077 /** The (optional) image scaler that contains the image downscale settings. */ 078 private CmsImageScaler m_imageDownScaler; 079 080 /** The image properties. */ 081 private List<CmsProperty> m_properties; 082 083 /** The image root path. */ 084 private String m_rootPath; 085 086 /** 087 * Creates a new image data container.<p> 088 * 089 * @param content the image byte content 090 * @param rootPath the image root path 091 * @param properties the image properties 092 * @param downScaler the (optional) image scaler that contains the image downscale settings 093 */ 094 public CmsImageAdjuster( 095 byte[] content, 096 String rootPath, 097 List<CmsProperty> properties, 098 CmsImageScaler downScaler) { 099 100 m_content = content; 101 m_rootPath = rootPath; 102 m_properties = properties; 103 m_imageDownScaler = downScaler; 104 } 105 106 /** 107 * Calculates the image size and adjusts the image dimensions (if required) accoring to the configured 108 * image downscale settings.<p> 109 * 110 * The image dimensions are always calculated from the given image. The internal list of properties is updated 111 * with a value for <code>{@link CmsPropertyDefinition#PROPERTY_IMAGE_SIZE}</code> that 112 * contains the calculated image dimensions.<p> 113 */ 114 public void adjust() { 115 116 CmsImageScaler scaler = new CmsImageScaler(getContent(), getRootPath()); 117 if (!scaler.isValid()) { 118 // error calculating image dimensions - this image can't be scaled or resized 119 return; 120 } 121 122 // check if the image is to big and needs to be rescaled 123 if (scaler.isDownScaleRequired(m_imageDownScaler)) { 124 // image is to big, perform rescale operation 125 CmsImageScaler downScaler = scaler.getDownScaler(m_imageDownScaler); 126 // perform the rescale using the adjusted size 127 m_content = downScaler.scaleImage(m_content, m_rootPath); 128 // image size has been changed, adjust the scaler for later setting of properties 129 scaler.setHeight(downScaler.getHeight()); 130 scaler.setWidth(downScaler.getWidth()); 131 } 132 133 CmsProperty p = new CmsProperty(CmsPropertyDefinition.PROPERTY_IMAGE_SIZE, null, scaler.toString()); 134 // create the new property list if required (don't modify the original List) 135 List<CmsProperty> result = new ArrayList<CmsProperty>(); 136 if ((m_properties != null) && (m_properties.size() > 0)) { 137 result.addAll(m_properties); 138 result.remove(p); 139 } 140 // add the updated property 141 result.add(p); 142 // store the changed properties 143 m_properties = result; 144 } 145 146 /** 147 * Returns the image content.<p> 148 * 149 * @return the image content 150 */ 151 public byte[] getContent() { 152 153 return m_content; 154 } 155 156 /** 157 * Returns the image properties.<p> 158 * 159 * @return the image properties 160 */ 161 public List<CmsProperty> getProperties() { 162 163 return m_properties; 164 } 165 166 /** 167 * Returns the image VFS root path.<p> 168 * 169 * @return the image VFS root path 170 */ 171 public String getRootPath() { 172 173 return m_rootPath; 174 } 175 } 176 177 /** 178 * Helper class for parsing SVG sizes.<p> 179 * 180 * Note: This is *not* intended as a general purpose tool, it is only used for parsing SVG sizes 181 * as part of the image.size property determination. 182 */ 183 private static class SvgSize { 184 185 /** The numeric value of the size. */ 186 private double m_size; 187 188 /** The unit of the size. */ 189 private String m_unit; 190 191 /** 192 * Parses the SVG size.<p> 193 * 194 * @param s the string containing the size 195 * 196 * @return the parsed size 197 */ 198 public static SvgSize parse(String s) { 199 200 if (CmsStringUtil.isEmptyOrWhitespaceOnly(s)) { 201 return null; 202 } 203 s = s.trim(); 204 double length = -1; 205 int unitPos; 206 String unit = ""; 207 // find longest prefix of s that can be parsed as a number, use the remaining part as the unit 208 for (unitPos = s.length(); unitPos >= 0; unitPos--) { 209 String prefix = s.substring(0, unitPos); 210 unit = s.substring(unitPos); 211 try { 212 length = Double.parseDouble(prefix); 213 break; 214 } catch (NumberFormatException e) { 215 // ignore 216 } 217 } 218 if (length < 0) { 219 LOG.warn("Invalid string for SVG size: " + s); 220 return null; 221 } 222 SvgSize result = new SvgSize(); 223 result.m_size = length; 224 result.m_unit = unit; 225 return result; 226 } 227 228 /** 229 * Gets the numeric value of the size.<p> 230 * 231 * @return the size 232 */ 233 public double getSize() { 234 235 return m_size; 236 } 237 238 /** 239 * Gets the unit of the size.<p> 240 * 241 * @return the unit 242 */ 243 public String getUnit() { 244 245 return m_unit; 246 247 } 248 249 /** 250 * @see java.lang.Object#toString() 251 */ 252 @Override 253 public String toString() { 254 255 return m_size + m_unit; 256 } 257 } 258 259 /** The log object for this class. */ 260 public static final Log LOG = CmsLog.getLog(CmsResourceTypeImage.class); 261 262 /** 263 * The value for the {@link CmsPropertyDefinition#PROPERTY_IMAGE_SIZE} property if resources in 264 * a folder should never be downscaled.<p> 265 */ 266 public static final String PROPERTY_VALUE_UNLIMITED = "unlimited"; 267 268 /** The default image preview provider. */ 269 private static final String GALLERY_PREVIEW_PROVIDER = "org.opencms.ade.galleries.preview.CmsImagePreviewProvider"; 270 271 /** The image scaler for the image downscale operation (if configured). */ 272 private static CmsImageScaler m_downScaler; 273 274 /** Indicates that the static configuration of the resource type has been frozen. */ 275 private static boolean m_staticFrozen; 276 277 /** The static resource loader id of this resource type. */ 278 private static int m_staticLoaderId; 279 280 /** The static type id of this resource type. */ 281 private static int m_staticTypeId; 282 283 /** The type id of this resource type. */ 284 private static final int RESOURCE_TYPE_ID = 3; 285 286 /** The name of this resource type. */ 287 private static final String RESOURCE_TYPE_NAME = "image"; 288 289 /** The serial version id. */ 290 private static final long serialVersionUID = -8708850913653288684L; 291 292 /** 293 * Default constructor, used to initialize member variables.<p> 294 */ 295 public CmsResourceTypeImage() { 296 297 super(); 298 m_typeId = RESOURCE_TYPE_ID; 299 m_typeName = RESOURCE_TYPE_NAME; 300 } 301 302 /** 303 * Returns the image downscaler to use when writing an image resource to the given root path.<p> 304 * 305 * If <code>null</code> is returned, image downscaling must not be used for the resource with the given path. 306 * This may be the case if image downscaling is not configured at all, or if image downscaling has been disabled 307 * for the parent folder by setting the folders property {@link CmsPropertyDefinition#PROPERTY_IMAGE_SIZE} 308 * to the value {@link #PROPERTY_VALUE_UNLIMITED}.<p> 309 * 310 * @param cms the current OpenCms user context 311 * @param rootPath the root path of the resource to write 312 * 313 * @return the downscaler to use, or <code>null</code> if no downscaling is required for the resource 314 */ 315 public static CmsImageScaler getDownScaler(CmsObject cms, String rootPath) { 316 317 if (m_downScaler == null) { 318 // downscaling is not configured at all 319 return null; 320 } 321 // try to read the image.size property from the parent folder 322 String parentFolder = CmsResource.getParentFolder(rootPath); 323 parentFolder = cms.getRequestContext().removeSiteRoot(parentFolder); 324 try { 325 CmsProperty fileSizeProperty = cms.readPropertyObject( 326 parentFolder, 327 CmsPropertyDefinition.PROPERTY_IMAGE_SIZE, 328 true); 329 if (!fileSizeProperty.isNullProperty()) { 330 // image.size property has been set 331 String value = fileSizeProperty.getValue().trim(); 332 if (CmsStringUtil.isNotEmpty(value)) { 333 if (PROPERTY_VALUE_UNLIMITED.equals(value)) { 334 // in this case no downscaling must be done 335 return null; 336 } else { 337 CmsImageScaler scaler = new CmsImageScaler(value); 338 if (scaler.isValid()) { 339 // special folder based scaler settings have been set 340 return scaler; 341 } 342 } 343 } 344 } 345 } catch (CmsException e) { 346 // ignore, continue with given downScaler 347 } 348 return (CmsImageScaler)m_downScaler.clone(); 349 } 350 351 /** 352 * Returns the static type id of this (default) resource type.<p> 353 * 354 * @return the static type id of this (default) resource type 355 */ 356 public static int getStaticTypeId() { 357 358 return m_staticTypeId; 359 } 360 361 /** 362 * Returns the static type name of this (default) resource type.<p> 363 * 364 * @return the static type name of this (default) resource type 365 */ 366 public static String getStaticTypeName() { 367 368 return RESOURCE_TYPE_NAME; 369 } 370 371 /** 372 * @see org.opencms.file.types.I_CmsResourceType#createResource(org.opencms.file.CmsObject, org.opencms.db.CmsSecurityManager, java.lang.String, byte[], java.util.List) 373 */ 374 @Override 375 public CmsResource createResource( 376 CmsObject cms, 377 CmsSecurityManager securityManager, 378 String resourcename, 379 byte[] content, 380 List<CmsProperty> properties) 381 throws CmsException { 382 383 if (resourcename.toLowerCase().endsWith(".svg")) { 384 List<CmsProperty> prop2 = tryAddImageSizeFromSvg(content, properties); 385 properties = prop2; 386 } else if (CmsImageLoader.isEnabled()) { 387 String rootPath = cms.getRequestContext().addSiteRoot(resourcename); 388 // get the downscaler to use 389 CmsImageScaler downScaler = getDownScaler(cms, rootPath); 390 // create a new image scale adjuster 391 CmsImageAdjuster adjuster = new CmsImageAdjuster(content, rootPath, properties, downScaler); 392 // update the image scale adjuster - this will calculate the image dimensions and (optionally) downscale the size 393 adjuster.adjust(); 394 // continue with the updated content and properties 395 content = adjuster.getContent(); 396 properties = adjuster.getProperties(); 397 } 398 return super.createResource(cms, securityManager, resourcename, content, properties); 399 } 400 401 /** 402 * @see org.opencms.file.types.I_CmsResourceType#getGalleryPreviewProvider() 403 */ 404 @Override 405 public String getGalleryPreviewProvider() { 406 407 if (m_galleryPreviewProvider == null) { 408 m_galleryPreviewProvider = getConfiguration().getString( 409 CONFIGURATION_GALLERY_PREVIEW_PROVIDER, 410 GALLERY_PREVIEW_PROVIDER); 411 } 412 return m_galleryPreviewProvider; 413 } 414 415 /** 416 * @see org.opencms.file.types.I_CmsResourceType#getLoaderId() 417 */ 418 @Override 419 public int getLoaderId() { 420 421 return m_staticLoaderId; 422 } 423 424 /** 425 * @see org.opencms.file.types.I_CmsResourceType#importResource(org.opencms.file.CmsObject, org.opencms.db.CmsSecurityManager, java.lang.String, org.opencms.file.CmsResource, byte[], java.util.List) 426 */ 427 @Override 428 public CmsResource importResource( 429 CmsObject cms, 430 CmsSecurityManager securityManager, 431 String resourcename, 432 CmsResource resource, 433 byte[] content, 434 List<CmsProperty> properties) 435 throws CmsException { 436 437 if (resourcename.toLowerCase().endsWith(".svg")) { 438 properties = tryAddImageSizeFromSvg(content, properties); 439 } else if (CmsImageLoader.isEnabled()) { 440 // siblings have null content in import 441 if (content != null) { 442 // get the downscaler to use 443 CmsImageScaler downScaler = getDownScaler(cms, resource.getRootPath()); 444 // create a new image scale adjuster 445 CmsImageAdjuster adjuster = new CmsImageAdjuster( 446 content, 447 resource.getRootPath(), 448 properties, 449 downScaler); 450 // update the image scale adjuster - this will calculate the image dimensions and (optionally) adjust the size 451 adjuster.adjust(); 452 // continue with the updated content and properties 453 content = adjuster.getContent(); 454 properties = adjuster.getProperties(); 455 } 456 } 457 return super.importResource(cms, securityManager, resourcename, resource, content, properties); 458 } 459 460 /** 461 * @see org.opencms.file.types.A_CmsResourceType#initConfiguration(java.lang.String, java.lang.String, String) 462 */ 463 @Override 464 public void initConfiguration(String name, String id, String className) throws CmsConfigurationException { 465 466 if ((OpenCms.getRunLevel() > OpenCms.RUNLEVEL_2_INITIALIZING) && m_staticFrozen) { 467 // configuration already frozen 468 throw new CmsConfigurationException( 469 Messages.get().container( 470 Messages.ERR_CONFIG_FROZEN_3, 471 this.getClass().getName(), 472 getStaticTypeName(), 473 new Integer(getStaticTypeId()))); 474 } 475 476 if (!RESOURCE_TYPE_NAME.equals(name)) { 477 // default resource type MUST have default name 478 throw new CmsConfigurationException( 479 Messages.get().container( 480 Messages.ERR_INVALID_RESTYPE_CONFIG_NAME_3, 481 this.getClass().getName(), 482 RESOURCE_TYPE_NAME, 483 name)); 484 } 485 486 // freeze the configuration 487 m_staticFrozen = true; 488 489 super.initConfiguration(RESOURCE_TYPE_NAME, id, className); 490 // set static members with values from the configuration 491 m_staticTypeId = m_typeId; 492 493 if (CmsImageLoader.isEnabled()) { 494 // the image loader is enabled, image operations are supported 495 m_staticLoaderId = CmsImageLoader.RESOURCE_LOADER_ID_IMAGE_LOADER; 496 // set the maximum size scaler 497 String downScaleParams = CmsImageLoader.getDownScaleParams(); 498 if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(downScaleParams)) { 499 m_downScaler = new CmsImageScaler(downScaleParams); 500 if (!m_downScaler.isValid()) { 501 // ignore invalid parameters 502 m_downScaler = null; 503 } 504 } 505 } else { 506 // no image operations are supported, use dump loader 507 m_staticLoaderId = CmsDumpLoader.RESOURCE_LOADER_ID; 508 // disable maximum image size operation 509 m_downScaler = null; 510 } 511 } 512 513 /** 514 * @see org.opencms.file.types.I_CmsResourceType#replaceResource(org.opencms.file.CmsObject, org.opencms.db.CmsSecurityManager, org.opencms.file.CmsResource, int, byte[], java.util.List) 515 */ 516 @Override 517 public void replaceResource( 518 CmsObject cms, 519 CmsSecurityManager securityManager, 520 CmsResource resource, 521 int type, 522 byte[] content, 523 List<CmsProperty> properties) 524 throws CmsException { 525 526 if (resource.getRootPath().toLowerCase().endsWith(".svg")) { 527 List<CmsProperty> newProperties = tryAddImageSizeFromSvg(content, properties); 528 if (properties != newProperties) { // yes, we actually do want to compare object identity here 529 writePropertyObjects(cms, securityManager, resource, newProperties); 530 } 531 } else if (CmsImageLoader.isEnabled()) { 532 // check if the user has write access and if resource is locked 533 // done here so that no image operations are performed in case no write access is granted 534 securityManager.checkPermissions( 535 cms.getRequestContext(), 536 resource, 537 CmsPermissionSet.ACCESS_WRITE, 538 true, 539 CmsResourceFilter.ALL); 540 541 // get the downscaler to use 542 CmsImageScaler downScaler = getDownScaler(cms, resource.getRootPath()); 543 // create a new image scale adjuster 544 CmsImageAdjuster adjuster = new CmsImageAdjuster(content, resource.getRootPath(), properties, downScaler); 545 // update the image scale adjuster - this will calculate the image dimensions and (optionally) adjust the size 546 adjuster.adjust(); 547 // continue with the updated content 548 content = adjuster.getContent(); 549 if (adjuster.getProperties() != null) { 550 // write properties 551 writePropertyObjects(cms, securityManager, resource, adjuster.getProperties()); 552 } 553 } 554 super.replaceResource(cms, securityManager, resource, type, content, properties); 555 } 556 557 /** 558 * @see org.opencms.file.types.I_CmsResourceType#writeFile(org.opencms.file.CmsObject, org.opencms.db.CmsSecurityManager, org.opencms.file.CmsFile) 559 */ 560 @Override 561 public CmsFile writeFile(CmsObject cms, CmsSecurityManager securityManager, CmsFile resource) 562 throws CmsException, CmsVfsException, CmsSecurityException { 563 564 if (CmsImageLoader.isEnabled()) { 565 // check if the user has write access and if resource is locked 566 // done here so that no image operations are performed in case no write access is granted 567 securityManager.checkPermissions( 568 cms.getRequestContext(), 569 resource, 570 CmsPermissionSet.ACCESS_WRITE, 571 true, 572 CmsResourceFilter.ALL); 573 574 // get the downscaler to use 575 CmsImageScaler downScaler = getDownScaler(cms, resource.getRootPath()); 576 // create a new image scale adjuster 577 CmsImageAdjuster adjuster = new CmsImageAdjuster( 578 resource.getContents(), 579 resource.getRootPath(), 580 null, 581 downScaler); 582 // update the image scale adjuster - this will calculate the image dimensions and (optionally) adjust the size 583 adjuster.adjust(); 584 // continue with the updated content 585 resource.setContents(adjuster.getContent()); 586 if (adjuster.getProperties() != null) { 587 // write properties 588 writePropertyObjects(cms, securityManager, resource, adjuster.getProperties()); 589 } 590 } 591 return super.writeFile(cms, securityManager, resource); 592 } 593 594 /** 595 * Tries to use the viewbox from the SVG data to determine the image size and add it to the list of properties.<p> 596 * 597 * @param content the content bytes of an SVG file 598 * @param properties the original properties (this object will not be modified) 599 * 600 * @return the amended properties 601 */ 602 protected List<CmsProperty> tryAddImageSizeFromSvg(byte[] content, List<CmsProperty> properties) { 603 604 if ((content == null) || (content.length == 0)) { 605 return properties; 606 } 607 List<CmsProperty> newProps = properties; 608 try { 609 double w = -1, h = -1; 610 SAXReader reader = new SAXReader(); 611 Document doc = reader.read(new ByteArrayInputStream(content)); 612 Element node = (Element)(doc.selectSingleNode("/svg")); 613 if (node != null) { 614 String widthStr = node.attributeValue("width"); 615 String heightStr = node.attributeValue("height"); 616 SvgSize width = SvgSize.parse(widthStr); 617 SvgSize height = SvgSize.parse(heightStr); 618 if ((width != null) && (height != null) && Objects.equals(width.getUnit(), height.getUnit())) { 619 // If width and height are given and have the same units, just interpret them as pixels, otherwise use viewbox 620 w = width.getSize(); 621 h = height.getSize(); 622 } else { 623 String viewboxStr = node.attributeValue("viewBox"); 624 if (viewboxStr != null) { 625 viewboxStr = viewboxStr.replace(",", " "); 626 String[] viewboxParts = viewboxStr.trim().split(" +"); 627 if (viewboxParts.length == 4) { 628 w = Double.parseDouble(viewboxParts[2]); 629 h = Double.parseDouble(viewboxParts[3]); 630 } 631 } 632 } 633 if ((w > 0) && (h > 0)) { 634 String propValue = "w:" + (int)Math.round(w) + ",h:" + (int)Math.round(h); 635 Map<String, CmsProperty> propsMap = properties == null 636 ? new HashMap<>() 637 : CmsProperty.toObjectMap(properties); 638 propsMap.put( 639 CmsPropertyDefinition.PROPERTY_IMAGE_SIZE, 640 new CmsProperty(CmsPropertyDefinition.PROPERTY_IMAGE_SIZE, null, propValue)); 641 newProps = new ArrayList<>(propsMap.values()); 642 } 643 644 } 645 } catch (Exception e) { 646 LOG.error("Error while trying to determine size of SVG: " + e.getLocalizedMessage(), e); 647 } 648 return newProps; 649 } 650 651}