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