diff --git a/modules/core/core-content/src/main/java/com/enonic/xp/core/impl/content/processor/ImageContentProcessor.java b/modules/core/core-content/src/main/java/com/enonic/xp/core/impl/content/processor/ImageContentProcessor.java index 8fba6197d98..eb7ce7f9c6d 100644 --- a/modules/core/core-content/src/main/java/com/enonic/xp/core/impl/content/processor/ImageContentProcessor.java +++ b/modules/core/core-content/src/main/java/com/enonic/xp/core/impl/content/processor/ImageContentProcessor.java @@ -5,7 +5,11 @@ import java.io.InputStream; import java.util.Collection; import java.util.HashMap; +import java.util.HashSet; +import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.Set; import java.util.stream.Collectors; import javax.imageio.ImageIO; @@ -66,37 +70,127 @@ public final class ImageContentProcessor { private static final Logger LOG = LoggerFactory.getLogger( ImageContentProcessor.class ); - private static final ImmutableMap FIELD_CONFORMITY_MAP = ImmutableMap.builder(). - put( "tiffImagelength", "imageHeight" ). - put( "tiffImagewidth", "imageWidth" ). - put( "exposureBiasValue", "exposureBias" ). - put( "FNumber", "aperture" ). - put( "exposureTime", "shutterTime" ). - put( "subjectDistanceRange", "focusDistance" ). - put( "gpsAltitude", "altitude" ). - put( "gpsImgDirection", "direction" ). - put( "whiteBalanceMode", "whiteBalance" ). - put( "isoSpeedRatings", "iso" ). - put( "dcDescription", "description" ). - put( "iccColorSpace", "colorSpace" ). - put( "exifSubifdColorSpace", "colorSpace" ). - put( "dctermsModified", "date" ). - put( "tiffMake", "make" ). - put( "tiffModel", "model" ). - put( "exifSubifdLensModel", "lens" ). - put( "exifIsospeedratings", "iso" ). - put( "exifSubifdFocalLength", "focalLength" ). - put( "exifSubifdExposureBiasValue", "exposureBias" ). - put( "exifSubifdApertureValue", "aperture" ). - put( "exifSubifdExposureTime", "shutterTime" ). - put( "exifSubifdWhiteBalanceMode", "whiteBalance" ). - put( "exifSubifdExposureProgram", "exposureProgram" ). - put( "exifSubifdMeteringMode", "meteringMode" ). - put( "exifSubifdExposureMode", "exposureMode" ). - put( "exifIfd0Orientation", "orientation" ). - put( "globalAltitude", "altitude" ). - put( "exifSubifdFlash", "flash" ). - build(); + private static final List IMAGE_LENGTH_VALUES = List.of( "tiffImagelength", "imageHeight" ); + + private static final List IMAGE_WIDTH_VALUES = List.of( "tiffImagewidth", "imageWidth" ); + + private static final List EXPOSURE_BIAS_VALUES = List.of( "exifSubifdExposureBiasValue", "exposureBiasValue", "exposureBias" ); + + private static final List APERTURE_VALUES = List.of( "exifSubifdApertureValue", "FNumber", "aperture" ); + + private static final List SHUTTER_TIME_VALUES = List.of( "exifSubifdExposureTime", "exposureTime", "shutterTime" ); + + private static final List FOCUS_DISTANCE_VALUES = List.of( "subjectDistanceRange", "focusDistance" ); + + private static final List ALTITUDE_VALUES = List.of( "gpsAltitude", "globalAltitude", "altitude" ); + + private static final List DIRECTION_VALUES = List.of( "gpsImgDirection", "direction" ); + + private static final List WHITE_BALANCE_VALUES = List.of( "exifSubifdWhiteBalanceMode", "whiteBalanceMode", "whiteBalance" ); + + private static final List ISO_VALUES = List.of( "isoSpeedRatings", "exifIsospeedratings", "iso" ); + + private static final List DESCRIPTION_VALUES = List.of( "dcDescription", "description" ); + + private static final List COLOR_SPACE_VALUES = List.of( "exifSubifdColorSpace", "iccColorSpace", "colorSpace" ); + + private static final List DATE_VALUES = List.of( "dctermsModified", "date" ); + + private static final List MAKE_VALUES = List.of( "tiffMake", "make" ); + + private static final List MODEL_VALUES = List.of( "tiffModel", "model" ); + + private static final List LENS_VALUES = List.of( "exifSubifdLensModel", "lens" ); + + private static final List FOCAL_LENGTH_VALUES = List.of( "exifSubifdFocalLength", "focalLength" ); + + private static final List EXPOSURE_PROGRAM_VALUES = List.of( "exifSubifdExposureProgram", "exposureProgram" ); + + private static final List METERING_MODE_VALUES = List.of( "exifSubifdMeteringMode", "meteringMode" ); + + private static final List EXPOSURE_MODE_VALUES = List.of( "exifSubifdExposureMode", "exposureMode" ); + + private static final List ORIENTATION_VALUES = List.of( "exifIfd0Orientation", "orientation" ); + + private static final List FLASH_VALUES = List.of( "exifSubifdFlash", "flash" ); + + private static final Map> METADATA_PRIORITY_MAP = ImmutableMap.>builder() + .put( "tiffImagelength", IMAGE_LENGTH_VALUES ) + .put( "imageHeight", IMAGE_LENGTH_VALUES ) + .put( "tiffImagewidth", IMAGE_WIDTH_VALUES ) + .put( "imageWidth", IMAGE_WIDTH_VALUES ) + .put( "exifSubifdExposureBiasValue", EXPOSURE_BIAS_VALUES ) + .put( "exposureBiasValue", EXPOSURE_BIAS_VALUES ) + .put( "exposureBias", EXPOSURE_BIAS_VALUES ) + .put( "exifSubifdApertureValue", APERTURE_VALUES ) + .put( "FNumber", APERTURE_VALUES ) + .put( "aperture", APERTURE_VALUES ) + .put( "exifSubifdExposureTime", SHUTTER_TIME_VALUES ) + .put( "exposureTime", SHUTTER_TIME_VALUES ) + .put( "shutterTime", SHUTTER_TIME_VALUES ) + .put( "subjectDistanceRange", FOCUS_DISTANCE_VALUES ) + .put( "focusDistance", FOCUS_DISTANCE_VALUES ) + .put( "gpsAltitude", ALTITUDE_VALUES ) + .put( "globalAltitude", ALTITUDE_VALUES ) + .put( "altitude", ALTITUDE_VALUES ) + .put( "gpsImgDirection", DIRECTION_VALUES ) + .put( "direction", DIRECTION_VALUES ) + .put( "exifSubifdWhiteBalanceMode", WHITE_BALANCE_VALUES ) + .put( "whiteBalanceMode", WHITE_BALANCE_VALUES ) + .put( "whiteBalance", WHITE_BALANCE_VALUES ) + .put( "isoSpeedRatings", ISO_VALUES ) + .put( "exifIsospeedratings", ISO_VALUES ) + .put( "iso", ISO_VALUES ) + .put( "dcDescription", DESCRIPTION_VALUES ) + .put( "description", DESCRIPTION_VALUES ) + .put( "exifSubifdColorSpace", COLOR_SPACE_VALUES ) + .put( "iccColorSpace", COLOR_SPACE_VALUES ) + .put( "colorSpace", COLOR_SPACE_VALUES ) + .put( "dctermsModified", DATE_VALUES ) + .put( "date", DATE_VALUES ) + .put( "tiffMake", MAKE_VALUES ) + .put( "make", MAKE_VALUES ) + .put( "tiffModel", MODEL_VALUES ) + .put( "model", MODEL_VALUES ) + .put( "exifSubifdLensModel", LENS_VALUES ) + .put( "lens", LENS_VALUES ) + .put( "exifSubifdFocalLength", FOCAL_LENGTH_VALUES ) + .put( "focalLength", FOCAL_LENGTH_VALUES ) + .put( "exifSubifdExposureProgram", EXPOSURE_PROGRAM_VALUES ) + .put( "exposureProgram", EXPOSURE_PROGRAM_VALUES ) + .put( "exifSubifdMeteringMode", METERING_MODE_VALUES ) + .put( "meteringMode", METERING_MODE_VALUES ) + .put( "exifSubifdExposureMode", EXPOSURE_MODE_VALUES ) + .put( "exposureMode", EXPOSURE_MODE_VALUES ) + .put( "exifIfd0Orientation", ORIENTATION_VALUES ) + .put( "orientation", ORIENTATION_VALUES ) + .put( "flash", FLASH_VALUES ) + .build(); + + private static final ImmutableMap FORM_CONFORMITY_MAP = ImmutableMap.builder() + .putAll( getFlattenedMap( IMAGE_LENGTH_VALUES, "imageHeight" ) ) + .putAll( getFlattenedMap( IMAGE_WIDTH_VALUES, "imageWidth" ) ) + .putAll( getFlattenedMap( EXPOSURE_BIAS_VALUES, "exposureBias" ) ) + .putAll( getFlattenedMap( APERTURE_VALUES, "aperture" ) ) + .putAll( getFlattenedMap( SHUTTER_TIME_VALUES, "shutterTime" ) ) + .putAll( getFlattenedMap( FOCUS_DISTANCE_VALUES, "focusDistance" ) ) + .putAll( getFlattenedMap( ALTITUDE_VALUES, "altitude" ) ) + .putAll( getFlattenedMap( DIRECTION_VALUES, "direction" ) ) + .putAll( getFlattenedMap( WHITE_BALANCE_VALUES, "whiteBalance" ) ) + .putAll( getFlattenedMap( ISO_VALUES, "iso" ) ) + .putAll( getFlattenedMap( DESCRIPTION_VALUES, "description" ) ) + .putAll( getFlattenedMap( COLOR_SPACE_VALUES, "colorSpace" ) ) + .putAll( getFlattenedMap( DATE_VALUES, "date" ) ) + .putAll( getFlattenedMap( MAKE_VALUES, "make" ) ) + .putAll( getFlattenedMap( MODEL_VALUES, "model" ) ) + .putAll( getFlattenedMap( LENS_VALUES, "lens" ) ) + .putAll( getFlattenedMap( FOCAL_LENGTH_VALUES, "focalLength" ) ) + .putAll( getFlattenedMap( EXPOSURE_PROGRAM_VALUES, "exposureProgram" ) ) + .putAll( getFlattenedMap( METERING_MODE_VALUES, "meteringMode" ) ) + .putAll( getFlattenedMap( EXPOSURE_MODE_VALUES, "exposureMode" ) ) + .putAll( getFlattenedMap( ORIENTATION_VALUES, "orientation" ) ) + .putAll( getFlattenedMap( FLASH_VALUES, "flash" ) ) + .build(); private static final String GEO_LONGITUDE = "geoLong"; @@ -120,6 +214,17 @@ private XDatas getXDatas( final ContentTypeName contentTypeName ) return xDataService.getFromContentType( contentType ); } + // Helper function to create a map where each key in the list points to the same value + private static ImmutableMap getFlattenedMap( List keys, String value ) + { + ImmutableMap.Builder builder = ImmutableMap.builder(); + for ( String key : keys ) + { + builder.put( key, value ); + } + return builder.build(); + } + @Override public ProcessCreateResult processCreate( final ProcessCreateParams params ) { @@ -142,50 +247,8 @@ public ProcessCreateResult processCreate( final ProcessCreateParams params ) final CreateAttachments.Builder builder = CreateAttachments.create(); builder.add( sourceAttachment ); - return new ProcessCreateResult( CreateContentParams.create( createContentParams ). - createAttachments( builder.build() ).extraDatas( extraDatas ). - build() ); - } - - @Override - public ProcessUpdateResult processUpdate( final ProcessUpdateParams params ) - { - final CreateAttachments createAttachments = params.getCreateAttachments(); - final MediaInfo mediaInfo = params.getMediaInfo(); - - final CreateAttachment sourceAttachment = createAttachments == null ? null : createAttachments.first(); - - final ContentEditor editor; - if ( mediaInfo != null ) - { - editor = editable -> { - - final Map extraDatas = - editable.extraDatas.stream().collect( Collectors.toMap( ExtraData::getName, o -> o ) ); - - final XDatas contentXDatas = getXDatas( params.getContentType().getName() ); - - extractMetadata( mediaInfo, contentXDatas, sourceAttachment ). - forEach( extraData -> extraDatas.put( extraData.getName(), extraData ) ); - - editable.extraDatas = ExtraDatas.create().addAll( extraDatas.values() ).build(); - }; - } - else - { - editor = editable -> { - - if ( !params.getContentType().getName().isDescendantOfMedia() ) - { - return; - } - - editable.extraDatas = updateImageMetadata( editable ); - - }; - } - return new ProcessUpdateResult( createAttachments, editor ); - + return new ProcessCreateResult( + CreateContentParams.create( createContentParams ).createAttachments( builder.build() ).extraDatas( extraDatas ).build() ); } @@ -270,55 +333,45 @@ private BufferedImage cropImage( final BufferedImage image, final Cropping cropp (int) ( height * cropping.height() ) ); } - private ExtraDatas extractMetadata( final MediaInfo mediaInfo, final XDatas xDatas, final CreateAttachment sourceAttachment ) + @Override + public ProcessUpdateResult processUpdate( final ProcessUpdateParams params ) { - final ExtraDatas.Builder extradatasBuilder = ExtraDatas.create(); - final Map metadataMap = new HashMap<>(); - final ExtraData geoData = extractGeoLocation( mediaInfo, xDatas ); - if ( geoData != null ) + final CreateAttachments createAttachments = params.getCreateAttachments(); + final MediaInfo mediaInfo = params.getMediaInfo(); + + final CreateAttachment sourceAttachment = createAttachments == null ? null : createAttachments.first(); + + final ContentEditor editor; + if ( mediaInfo != null ) { - metadataMap.put( MediaInfo.GPS_INFO_METADATA_NAME, geoData ); - extradatasBuilder.add( geoData ); + editor = editable -> { + + final Map extraDatas = + editable.extraDatas.stream().collect( Collectors.toMap( ExtraData::getName, o -> o ) ); + + final XDatas contentXDatas = getXDatas( params.getContentType().getName() ); + + extractMetadata( mediaInfo, contentXDatas, sourceAttachment ).forEach( + extraData -> extraDatas.put( extraData.getName(), extraData ) ); + + editable.extraDatas = ExtraDatas.create().addAll( extraDatas.values() ).build(); + }; } - for ( Map.Entry> entry : mediaInfo.getMetadata().asMap().entrySet() ) + else { - for ( XData xData : xDatas ) - { - final String formItemName = getConformityName( entry.getKey() ); - final FormItem formItem = xData.getForm().getFormItems().getItemByName( formItemName ); - if ( formItem == null ) - { - continue; - } - ExtraData extraData = metadataMap.get( xData.getName() ); - if ( extraData == null ) - { - extraData = new ExtraData( xData.getName(), new PropertyTree() ); - metadataMap.put( xData.getName(), extraData ); - extradatasBuilder.add( extraData ); - } - if ( FormItemType.INPUT.equals( formItem.getType() ) ) + editor = editable -> { + + if ( !params.getContentType().getName().isDescendantOfMedia() ) { - Input input = (Input) formItem; - if ( InputTypeName.DATE_TIME.equals( input.getInputType() ) ) - { - extraData.getData().addLocalDateTime( formItemName, - ValueTypes.LOCAL_DATE_TIME.convert( entry.getValue().toArray()[0] ) ); - } - else if ( InputTypeName.LONG.equals( input.getInputType() ) ) - { - final Long[] longValues = entry.getValue().stream().map( Long::parseLong ).toArray( Long[]::new ); - extraData.getData().addLongs( formItemName, longValues ); - } - else - { - extraData.getData().addStrings( formItemName, entry.getValue() ); - } + return; } - } + + editable.extraDatas = updateImageMetadata( editable ); + + }; } - fillComputedFormItems( metadataMap.values(), mediaInfo, sourceAttachment ); - return extradatasBuilder.build(); + return new ProcessUpdateResult( createAttachments, editor ); + } private ExtraData extractGeoLocation( final MediaInfo mediaInfo, final XDatas xDatas ) @@ -365,15 +418,6 @@ private Double parseDouble( final String str ) } } - private String getConformityName( String tikaFieldValue ) - { - if ( FIELD_CONFORMITY_MAP.containsValue( tikaFieldValue ) ) - { - return null; - } - return FIELD_CONFORMITY_MAP.getOrDefault( tikaFieldValue, tikaFieldValue ); - } - private void fillComputedFormItems( Collection extraDataList, MediaInfo mediaInfo, final CreateAttachment sourceAttachment ) { for ( ExtraData extraData : extraDataList ) @@ -407,6 +451,85 @@ private void fillComputedFormItems( Collection extraDataList, MediaIn } } + private ExtraDatas extractMetadata( final MediaInfo mediaInfo, final XDatas xDatas, final CreateAttachment sourceAttachment ) + { + final ExtraDatas.Builder extradatasBuilder = ExtraDatas.create(); + final Map metadataMap = new HashMap<>(); + final ExtraData geoData = extractGeoLocation( mediaInfo, xDatas ); + if ( geoData != null ) + { + metadataMap.put( MediaInfo.GPS_INFO_METADATA_NAME, geoData ); + extradatasBuilder.add( geoData ); + } + + final Set visitedFormItems = new HashSet<>(); + + for ( Map.Entry> mediaInfoEntry : mediaInfo.getMetadata().asMap().entrySet() ) + { + String formItemName; + Collection mediaEntryValues; + + final List priorityList = METADATA_PRIORITY_MAP.get( mediaInfoEntry.getKey() ); + + if ( priorityList != null ) + { + formItemName = FORM_CONFORMITY_MAP.get( mediaInfoEntry.getKey() ); + + if ( visitedFormItems.contains( formItemName ) ) + { + continue; + } + + mediaEntryValues = + priorityList.stream().map( mediaInfo.getMetadata().asMap()::get ).filter( Objects::nonNull ).findFirst().orElseThrow(); + + visitedFormItems.add( formItemName ); + + } + else + { + formItemName = mediaInfoEntry.getKey(); + mediaEntryValues = mediaInfoEntry.getValue(); + } + + for ( XData xData : xDatas ) + { + final FormItem formItem = xData.getForm().getFormItems().getItemByName( formItemName ); + if ( formItem == null ) + { + continue; + } + ExtraData extraData = metadataMap.get( xData.getName() ); + if ( extraData == null ) + { + extraData = new ExtraData( xData.getName(), new PropertyTree() ); + metadataMap.put( xData.getName(), extraData ); + extradatasBuilder.add( extraData ); + } + if ( FormItemType.INPUT.equals( formItem.getType() ) ) + { + Input input = (Input) formItem; + if ( InputTypeName.DATE_TIME.equals( input.getInputType() ) ) + { + extraData.getData() + .addLocalDateTime( formItemName, ValueTypes.LOCAL_DATE_TIME.convert( mediaEntryValues.toArray()[0] ) ); + } + else if ( InputTypeName.LONG.equals( input.getInputType() ) ) + { + final Long[] longValues = mediaEntryValues.stream().map( Long::parseLong ).toArray( Long[]::new ); + extraData.getData().addLongs( formItemName, longValues ); + } + else + { + extraData.getData().addStrings( formItemName, mediaEntryValues ); + } + } + } + } + fillComputedFormItems( metadataMap.values(), mediaInfo, sourceAttachment ); + return extradatasBuilder.build(); + } + @Reference public void setXDataService( final XDataService xDataService ) { diff --git a/modules/core/core-content/src/test/java/com/enonic/xp/core/impl/content/processor/ImageContentProcessorTest.java b/modules/core/core-content/src/test/java/com/enonic/xp/core/impl/content/processor/ImageContentProcessorTest.java index d782b29f7fb..56decbe3e4c 100644 --- a/modules/core/core-content/src/test/java/com/enonic/xp/core/impl/content/processor/ImageContentProcessorTest.java +++ b/modules/core/core-content/src/test/java/com/enonic/xp/core/impl/content/processor/ImageContentProcessorTest.java @@ -261,17 +261,37 @@ public void testProcessUpdateWithMediaInfo() build(); final ProcessUpdateResult result = this.imageContentProcessor.processUpdate( processUpdateParams ); final PropertyTree data = new PropertyTree(); - final EditableContent editableContent = new EditableContent( Content.create(). - name( "myContentName" ). - parentPath( ContentPath.ROOT ). - data( data ). - build() ); + final EditableContent editableContent = + new EditableContent( Content.create().name( "myContentName" ).parentPath( ContentPath.ROOT ).data( data ).build() ); result.getEditor().edit( editableContent ); assertEquals( editableContent.extraDatas.first().getData().getString( "shutterTime", 0 ), "1" ); assertEquals( editableContent.extraDatas.first().getData().getString( "altitude", 0 ), "2" ); assertEquals( 13, editableContent.extraDatas.first().getData().getLong( MediaInfo.MEDIA_INFO_BYTE_SIZE, 0 ) ); } + @Test + public void testProcessUpdateWithMediaInfoOverwritten() + throws IOException + { + final Form.Builder form = Form.create(); + form.addFormItem( createTextLine( "shutterTime", "Exposure Time" ).occurrences( 0, 1 ).build() ); + final XData xDataInfo = createXData( MediaInfo.IMAGE_INFO_METADATA_NAME, "Extra Info", form.build() ); + Mockito.when( this.xDataService.getFromContentType( Mockito.any() ) ).thenReturn( XDatas.from( xDataInfo ) ); + final ProcessUpdateParams processUpdateParams = ProcessUpdateParams.create() + .contentType( ContentType.create().superType( ContentTypeName.imageMedia() ).name( "myContent" ).build() ) + .mediaInfo( MediaInfo.create() + .addMetadata( "exposure time", "1" ) + .addMetadata( "exif Subifd Exposure Time", "2" ) + .addMetadata( "shutter Time", "3" ) + .build() ) + .build(); + final ProcessUpdateResult result = this.imageContentProcessor.processUpdate( processUpdateParams ); + final EditableContent editableContent = new EditableContent( + Content.create().name( "myContentName" ).parentPath( ContentPath.ROOT ).data( new PropertyTree() ).build() ); + result.getEditor().edit( editableContent ); + assertEquals( editableContent.extraDatas.first().getData().getString( "shutterTime", 0 ), "2" ); + } + private static Form createGpsInfoMixinForm() { final Form.Builder form = Form.create();