001// -------------------------------------------------------------------------------- 002// Copyright 2002-2025 Echo Three, LLC 003// 004// Licensed under the Apache License, Version 2.0 (the "License"); 005// you may not use this file except in compliance with the License. 006// You may obtain a copy of the License at 007// 008// http://www.apache.org/licenses/LICENSE-2.0 009// 010// Unless required by applicable law or agreed to in writing, software 011// distributed under the License is distributed on an "AS IS" BASIS, 012// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013// See the License for the specific language governing permissions and 014// limitations under the License. 015// -------------------------------------------------------------------------------- 016 017package com.echothree.model.control.item.server.logic; 018 019import com.echothree.model.control.core.common.EntityAttributeTypes; 020import com.echothree.model.control.core.common.MimeTypeUsageTypes; 021import com.echothree.model.control.item.common.exception.InvalidItemDescriptionTypeException; 022import com.echothree.model.control.item.common.exception.UnknownItemDescriptionTypeNameException; 023import com.echothree.model.control.item.server.control.ItemControl; 024import com.echothree.model.control.party.server.control.PartyControl; 025import com.echothree.model.data.core.server.entity.MimeType; 026import com.echothree.model.data.item.server.entity.Item; 027import com.echothree.model.data.item.server.entity.ItemBlobDescription; 028import com.echothree.model.data.item.server.entity.ItemDescription; 029import com.echothree.model.data.item.server.entity.ItemDescriptionType; 030import com.echothree.model.data.item.server.value.ItemImageDescriptionTypeValue; 031import com.echothree.model.data.item.server.value.ItemImageTypeDetailValue; 032import com.echothree.model.data.party.server.entity.Language; 033import com.echothree.model.data.party.server.entity.Party; 034import com.echothree.util.common.exception.PersistenceDatabaseException; 035import com.echothree.util.common.message.ExecutionErrors; 036import com.echothree.util.common.persistence.BasePK; 037import com.echothree.util.common.persistence.type.ByteArray; 038import com.echothree.util.server.control.BaseLogic; 039import com.echothree.util.server.message.ExecutionErrorAccumulator; 040import com.echothree.util.server.persistence.PersistenceUtils; 041import com.echothree.util.server.persistence.Session; 042import java.awt.RenderingHints; 043import java.awt.Transparency; 044import java.awt.image.BufferedImage; 045import java.io.ByteArrayOutputStream; 046import java.io.IOException; 047import javax.enterprise.context.ApplicationScoped; 048import javax.enterprise.inject.spi.CDI; 049import javax.imageio.IIOImage; 050import javax.imageio.ImageIO; 051import javax.imageio.ImageReader; 052import javax.imageio.ImageWriteParam; 053import javax.imageio.stream.ImageOutputStream; 054import javax.imageio.stream.MemoryCacheImageInputStream; 055import javax.imageio.stream.MemoryCacheImageOutputStream; 056 057@ApplicationScoped 058public class ItemDescriptionLogic 059 extends BaseLogic { 060 061 protected ItemDescriptionLogic() { 062 super(); 063 } 064 065 public static ItemDescriptionLogic getInstance() { 066 return CDI.current().select(ItemDescriptionLogic.class).get(); 067 } 068 069 public String getIndexDefaultItemDescriptionTypeName() { 070 var itemControl = Session.getModelController(ItemControl.class); 071 var itemDescriptionType = itemControl.getIndexDefaultItemDescriptionType(); 072 073 return itemDescriptionType.getLastDetail().getItemDescriptionTypeName(); 074 } 075 076 public boolean isImage(ItemDescriptionType itemDescriptionType) { 077 var mimeTypeUsageType = itemDescriptionType.getLastDetail().getMimeTypeUsageType(); 078 var result = false; 079 080 if(mimeTypeUsageType != null) { 081 result = mimeTypeUsageType.getMimeTypeUsageTypeName().equals(MimeTypeUsageTypes.IMAGE.name()); 082 } 083 084 return result; 085 } 086 087 // Find the first available parent ItemDescription. 088 public ItemDescription getBestParent(ItemControl itemControl, ItemDescriptionType itemDescriptionType, Item item, Language language) { 089 ItemDescription itemDescription = null; 090 var itemDescriptionTypeDetail = itemDescriptionType.getLastDetail(); 091 092 if(itemDescriptionTypeDetail.getUseParentIfMissing()) { 093 var parentItemDescriptionType = itemDescriptionTypeDetail.getParentItemDescriptionType(); 094 095 if(parentItemDescriptionType != null) { 096 var parentItemDescription = itemControl.getItemDescription(parentItemDescriptionType, item, language); 097 098 if(parentItemDescription == null) { 099 // If there isn't a parent, or if the parent is scaled, then try the parent's parent 100 itemDescription = getBestParent(itemControl, parentItemDescriptionType, item, language); 101 } else { 102 // If the parent image wasn't scaled, then we'll use that one. 103 itemDescription = parentItemDescription; 104 } 105 } 106 } 107 108 return itemDescription; 109 } 110 111 // Find the first available parent ItemDescription based on the Party's preferred Language. 112 public ItemDescription getBestParent(final ItemDescriptionType itemDescriptionType, final Item item, final Party party) { 113 var itemControl = Session.getModelController(ItemControl.class); 114 var partyControl = Session.getModelController(PartyControl.class); 115 var language = party == null ? partyControl.getDefaultLanguage() : partyControl.getPreferredLanguage(party); 116 117 return getBestParent(itemControl, itemDescriptionType, item, language); 118 } 119 120 // Find the first available parent ItemDescription based on the Party's preferred Language. 121 public ItemDescription getBestParentUsingNames(final ExecutionErrorAccumulator eea, final String itemDescriptionTypeName, final Item item, 122 final Party party) { 123 var itemControl = Session.getModelController(ItemControl.class); 124 var itemDescriptionType = itemControl.getItemDescriptionTypeByName(itemDescriptionTypeName); 125 126 if(itemDescriptionType == null) { 127 handleExecutionError(UnknownItemDescriptionTypeNameException.class, eea, ExecutionErrors.UnknownItemDescriptionTypeName.name(), itemDescriptionTypeName); 128 } 129 130 return (eea == null ? false : eea.hasExecutionErrors()) ? null : getBestParent(itemDescriptionType, item, party); 131 } 132 133 // Find the first available parent ItemDescription based on the Party's preferred Language. 134 public String getBestStringUsingNames(final ExecutionErrorAccumulator eea, final String itemDescriptionTypeName, final Item item, final Party party) { 135 var itemDescription = getBestParentUsingNames(eea, itemDescriptionTypeName, item, party); 136 String stringDescription = null; 137 138 if(itemDescription != null) { 139 var mimeType = itemDescription.getLastDetail().getMimeType(); 140 141 if(mimeType == null) { 142 var itemControl = Session.getModelController(ItemControl.class); 143 var itemStringDescription = itemControl.getItemStringDescription(itemDescription); 144 145 stringDescription = itemStringDescription.getStringDescription(); 146 } else { 147 handleExecutionError(InvalidItemDescriptionTypeException.class, eea, ExecutionErrors.InvalidItemDescriptionType.name(), itemDescriptionTypeName); 148 } 149 } 150 151 return stringDescription; 152 } 153 154 // Find the highest quality parent ItemDescription. 155 public ItemDescription getBestParentImage(ItemControl itemControl, ItemDescriptionType itemDescriptionType, Item item, Language language) { 156 ItemDescription itemDescription = null; 157 var parentItemDescriptionType = itemDescriptionType.getLastDetail().getParentItemDescriptionType(); 158 159 if(parentItemDescriptionType != null) { 160 var parentItemDescription = itemControl.getItemDescription(parentItemDescriptionType, item, language); 161 162 if(parentItemDescription == null || itemControl.getItemImageDescription(parentItemDescription).getScaledFromParent()) { 163 // If there isn't a parent, or if the parent is scaled, then try the parent's parent 164 itemDescription = getBestParentImage(itemControl, parentItemDescriptionType, item, language); 165 } else { 166 // If the parent image wasn't scaled, then we'll use that one. 167 itemDescription = parentItemDescription; 168 } 169 } 170 171 172 return itemDescription; 173 } 174 175 public ImageReader getImageReader(MimeType mimeType, ItemBlobDescription itemBlobDescription) { 176 var memoryCacheImageInputStream = new MemoryCacheImageInputStream(itemBlobDescription.getBlobDescription().getByteArrayInputStream()); 177 var imageReaders = ImageIO.getImageReadersByMIMEType(mimeType.getLastDetail().getMimeTypeName()); 178 var imageReader = imageReaders.hasNext() ? imageReaders.next() : null; 179 180 if(imageReader != null) { 181 imageReader.setInput(memoryCacheImageInputStream); 182 183 try { 184 // If there isn't at least one image, then return null. 185 if(imageReader.getNumImages(true) == 0) { 186 imageReader = null; 187 } 188 } catch (IOException ioe) { 189 // Nothing, height and width stay null. 190 } 191 } 192 193 return imageReader; 194 } 195 196 // Based on: http://today.java.net/pub/a/today/2007/04/03/perils-of-image-getscaledinstance.html 197 /** 198 * Convenience method that returns a scaled instance of the 199 * provided {@code BufferedImage}. 200 * 201 * @param img the original image to be scaled 202 * @param targetWidth the desired width of the scaled instance, 203 * in pixels 204 * @param targetHeight the desired height of the scaled instance, 205 * in pixels 206 * @param hint one of the rendering hints that corresponds to 207 * {@code RenderingHints.KEY_INTERPOLATION} (e.g. 208 * {@code RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR}, 209 * {@code RenderingHints.VALUE_INTERPOLATION_BILINEAR}, 210 * {@code RenderingHints.VALUE_INTERPOLATION_BICUBIC}) 211 * @param higherQuality if true, this method will use a multi-step 212 * scaling technique that provides higher quality than the usual 213 * one-step technique (only useful in downscaling cases, where 214 * {@code targetWidth} or {@code targetHeight} is 215 * smaller than the original dimensions, and generally only when 216 * the {@code BILINEAR} hint is specified) 217 * @return a scaled version of the original {@code BufferedImage} 218 */ 219 public BufferedImage getScaledInstance(BufferedImage img, int targetWidth, int targetHeight, Object hint, boolean higherQuality) { 220 var type = (img.getTransparency() == Transparency.OPAQUE) ? BufferedImage.TYPE_INT_RGB : BufferedImage.TYPE_INT_ARGB; 221 var ret = (BufferedImage)img; 222 int w, h; 223 224 if(higherQuality) { 225 // Use multi-step technique: start with original size, then 226 // scale down in multiple passes with drawImage() 227 // until the target size is reached 228 w = img.getWidth(); 229 h = img.getHeight(); 230 } else { 231 // Use one-step technique: scale directly from original 232 // size to target size with a single drawImage() call 233 w = targetWidth; 234 h = targetHeight; 235 } 236 237 do { 238 if(higherQuality && w > targetWidth) { 239 w /= 2; 240 if(w < targetWidth) { 241 w = targetWidth; 242 } 243 } 244 245 if(higherQuality && h > targetHeight) { 246 h /= 2; 247 if(h < targetHeight) { 248 h = targetHeight; 249 } 250 } 251 252 var tmp = new BufferedImage(w, h, type); 253 var g2 = tmp.createGraphics(); 254 g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, hint); 255 g2.drawImage(ret, 0, 0, w, h, null); 256 g2.dispose(); 257 258 ret = tmp; 259 } while(w != targetWidth || h != targetHeight); 260 261 return ret; 262 } 263 264 // http://www.nearinfinity.com/blogs/jim_clark/thumbnail_generation_gotchas.html 265 public double scaleToFit(double w1, double h1, double w2, double h2) { 266 var scale = 1.0D; 267 268 if(w1 > h1) { 269 if(w1 > w2) 270 scale = w2 / w1; 271 h1 *= scale; 272 if(h1 > h2) 273 scale *= h2 / h1; 274 } else { 275 if(h1 > h2) 276 scale = h2 / h1; 277 w1 *= scale; 278 if(w1 > w2) 279 scale *= w2 / w1; 280 } 281 282 return scale; 283 } 284 285 public ItemDescription searchForItemDescription(ItemDescriptionType itemDescriptionType, Item item, Language language, BasePK createdBy) { 286 var itemControl = Session.getModelController(ItemControl.class); 287 ItemDescription itemDescription = null; 288 289 if(isImage(itemDescriptionType)) { 290 var itemImageDescriptionType = itemControl.getItemImageDescriptionType(itemDescriptionType); 291 292 if(itemImageDescriptionType.getScaleFromParent()) { 293 var preferredHeight = itemImageDescriptionType.getPreferredHeight(); 294 var preferredWidth = itemImageDescriptionType.getPreferredWidth(); 295 296 // preferredHeight and preferredWidth are used as the target sizes for the scaling. Without them, it isn't going to happen. 297 if(preferredHeight != null && preferredWidth != null) { 298 var originalItemDescription = getBestParentImage(itemControl, itemDescriptionType, item, language); 299 300 if(originalItemDescription != null) { 301 var originalItemDescriptionDetail = originalItemDescription.getLastDetail(); 302 var originalItemImageDescription = itemControl.getItemImageDescription(originalItemDescription); 303 304 // BLOBs only. 305 if(originalItemDescriptionDetail.getMimeType().getLastDetail().getEntityAttributeType().getEntityAttributeTypeName().equals(EntityAttributeTypes.BLOB.name())) { 306 var originalMimeType = originalItemDescriptionDetail.getMimeType(); 307 var preferredMimeType = itemImageDescriptionType.getPreferredMimeType(); 308 var quality = itemImageDescriptionType.getQuality(); 309 var originalItemImageType = originalItemImageDescription.getItemImageType(); 310 var originalItemImageTypeDetail = originalItemImageType.getLastDetail(); 311 var originalItemBlobDescription = itemControl.getItemBlobDescription(originalItemDescription); 312 313 // ItemImageType settings override any that came from the ItemImageDescriptionType 314 if(originalItemImageTypeDetail.getPreferredMimeType() != null) { 315 preferredMimeType = originalItemImageTypeDetail.getPreferredMimeType(); 316 } 317 318 if(originalItemImageTypeDetail.getQuality() != null) { 319 quality = originalItemImageTypeDetail.getQuality(); 320 } 321 322 // If there's still no preferredMimeType, fall back to the original image's MimeType. 323 if(preferredMimeType == null) { 324 preferredMimeType = originalMimeType; 325 } 326 327 if(quality == null) { 328 quality = 90; // Default quality 329 } 330 331 var imageReader = getImageReader(originalMimeType, originalItemBlobDescription); 332 if(imageReader != null) { 333 BufferedImage originalBufferedImage = null; 334 335 try { 336 originalBufferedImage = imageReader.read(0); 337 } catch (IOException ioe) { 338 // Ignore, image reading failed, leave originalBufferedImage null. 339 } 340 341 if(originalBufferedImage != null) { 342 var originalHeight = originalBufferedImage.getHeight(); 343 var originalWidth = originalBufferedImage.getWidth(); 344 var scale = scaleToFit(originalWidth, originalHeight, preferredWidth, preferredHeight); 345 var mimeTypeName = preferredMimeType.getLastDetail().getMimeTypeName(); 346 347 var scaledBufferedImage = getScaledInstance(originalBufferedImage, (int)Math.round(originalWidth * scale), 348 (int)Math.round(originalHeight * scale), RenderingHints.VALUE_INTERPOLATION_BILINEAR, 349 true); 350 351 var imageWriters = ImageIO.getImageWritersByMIMEType(mimeTypeName); 352 var imageWriter = imageWriters.hasNext() ? imageWriters.next() : null; 353 354 if(imageWriter != null) { 355 var byteArrayOutputStream = new ByteArrayOutputStream(); 356 357 try { 358 var scaledQuality = (float)quality / 100.0f; 359 var iwp = imageWriter.getDefaultWriteParam(); 360 361 if(iwp.canWriteCompressed()) { 362 var compressionTypes = iwp.getCompressionTypes(); 363 364 iwp.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); 365 366 // JPEG doesn't appear to "need" this, but GIF throws an IllegalStateException without it. Pick the first choice. 367 if(compressionTypes.length > 0) { 368 iwp.setCompressionType(compressionTypes[0]); 369 } 370 371 iwp.setCompressionQuality(scaledQuality); 372 } 373 374 ImageOutputStream imageOutputStream = new MemoryCacheImageOutputStream(byteArrayOutputStream); 375 var img = new IIOImage(scaledBufferedImage, null, null); 376 377 imageWriter.setOutput(imageOutputStream); 378 imageWriter.write(null, img, iwp); 379 imageWriter.dispose(); 380 imageOutputStream.close(); 381 } catch (IOException ioe) { 382 // Exception? Nuke the result, no scaling. 383 byteArrayOutputStream = null; 384 } 385 386 if(byteArrayOutputStream != null) { 387 try { 388 itemDescription = itemControl.createItemDescription(itemDescriptionType, item, language, preferredMimeType, createdBy); 389 itemControl.createItemImageDescription(itemDescription, originalItemImageType, scaledBufferedImage.getHeight(), 390 scaledBufferedImage.getWidth(), true, createdBy); 391 itemControl.createItemBlobDescription(itemDescription, new ByteArray(byteArrayOutputStream.toByteArray()), createdBy); 392 } catch(PersistenceDatabaseException pde) { 393 if(PersistenceUtils.getInstance().isIntegrityConstraintViolation(pde)) { 394 itemDescription = itemControl.getItemDescription(itemDescriptionType, item, language); 395 396 if(itemDescription != null) { 397 pde = null; 398 } 399 } 400 401 if(pde != null) { 402 throw pde; 403 } 404 } 405 } 406 } 407 } 408 } 409 } 410 } 411 } 412 } else { 413 itemDescription = getBestParent(itemControl, itemDescriptionType, item, language); 414 } 415 } else { 416 itemDescription = getBestParent(itemControl, itemDescriptionType, item, language); 417 } 418 419 return itemDescription; 420 } 421 422 public static class ImageDimensions { 423 424 private Integer height; 425 private Integer width; 426 427 public ImageDimensions(Integer height, Integer width) { 428 this.height = height; 429 this.width = width; 430 } 431 432 public Integer getHeight() { 433 return height; 434 } 435 436 public void setHeight(Integer height) { 437 this.height = height; 438 } 439 440 public Integer getWidth() { 441 return width; 442 } 443 444 public void setWidth(Integer width) { 445 this.width = width; 446 } 447 448 } 449 450 public ImageDimensions getImageDimensions(String mimeTypeName, ByteArray blobDescription) { 451 var memoryCacheImageInputStream = blobDescription.getMemoryCacheImageInputStream(); 452 var imageReaders = ImageIO.getImageReadersByMIMEType(mimeTypeName); 453 var imageReader = imageReaders.hasNext() ? imageReaders.next() : null; 454 ImageDimensions result = null; 455 456 if(imageReader != null) { 457 imageReader.setInput(memoryCacheImageInputStream); 458 459 try { 460 if(imageReader.getNumImages(true) > 0) { 461 result = new ImageDimensions(imageReader.getHeight(0), imageReader.getWidth(0)); 462 } 463 } catch (IOException ioe) { 464 // Nothing, result stays null. 465 } 466 } 467 468 return result; 469 } 470 471 public void deleteItemImageDescriptionChildren(ItemDescriptionType itemDescriptionType, Item item, Language language, BasePK deletedBy) { 472 var itemControl = Session.getModelController(ItemControl.class); 473 var childItemDescriptionTypes = itemControl.getItemDescriptionTypesByParentItemDescriptionType(itemDescriptionType); 474 475 childItemDescriptionTypes.forEach((childItemDescriptionType) -> { 476 var childItemDescription = itemControl.getItemDescriptionForUpdate(childItemDescriptionType, item, language); 477 var childWasScaled = true; 478 if(childItemDescription != null) { 479 var childItemImageDescription = itemControl.getItemImageDescription(childItemDescription); 480 481 childWasScaled = childItemImageDescription.getScaledFromParent(); 482 483 if(childWasScaled) { 484 itemControl.deleteItemDescription(childItemDescription, deletedBy); 485 } 486 } 487 488 // If no child description existed, or if it was scaled, then go through and make sure there are no scaled children under it. 489 if (childWasScaled) { 490 deleteItemImageDescriptionChildren(childItemDescriptionType, item, language, deletedBy); 491 } 492 }); 493 } 494 495 public void deleteItemImageDescriptionChildren(ItemDescription itemDescription, BasePK deletedBy) { 496 var itemDescriptionDetail = itemDescription.getLastDetail(); 497 498 deleteItemImageDescriptionChildren(itemDescriptionDetail.getItemDescriptionType(), itemDescriptionDetail.getItem(), itemDescriptionDetail.getLanguage(), deletedBy); 499 } 500 501 public void deleteItemDescription(ItemDescription itemDescription, BasePK deletedBy) { 502 var itemControl = Session.getModelController(ItemControl.class); 503 var mimeTypeUsageType = itemDescription.getLastDetail().getItemDescriptionType().getLastDetail().getMimeTypeUsageType(); 504 505 itemControl.deleteItemDescription(itemDescription, deletedBy); 506 507 if(mimeTypeUsageType != null && mimeTypeUsageType.getMimeTypeUsageTypeName().equals(MimeTypeUsageTypes.IMAGE.name())) { 508 var itemImageDescription = itemControl.getItemImageDescription(itemDescription); 509 510 if(!itemImageDescription.getScaledFromParent()) { 511 ItemDescriptionLogic.getInstance().deleteItemImageDescriptionChildren(itemDescription, deletedBy); 512 } 513 } 514 } 515 516 public void updateItemImageDescriptionTypeFromValue(ItemImageDescriptionTypeValue itemImageDescriptionTypeValue, BasePK updatedBy) { 517 var itemControl = Session.getModelController(ItemControl.class); 518 519 itemControl.updateItemImageDescriptionTypeFromValue(itemImageDescriptionTypeValue, updatedBy); 520 521 if(itemImageDescriptionTypeValue.getPreferredHeightHasBeenModified() || itemImageDescriptionTypeValue.getPreferredWidthHasBeenModified() 522 || itemImageDescriptionTypeValue.getPreferredMimeTypePKHasBeenModified() || itemImageDescriptionTypeValue.getQualityHasBeenModified() 523 || itemImageDescriptionTypeValue.getScaleFromParent()) { 524 itemControl.deleteItemDescriptions(itemControl.getScaledItemDescriptionsByItemDescriptionTypePKForUpdate(itemImageDescriptionTypeValue.getItemDescriptionTypePK()), updatedBy); 525 } 526 } 527 528 public void updateItemImageTypeFromValue(ItemImageTypeDetailValue itemImageTypeDetailValue, BasePK updatedBy) { 529 var itemControl = Session.getModelController(ItemControl.class); 530 531 itemControl.updateItemImageTypeFromValue(itemImageTypeDetailValue, updatedBy); 532 533 if(itemImageTypeDetailValue.getPreferredMimeTypePKHasBeenModified() || itemImageTypeDetailValue.getQualityHasBeenModified()) { 534 itemControl.deleteItemDescriptions(itemControl.getScaledItemDescriptionsByItemImageTypePKForUpdate(itemImageTypeDetailValue.getItemImageTypePK()), updatedBy); 535 } 536 } 537 538}