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.getLocalService(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.getLocalService(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.getLocalService(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 String viewFilename = String.format("%s_%s.%s", title, FilenameUtils.getBaseName(blob.getFilename()), 403 FilenameUtils.getExtension(viewBlob.getFilename())); 404 viewBlob.setFilename(viewFilename); 405 pictureViewMap.put(PictureView.FIELD_FILENAME, viewFilename); 406 pictureViewMap.put(PictureView.FIELD_CONTENT, (Serializable) viewBlob); 407 pictureViewMap.put(PictureView.FIELD_INFO, getImageInfo(viewBlob)); 408 409 return new PictureViewImpl(pictureViewMap); 410 } 411 412 protected Blob callPictureConversionChain(DocumentModel doc, Blob blob, PictureConversion pictureConversion, 413 ImageInfo imageInfo, Point size, String conversionFormat) { 414 String chainId = pictureConversion.getChainId(); 415 416 // if the chainId is null just use the same blob (wrapped) 417 if (StringUtils.isBlank(chainId)) { 418 return wrapBlob(blob); 419 } 420 421 Properties parameters = new Properties(); 422 parameters.put(OPTION_RESIZE_WIDTH, String.valueOf(size.x)); 423 parameters.put(OPTION_RESIZE_HEIGHT, String.valueOf(size.y)); 424 parameters.put(OPTION_RESIZE_DEPTH, String.valueOf(imageInfo.getDepth())); 425 parameters.put(CONVERSION_FORMAT, conversionFormat); 426 427 Map<String, Object> chainParameters = new HashMap<>(); 428 chainParameters.put("parameters", parameters); 429 430 OperationContext context = new OperationContext(); 431 if (doc != null) { 432 DocumentModel pictureDocument = doc.getCoreSession().getDocument(doc.getRef()); 433 pictureDocument.detach(true); 434 context.put("pictureDocument", pictureDocument); 435 } 436 context.setInput(blob); 437 438 boolean txWasActive = false; 439 try { 440 if (TransactionHelper.isTransactionActive()) { 441 txWasActive = true; 442 TransactionHelper.commitOrRollbackTransaction(); 443 } 444 445 Blob viewBlob = (Blob) Framework.getService(AutomationService.class).run(context, chainId, chainParameters); 446 if (viewBlob == null) { 447 viewBlob = wrapBlob(blob); 448 } 449 return viewBlob; 450 } catch (OperationException e) { 451 throw new NuxeoException(e); 452 } finally { 453 if (txWasActive && !TransactionHelper.isTransactionActiveOrMarkedRollback()) { 454 TransactionHelper.startTransaction(); 455 } 456 } 457 } 458 459 @Override 460 public List<PictureView> computeViewsFor(DocumentModel doc, Blob blob, ImageInfo imageInfo, boolean convert) 461 throws IOException { 462 List<PictureConversion> pictureConversions = getPictureConversions(); 463 List<PictureView> pictureViews = new ArrayList<>(pictureConversions.size()); 464 465 for (PictureConversion pictureConversion : pictureConversions) { 466 if (canApplyPictureConversion(pictureConversion, doc)) { 467 PictureView pictureView = computeView(doc, blob, pictureConversion, imageInfo, convert); 468 pictureViews.add(pictureView); 469 } 470 } 471 472 return pictureViews; 473 } 474 475 protected boolean canApplyPictureConversion(PictureConversion pictureConversion, DocumentModel doc) { 476 if (pictureConversion.isDefault()) { 477 return true; 478 } 479 480 ActionManager actionService = Framework.getService(ActionManager.class); 481 return actionService.checkFilters(pictureConversion.getFilterIds(), createActionContext(doc)); 482 } 483 484 protected ActionContext createActionContext(DocumentModel doc) { 485 ActionContext actionContext = new ELActionContext(); 486 actionContext.setCurrentDocument(doc); 487 return actionContext; 488 } 489 490 protected PictureView computeViewWithoutConversion(Blob blob, PictureConversion pictureConversion, 491 ImageInfo imageInfo) { 492 PictureView view = new PictureViewImpl(); 493 view.setBlob(blob); 494 view.setWidth(imageInfo.getWidth()); 495 view.setHeight(imageInfo.getHeight()); 496 view.setFilename(blob.getFilename()); 497 view.setTitle(pictureConversion.getId()); 498 view.setDescription(pictureConversion.getDescription()); 499 view.setTag(pictureConversion.getTag()); 500 view.setImageInfo(imageInfo); 501 return view; 502 } 503 504 protected static Point getSize(Point current, int max) { 505 int x = current.x; 506 int y = current.y; 507 int newX; 508 int newY; 509 if (x > y) { // landscape 510 newY = (y * max) / x; 511 newX = max; 512 } else { // portrait 513 newX = (x * max) / y; 514 newY = max; 515 } 516 if (newX > x || newY > y) { 517 return current; 518 } 519 return new Point(newX, newY); 520 } 521 522 @Override 523 public List<List<PictureView>> computeViewsFor(List<Blob> blobs, List<PictureConversion> pictureConversions, 524 boolean convert) throws IOException { 525 return computeViewsFor(blobs, pictureConversions, null, convert); 526 } 527 528 @Override 529 public List<List<PictureView>> computeViewsFor(List<Blob> blobs, List<PictureConversion> pictureConversions, 530 ImageInfo imageInfo, boolean convert) throws IOException { 531 List<List<PictureView>> allViews = new ArrayList<List<PictureView>>(); 532 for (Blob blob : blobs) { 533 allViews.add(computeViewsFor(blob, pictureConversions, imageInfo, convert)); 534 } 535 return allViews; 536 } 537 538 @Override 539 public void activate(ComponentContext context) { 540 pictureMigrationHandler.install(); 541 } 542 543 @Override 544 public void deactivate(ComponentContext context) { 545 pictureMigrationHandler.uninstall(); 546 } 547}