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