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