001// -------------------------------------------------------------------------------- 002// Copyright 2002-2026 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 java.awt.RenderingHints; 042import java.awt.Transparency; 043import java.awt.image.BufferedImage; 044import java.io.ByteArrayOutputStream; 045import java.io.IOException; 046import javax.enterprise.context.ApplicationScoped; 047import javax.enterprise.inject.spi.CDI; 048import javax.imageio.IIOImage; 049import javax.imageio.ImageIO; 050import javax.imageio.ImageReader; 051import javax.imageio.ImageWriteParam; 052import javax.imageio.stream.ImageOutputStream; 053import javax.imageio.stream.MemoryCacheImageInputStream; 054import javax.imageio.stream.MemoryCacheImageOutputStream; 055import javax.inject.Inject; 056 057@ApplicationScoped 058public class ItemDescriptionLogic 059 extends BaseLogic { 060 061 @Inject 062 ItemControl itemControl; 063 064 @Inject 065 PartyControl partyControl; 066 067 protected ItemDescriptionLogic() { 068 super(); 069 } 070 071 public static ItemDescriptionLogic getInstance() { 072 return CDI.current().select(ItemDescriptionLogic.class).get(); 073 } 074 075 public String getIndexDefaultItemDescriptionTypeName() { 076 var itemDescriptionType = itemControl.getIndexDefaultItemDescriptionType(); 077 078 return itemDescriptionType.getLastDetail().getItemDescriptionTypeName(); 079 } 080 081 public boolean isImage(ItemDescriptionType itemDescriptionType) { 082 var mimeTypeUsageType = itemDescriptionType.getLastDetail().getMimeTypeUsageType(); 083 var result = false; 084 085 if(mimeTypeUsageType != null) { 086 result = mimeTypeUsageType.getMimeTypeUsageTypeName().equals(MimeTypeUsageTypes.IMAGE.name()); 087 } 088 089 return result; 090 } 091 092 // Find the first available parent ItemDescription. 093 public ItemDescription getBestParent(ItemControl itemControl, ItemDescriptionType itemDescriptionType, Item item, Language language) { 094 ItemDescription itemDescription = null; 095 var itemDescriptionTypeDetail = itemDescriptionType.getLastDetail(); 096 097 if(itemDescriptionTypeDetail.getUseParentIfMissing()) { 098 var parentItemDescriptionType = itemDescriptionTypeDetail.getParentItemDescriptionType(); 099 100 if(parentItemDescriptionType != null) { 101 var parentItemDescription = itemControl.getItemDescription(parentItemDescriptionType, item, language); 102 103 if(parentItemDescription == null) { 104 // If there isn't a parent, or if the parent is scaled, then try the parent's parent 105 itemDescription = getBestParent(itemControl, parentItemDescriptionType, item, language); 106 } else { 107 // If the parent image wasn't scaled, then we'll use that one. 108 itemDescription = parentItemDescription; 109 } 110 } 111 } 112 113 return itemDescription; 114 } 115 116 // Find the first available parent ItemDescription based on the Party's preferred Language. 117 public ItemDescription getBestParent(final ItemDescriptionType itemDescriptionType, final Item item, final Party party) { 118 var language = party == null ? partyControl.getDefaultLanguage() : partyControl.getPreferredLanguage(party); 119 120 return getBestParent(itemControl, itemDescriptionType, item, language); 121 } 122 123 // Find the first available parent ItemDescription based on the Party's preferred Language. 124 public ItemDescription getBestParentUsingNames(final ExecutionErrorAccumulator eea, final String itemDescriptionTypeName, final Item item, 125 final Party party) { 126 var itemDescriptionType = itemControl.getItemDescriptionTypeByName(itemDescriptionTypeName); 127 128 if(itemDescriptionType == null) { 129 handleExecutionError(UnknownItemDescriptionTypeNameException.class, eea, ExecutionErrors.UnknownItemDescriptionTypeName.name(), itemDescriptionTypeName); 130 } 131 132 return (eea == null ? false : eea.hasExecutionErrors()) ? null : getBestParent(itemDescriptionType, item, party); 133 } 134 135 // Find the first available parent ItemDescription based on the Party's preferred Language. 136 public String getBestStringUsingNames(final ExecutionErrorAccumulator eea, final String itemDescriptionTypeName, final Item item, final Party party) { 137 var itemDescription = getBestParentUsingNames(eea, itemDescriptionTypeName, item, party); 138 String stringDescription = null; 139 140 if(itemDescription != null) { 141 var mimeType = itemDescription.getLastDetail().getMimeType(); 142 143 if(mimeType == null) { 144 var itemStringDescription = itemControl.getItemStringDescription(itemDescription); 145 146 stringDescription = itemStringDescription.getStringDescription(); 147 } else { 148 handleExecutionError(InvalidItemDescriptionTypeException.class, eea, ExecutionErrors.InvalidItemDescriptionType.name(), itemDescriptionTypeName); 149 } 150 } 151 152 return stringDescription; 153 } 154 155 // Find the highest quality parent ItemDescription. 156 public ItemDescription getBestParentImage(ItemControl itemControl, ItemDescriptionType itemDescriptionType, Item item, Language language) { 157 ItemDescription itemDescription = null; 158 var parentItemDescriptionType = itemDescriptionType.getLastDetail().getParentItemDescriptionType(); 159 160 if(parentItemDescriptionType != null) { 161 var parentItemDescription = itemControl.getItemDescription(parentItemDescriptionType, item, language); 162 163 if(parentItemDescription == null || itemControl.getItemImageDescription(parentItemDescription).getScaledFromParent()) { 164 // If there isn't a parent, or if the parent is scaled, then try the parent's parent 165 itemDescription = getBestParentImage(itemControl, parentItemDescriptionType, item, language); 166 } else { 167 // If the parent image wasn't scaled, then we'll use that one. 168 itemDescription = parentItemDescription; 169 } 170 } 171 172 173 return itemDescription; 174 } 175 176 public ImageReader getImageReader(MimeType mimeType, ItemBlobDescription itemBlobDescription) { 177 var memoryCacheImageInputStream = new MemoryCacheImageInputStream(itemBlobDescription.getBlobDescription().getByteArrayInputStream()); 178 var imageReaders = ImageIO.getImageReadersByMIMEType(mimeType.getLastDetail().getMimeTypeName()); 179 var imageReader = imageReaders.hasNext() ? imageReaders.next() : null; 180 181 if(imageReader != null) { 182 imageReader.setInput(memoryCacheImageInputStream); 183 184 try { 185 // If there isn't at least one image, then return null. 186 if(imageReader.getNumImages(true) == 0) { 187 imageReader = null; 188 } 189 } catch (IOException ioe) { 190 // Nothing, height and width stay null. 191 } 192 } 193 194 return imageReader; 195 } 196 197 // Based on: http://today.java.net/pub/a/today/2007/04/03/perils-of-image-getscaledinstance.html 198 /** 199 * Convenience method that returns a scaled instance of the 200 * provided {@code BufferedImage}. 201 * 202 * @param img the original image to be scaled 203 * @param targetWidth the desired width of the scaled instance, 204 * in pixels 205 * @param targetHeight the desired height of the scaled instance, 206 * in pixels 207 * @param hint one of the rendering hints that corresponds to 208 * {@code RenderingHints.KEY_INTERPOLATION} (e.g. 209 * {@code RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR}, 210 * {@code RenderingHints.VALUE_INTERPOLATION_BILINEAR}, 211 * {@code RenderingHints.VALUE_INTERPOLATION_BICUBIC}) 212 * @param higherQuality if true, this method will use a multi-step 213 * scaling technique that provides higher quality than the usual 214 * one-step technique (only useful in downscaling cases, where 215 * {@code targetWidth} or {@code targetHeight} is 216 * smaller than the original dimensions, and generally only when 217 * the {@code BILINEAR} hint is specified) 218 * @return a scaled version of the original {@code BufferedImage} 219 */ 220 public BufferedImage getScaledInstance(BufferedImage img, int targetWidth, int targetHeight, Object hint, boolean higherQuality) { 221 var type = (img.getTransparency() == Transparency.OPAQUE) ? BufferedImage.TYPE_INT_RGB : BufferedImage.TYPE_INT_ARGB; 222 var ret = (BufferedImage)img; 223 int w, h; 224 225 if(higherQuality) { 226 // Use multi-step technique: start with original size, then 227 // scale down in multiple passes with drawImage() 228 // until the target size is reached 229 w = img.getWidth(); 230 h = img.getHeight(); 231 } else { 232 // Use one-step technique: scale directly from original 233 // size to target size with a single drawImage() call 234 w = targetWidth; 235 h = targetHeight; 236 } 237 238 do { 239 if(higherQuality && w > targetWidth) { 240 w /= 2; 241 if(w < targetWidth) { 242 w = targetWidth; 243 } 244 } 245 246 if(higherQuality && h > targetHeight) { 247 h /= 2; 248 if(h < targetHeight) { 249 h = targetHeight; 250 } 251 } 252 253 var tmp = new BufferedImage(w, h, type); 254 var g2 = tmp.createGraphics(); 255 g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, hint); 256 g2.drawImage(ret, 0, 0, w, h, null); 257 g2.dispose(); 258 259 ret = tmp; 260 } while(w != targetWidth || h != targetHeight); 261 262 return ret; 263 } 264 265 // http://www.nearinfinity.com/blogs/jim_clark/thumbnail_generation_gotchas.html 266 public double scaleToFit(double w1, double h1, double w2, double h2) { 267 var scale = 1.0D; 268 269 if(w1 > h1) { 270 if(w1 > w2) 271 scale = w2 / w1; 272 h1 *= scale; 273 if(h1 > h2) 274 scale *= h2 / h1; 275 } else { 276 if(h1 > h2) 277 scale = h2 / h1; 278 w1 *= scale; 279 if(w1 > w2) 280 scale *= w2 / w1; 281 } 282 283 return scale; 284 } 285 286 public ItemDescription searchForItemDescription(ItemDescriptionType itemDescriptionType, Item item, Language language, BasePK createdBy) { 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 childItemDescriptionTypes = itemControl.getItemDescriptionTypesByParentItemDescriptionType(itemDescriptionType); 473 474 childItemDescriptionTypes.forEach((childItemDescriptionType) -> { 475 var childItemDescription = itemControl.getItemDescriptionForUpdate(childItemDescriptionType, item, language); 476 var childWasScaled = true; 477 if(childItemDescription != null) { 478 var childItemImageDescription = itemControl.getItemImageDescription(childItemDescription); 479 480 childWasScaled = childItemImageDescription.getScaledFromParent(); 481 482 if(childWasScaled) { 483 itemControl.deleteItemDescription(childItemDescription, deletedBy); 484 } 485 } 486 487 // If no child description existed, or if it was scaled, then go through and make sure there are no scaled children under it. 488 if (childWasScaled) { 489 deleteItemImageDescriptionChildren(childItemDescriptionType, item, language, deletedBy); 490 } 491 }); 492 } 493 494 public void deleteItemImageDescriptionChildren(ItemDescription itemDescription, BasePK deletedBy) { 495 var itemDescriptionDetail = itemDescription.getLastDetail(); 496 497 deleteItemImageDescriptionChildren(itemDescriptionDetail.getItemDescriptionType(), itemDescriptionDetail.getItem(), itemDescriptionDetail.getLanguage(), deletedBy); 498 } 499 500 public void deleteItemDescription(ItemDescription itemDescription, BasePK deletedBy) { 501 var mimeTypeUsageType = itemDescription.getLastDetail().getItemDescriptionType().getLastDetail().getMimeTypeUsageType(); 502 503 itemControl.deleteItemDescription(itemDescription, deletedBy); 504 505 if(mimeTypeUsageType != null && mimeTypeUsageType.getMimeTypeUsageTypeName().equals(MimeTypeUsageTypes.IMAGE.name())) { 506 var itemImageDescription = itemControl.getItemImageDescription(itemDescription); 507 508 if(!itemImageDescription.getScaledFromParent()) { 509 this.deleteItemImageDescriptionChildren(itemDescription, deletedBy); 510 } 511 } 512 } 513 514 public void updateItemImageDescriptionTypeFromValue(ItemImageDescriptionTypeValue itemImageDescriptionTypeValue, BasePK updatedBy) { 515 itemControl.updateItemImageDescriptionTypeFromValue(itemImageDescriptionTypeValue, updatedBy); 516 517 if(itemImageDescriptionTypeValue.getPreferredHeightHasBeenModified() || itemImageDescriptionTypeValue.getPreferredWidthHasBeenModified() 518 || itemImageDescriptionTypeValue.getPreferredMimeTypePKHasBeenModified() || itemImageDescriptionTypeValue.getQualityHasBeenModified() 519 || itemImageDescriptionTypeValue.getScaleFromParent()) { 520 itemControl.deleteItemDescriptions(itemControl.getScaledItemDescriptionsByItemDescriptionTypePKForUpdate(itemImageDescriptionTypeValue.getItemDescriptionTypePK()), updatedBy); 521 } 522 } 523 524 public void updateItemImageTypeFromValue(ItemImageTypeDetailValue itemImageTypeDetailValue, BasePK updatedBy) { 525 itemControl.updateItemImageTypeFromValue(itemImageTypeDetailValue, updatedBy); 526 527 if(itemImageTypeDetailValue.getPreferredMimeTypePKHasBeenModified() || itemImageTypeDetailValue.getQualityHasBeenModified()) { 528 itemControl.deleteItemDescriptions(itemControl.getScaledItemDescriptionsByItemImageTypePKForUpdate(itemImageTypeDetailValue.getItemImageTypePK()), updatedBy); 529 } 530 } 531 532}