001/* 002 * (C) Copyright 2007-2018 Nuxeo (http://nuxeo.com/) and others. 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 * Contributors: 017 * Max Stepanov 018 * Florent Guillaume 019 */ 020package org.nuxeo.ecm.platform.picture; 021 022import static org.nuxeo.ecm.platform.picture.api.ImagingConvertConstants.CONVERSION_FORMAT; 023import static org.nuxeo.ecm.platform.picture.api.ImagingConvertConstants.JPEG_CONVERSATION_FORMAT; 024import static org.nuxeo.ecm.platform.picture.api.ImagingConvertConstants.OPTION_RESIZE_DEPTH; 025import static org.nuxeo.ecm.platform.picture.api.ImagingConvertConstants.OPTION_RESIZE_HEIGHT; 026import static org.nuxeo.ecm.platform.picture.api.ImagingConvertConstants.OPTION_RESIZE_WIDTH; 027 028import java.awt.Point; 029import java.io.File; 030import java.io.IOException; 031import java.io.Serializable; 032import java.util.ArrayList; 033import java.util.Collections; 034import java.util.HashMap; 035import java.util.List; 036import java.util.Map; 037 038import org.apache.commons.io.FilenameUtils; 039import org.apache.commons.lang3.StringUtils; 040import org.apache.logging.log4j.LogManager; 041import org.apache.logging.log4j.Logger; 042import org.nuxeo.ecm.automation.AutomationService; 043import org.nuxeo.ecm.automation.OperationContext; 044import org.nuxeo.ecm.automation.OperationException; 045import org.nuxeo.ecm.automation.core.util.Properties; 046import org.nuxeo.ecm.core.api.Blob; 047import org.nuxeo.ecm.core.api.Blobs; 048import org.nuxeo.ecm.core.api.CloseableFile; 049import org.nuxeo.ecm.core.api.CoreSession; 050import org.nuxeo.ecm.core.api.DocumentModel; 051import org.nuxeo.ecm.core.api.NuxeoException; 052import org.nuxeo.ecm.core.api.impl.blob.BlobWrapper; 053import org.nuxeo.ecm.platform.actions.ActionContext; 054import org.nuxeo.ecm.platform.actions.ELActionContext; 055import org.nuxeo.ecm.platform.actions.ejb.ActionManager; 056import org.nuxeo.ecm.platform.commandline.executor.api.CommandException; 057import org.nuxeo.ecm.platform.commandline.executor.api.CommandNotAvailable; 058import org.nuxeo.ecm.platform.mimetype.MimetypeDetectionException; 059import org.nuxeo.ecm.platform.mimetype.MimetypeNotFoundException; 060import org.nuxeo.ecm.platform.mimetype.interfaces.MimetypeRegistry; 061import org.nuxeo.ecm.platform.picture.api.ImageInfo; 062import org.nuxeo.ecm.platform.picture.api.ImagingConfigurationDescriptor; 063import org.nuxeo.ecm.platform.picture.api.ImagingService; 064import org.nuxeo.ecm.platform.picture.api.PictureConversion; 065import org.nuxeo.ecm.platform.picture.api.PictureView; 066import org.nuxeo.ecm.platform.picture.api.PictureViewImpl; 067import org.nuxeo.ecm.platform.picture.core.libraryselector.LibrarySelector; 068import org.nuxeo.ecm.platform.picture.magick.utils.ImageIdentifier; 069import org.nuxeo.ecm.platform.picture.magick.utils.ImageResizer; 070import org.nuxeo.runtime.api.Framework; 071import org.nuxeo.runtime.model.ComponentContext; 072import org.nuxeo.runtime.model.ComponentInstance; 073import org.nuxeo.runtime.model.DefaultComponent; 074import org.nuxeo.runtime.transaction.TransactionHelper; 075 076public class ImagingComponent extends DefaultComponent implements ImagingService { 077 078 private static final Logger log = LogManager.getLogger(ImagingComponent.class); 079 080 public static final String CONFIGURATION_PARAMETERS_EP = "configuration"; 081 082 public static final String PICTURE_CONVERSIONS_EP = "pictureConversions"; 083 084 protected Map<String, String> configurationParameters = new HashMap<>(); 085 086 protected PictureConversionRegistry pictureConversionRegistry = new PictureConversionRegistry(); 087 088 protected final PictureMigrationHandler pictureMigrationHandler = new PictureMigrationHandler(); 089 090 @Override 091 public List<PictureConversion> getPictureConversions() { 092 return pictureConversionRegistry.getPictureConversions(); 093 } 094 095 @Override 096 public PictureConversion getPictureConversion(String id) { 097 return pictureConversionRegistry.getPictureConversion(id); 098 } 099 100 @Override 101 public Blob crop(Blob blob, int x, int y, int width, int height) { 102 return getLibrarySelectorService().getImageUtils().crop(blob, x, y, width, height); 103 } 104 105 @Override 106 public Blob resize(Blob blob, String finalFormat, int width, int height, int depth) { 107 return getLibrarySelectorService().getImageUtils().resize(blob, finalFormat, width, height, depth); 108 } 109 110 @Override 111 public Blob rotate(Blob blob, int angle) { 112 return getLibrarySelectorService().getImageUtils().rotate(blob, angle); 113 } 114 115 @Override 116 public Blob convertToPDF(Blob blob) { 117 return getLibrarySelectorService().getImageUtils().convertToPDF(blob); 118 } 119 120 @Override 121 public Map<String, Object> getImageMetadata(Blob blob) { 122 log.warn("org.nuxeo.ecm.platform.picture.ImagingComponent.getImageMetadata is deprecated. Please use " 123 + "org.nuxeo.binary.metadata.api.BinaryMetadataService#readMetadata(org.nuxeo.ecm.core.api.Blob)"); 124 return Collections.emptyMap(); 125 } 126 127 @Override 128 public String getImageMimeType(File file) { 129 try { 130 MimetypeRegistry mimetypeRegistry = Framework.getService(MimetypeRegistry.class); 131 return mimetypeRegistry.getMimetypeFromFilenameAndBlobWithDefault(file.getName(), Blobs.createBlob(file), 132 "image/jpeg"); 133 } catch (MimetypeNotFoundException | MimetypeDetectionException | IOException e) { 134 log.error("Unable to retrieve mime type", e); 135 } 136 return null; 137 } 138 139 @Override 140 public String getImageMimeType(Blob blob) { 141 try { 142 MimetypeRegistry mimetypeRegistry = Framework.getService(MimetypeRegistry.class); 143 if (blob.getFilename() != null) { 144 return mimetypeRegistry.getMimetypeFromFilenameAndBlobWithDefault(blob.getFilename(), blob, 145 "image/jpeg"); 146 } else { 147 return mimetypeRegistry.getMimetypeFromBlob(blob); 148 } 149 } catch (MimetypeNotFoundException | MimetypeDetectionException e) { 150 log.error("Unable to retrieve mime type", e); 151 } 152 return null; 153 } 154 155 private LibrarySelector getLibrarySelectorService() { 156 LibrarySelector librarySelector = Framework.getService(LibrarySelector.class); 157 if (librarySelector == null) { 158 log.error("Unable to get LibrarySelector runtime service"); 159 throw new NuxeoException("Unable to get LibrarySelector runtime service"); 160 } 161 return librarySelector; 162 } 163 164 @Override 165 public ImageInfo getImageInfo(Blob blob) { 166 ImageInfo imageInfo = null; 167 try { 168 String ext = blob.getFilename() == null ? ".tmp" : "." + FilenameUtils.getExtension(blob.getFilename()); 169 try (CloseableFile cf = blob.getCloseableFile(ext)) { 170 imageInfo = ImageIdentifier.getInfo(cf.getFile().getAbsolutePath()); 171 } 172 } catch (CommandNotAvailable | CommandException | IOException e) { 173 log.error("Failed to get ImageInfo for file {}", blob.getFilename(), e); 174 } 175 return imageInfo; 176 } 177 178 @Override 179 public void registerContribution(Object contribution, String extensionPoint, ComponentInstance contributor) { 180 if (CONFIGURATION_PARAMETERS_EP.equals(extensionPoint)) { 181 ImagingConfigurationDescriptor desc = (ImagingConfigurationDescriptor) contribution; 182 configurationParameters.putAll(desc.getParameters()); 183 } else if (PICTURE_CONVERSIONS_EP.equals(extensionPoint)) { 184 pictureConversionRegistry.addContribution((PictureConversion) contribution); 185 } 186 } 187 188 @Override 189 public void unregisterContribution(Object contribution, String extensionPoint, ComponentInstance contributor) { 190 if (CONFIGURATION_PARAMETERS_EP.equals(extensionPoint)) { 191 ImagingConfigurationDescriptor desc = (ImagingConfigurationDescriptor) contribution; 192 for (String configuration : desc.getParameters().keySet()) { 193 configurationParameters.remove(configuration); 194 } 195 } else if (PICTURE_CONVERSIONS_EP.equals(extensionPoint)) { 196 pictureConversionRegistry.removeContribution((PictureConversion) contribution); 197 } 198 } 199 200 @Override 201 public String getConfigurationValue(String configurationName) { 202 return configurationParameters.get(configurationName); 203 } 204 205 @Override 206 public String getConfigurationValue(String configurationName, String defaultValue) { 207 return configurationParameters.getOrDefault(configurationName, defaultValue); 208 } 209 210 @Override 211 public void setConfigurationValue(String configurationName, String configurationValue) { 212 configurationParameters.put(configurationName, configurationValue); 213 } 214 215 @Override 216 public PictureView computeViewFor(Blob blob, PictureConversion pictureConversion, boolean convert) { 217 return computeViewFor(blob, pictureConversion, null, convert); 218 } 219 220 @Override 221 public PictureView computeViewFor(Blob blob, PictureConversion pictureConversion, ImageInfo imageInfo, 222 boolean convert) { 223 String mimeType = blob.getMimeType(); 224 if (mimeType == null) { 225 blob.setMimeType(getImageMimeType(blob)); 226 } 227 228 if (imageInfo == null) { 229 imageInfo = getImageInfo(blob); 230 } 231 return computeView(blob, pictureConversion, imageInfo, convert); 232 } 233 234 @Override 235 public List<PictureView> computeViewsFor(Blob blob, List<PictureConversion> pictureConversions, boolean convert) { 236 return computeViewsFor(blob, pictureConversions, null, convert); 237 } 238 239 @Override 240 public List<PictureView> computeViewsFor(Blob blob, List<PictureConversion> pictureConversions, ImageInfo imageInfo, 241 boolean convert) { 242 String mimeType = blob.getMimeType(); 243 if (mimeType == null) { 244 blob.setMimeType(getImageMimeType(blob)); 245 } 246 247 if (imageInfo == null) { 248 imageInfo = getImageInfo(blob); 249 } 250 List<PictureView> views = new ArrayList<>(); 251 for (PictureConversion pictureConversion : pictureConversions) { 252 try { 253 views.add(computeView(blob, pictureConversion, imageInfo, convert)); 254 } catch (NuxeoException e) { 255 log.warn("Unable to compute view: {} exception message: {}", pictureConversion::getId, e::getMessage); 256 log.debug(e, e); 257 } 258 } 259 return views; 260 } 261 262 protected PictureView computeView(Blob blob, PictureConversion pictureConversion, ImageInfo imageInfo, 263 boolean convert) { 264 return computeView(null, blob, pictureConversion, imageInfo, convert); 265 } 266 267 protected PictureView computeView(DocumentModel doc, Blob blob, PictureConversion pictureConversion, 268 ImageInfo imageInfo, boolean convert) { 269 if (convert) { 270 return computeView(doc, blob, pictureConversion, imageInfo); 271 } else { 272 return computeViewWithoutConversion(blob, pictureConversion, imageInfo); 273 } 274 } 275 276 protected Blob wrapBlob(Blob blob) { 277 return new BlobWrapper(blob); 278 } 279 280 protected PictureView computeView(DocumentModel doc, Blob blob, PictureConversion pictureConversion, 281 ImageInfo imageInfo) { 282 283 String title = pictureConversion.getId(); 284 285 Map<String, Serializable> pictureViewMap = new HashMap<>(); 286 pictureViewMap.put(PictureView.FIELD_TITLE, title); 287 pictureViewMap.put(PictureView.FIELD_DESCRIPTION, pictureConversion.getDescription()); 288 pictureViewMap.put(PictureView.FIELD_TAG, pictureConversion.getTag()); 289 290 Point size = new Point(imageInfo.getWidth(), imageInfo.getHeight()); 291 292 /* 293 * If the picture template have a max size then use it for the new size computation, else take the current size 294 * will be used. 295 */ 296 if (pictureConversion.getMaxSize() != null) { 297 size = ImageResizer.scaleToMax(size.x, size.y, pictureConversion.getMaxSize()); 298 } 299 300 // Use the registered conversion format 301 String conversionFormat = getConfigurationValue(CONVERSION_FORMAT, JPEG_CONVERSATION_FORMAT); 302 303 Blob viewBlob = callPictureConversionChain(doc, blob, pictureConversion, imageInfo, size, conversionFormat); 304 305 // If the extension of the generated binary is empty, it's fetched from the mimetype 306 String extension = FilenameUtils.getExtension(viewBlob.getFilename()); 307 if (StringUtils.isEmpty(extension)) { 308 MimetypeRegistry mimetypeRegistry = Framework.getService(MimetypeRegistry.class); 309 List<String> extensions = mimetypeRegistry.getExtensionsFromMimetypeName(viewBlob.getMimeType()); 310 if (extensions != null && !extensions.isEmpty()) { 311 extension = extensions.get(0); 312 } 313 } 314 315 String viewFilename = String.format("%s_%s.%s", title, FilenameUtils.getBaseName(blob.getFilename()), 316 extension); 317 viewBlob.setFilename(viewFilename); 318 ImageInfo viewBlobImageInfo = getImageInfo(viewBlob); 319 pictureViewMap.put(PictureView.FIELD_FILENAME, viewFilename); 320 pictureViewMap.put(PictureView.FIELD_CONTENT, (Serializable) viewBlob); 321 pictureViewMap.put(PictureView.FIELD_INFO, viewBlobImageInfo); 322 if (viewBlobImageInfo != null){ 323 pictureViewMap.put(PictureView.FIELD_WIDTH, viewBlobImageInfo.getWidth()); 324 pictureViewMap.put(PictureView.FIELD_HEIGHT, viewBlobImageInfo.getHeight()); 325 } 326 return new PictureViewImpl(pictureViewMap); 327 } 328 329 protected Blob callPictureConversionChain(DocumentModel doc, Blob blob, PictureConversion pictureConversion, 330 ImageInfo imageInfo, Point size, String conversionFormat) { 331 String chainId = pictureConversion.getChainId(); 332 333 // if the chainId is null just use the same blob (wrapped) 334 if (StringUtils.isBlank(chainId)) { 335 return wrapBlob(blob); 336 } 337 338 Properties parameters = new Properties(); 339 parameters.put(OPTION_RESIZE_WIDTH, String.valueOf(size.x)); 340 parameters.put(OPTION_RESIZE_HEIGHT, String.valueOf(size.y)); 341 parameters.put(OPTION_RESIZE_DEPTH, String.valueOf(imageInfo.getDepth())); 342 parameters.put(CONVERSION_FORMAT, conversionFormat); 343 344 Map<String, Object> chainParameters = new HashMap<>(); 345 chainParameters.put("parameters", parameters); 346 347 boolean txWasActive = false; 348 try (OperationContext context = new OperationContext()) { 349 if (doc != null) { 350 DocumentModel pictureDocument = doc.getCoreSession().getDocument(doc.getRef()); 351 pictureDocument.detach(true); 352 context.put("pictureDocument", pictureDocument); 353 } 354 context.setInput(blob); 355 356 if (TransactionHelper.isTransactionActive()) { 357 txWasActive = true; 358 TransactionHelper.commitOrRollbackTransaction(); 359 } 360 361 Blob viewBlob = (Blob) Framework.getService(AutomationService.class).run(context, chainId, chainParameters); 362 if (viewBlob == null) { 363 viewBlob = wrapBlob(blob); 364 } 365 return viewBlob; 366 } catch (OperationException e) { 367 throw new NuxeoException(e); 368 } finally { 369 if (txWasActive && !TransactionHelper.isTransactionActiveOrMarkedRollback()) { 370 TransactionHelper.startTransaction(); 371 } 372 } 373 } 374 375 @Override 376 public List<PictureView> computeViewsFor(DocumentModel doc, Blob blob, ImageInfo imageInfo, boolean convert) { 377 List<PictureConversion> pictureConversions = getPictureConversions(); 378 List<PictureView> pictureViews = new ArrayList<>(pictureConversions.size()); 379 380 for (PictureConversion pictureConversion : pictureConversions) { 381 if (canApplyPictureConversion(pictureConversion, doc)) { 382 try { 383 PictureView pictureView = computeView(doc, blob, pictureConversion, imageInfo, convert); 384 pictureViews.add(pictureView); 385 } catch (NuxeoException e) { 386 log.warn("Unable to compute view: {} for document: {} exception message: {}", 387 pictureConversion::getId, doc::toString, e::getMessage); 388 log.debug(e, e); 389 } 390 } 391 } 392 393 return pictureViews; 394 } 395 396 protected boolean canApplyPictureConversion(PictureConversion pictureConversion, DocumentModel doc) { 397 ActionManager actionService = Framework.getService(ActionManager.class); 398 return actionService.checkFilters(pictureConversion.getFilterIds(), createActionContext(doc)); 399 } 400 401 protected ActionContext createActionContext(DocumentModel doc) { 402 ActionContext actionContext = new ELActionContext(); 403 actionContext.setCurrentDocument(doc); 404 CoreSession coreSession = doc.getCoreSession(); 405 actionContext.setDocumentManager(coreSession); 406 if (coreSession != null) { 407 actionContext.setCurrentPrincipal(coreSession.getPrincipal()); 408 } 409 return actionContext; 410 } 411 412 protected PictureView computeViewWithoutConversion(Blob blob, PictureConversion pictureConversion, 413 ImageInfo imageInfo) { 414 PictureView view = new PictureViewImpl(); 415 view.setBlob(blob); 416 view.setWidth(imageInfo.getWidth()); 417 view.setHeight(imageInfo.getHeight()); 418 view.setFilename(blob.getFilename()); 419 view.setTitle(pictureConversion.getId()); 420 view.setDescription(pictureConversion.getDescription()); 421 view.setTag(pictureConversion.getTag()); 422 view.setImageInfo(imageInfo); 423 return view; 424 } 425 426 /** 427 * @deprecated since 11.1 use {@link ImageResizer#scaleToMax(int, int, int)} instead. 428 */ 429 @Deprecated 430 protected static Point getSize(Point current, int max) { 431 int x = current.x; 432 int y = current.y; 433 int newX; 434 int newY; 435 if (x > y) { // landscape 436 newY = (y * max) / x; 437 newX = max; 438 } else { // portrait 439 newX = (x * max) / y; 440 newY = max; 441 } 442 if (newX > x || newY > y) { 443 return current; 444 } 445 return new Point(newX, newY); 446 } 447 448 @Override 449 public List<List<PictureView>> computeViewsFor(List<Blob> blobs, List<PictureConversion> pictureConversions, 450 boolean convert) { 451 return computeViewsFor(blobs, pictureConversions, null, convert); 452 } 453 454 @Override 455 public List<List<PictureView>> computeViewsFor(List<Blob> blobs, List<PictureConversion> pictureConversions, 456 ImageInfo imageInfo, boolean convert) { 457 List<List<PictureView>> allViews = new ArrayList<>(); 458 for (Blob blob : blobs) { 459 allViews.add(computeViewsFor(blob, pictureConversions, imageInfo, convert)); 460 } 461 return allViews; 462 } 463 464 @Override 465 public void activate(ComponentContext context) { 466 pictureMigrationHandler.install(); 467 } 468 469 @Override 470 public void deactivate(ComponentContext context) { 471 pictureMigrationHandler.uninstall(); 472 } 473}