Tag ordering

Started by Tom Cunningham, September 25, 2016, 12:26:27 AM

Previous topic - Next topic

Tom Cunningham

When I perform several SetNewValues then do a WriteInfo, an exiftool -s command reports the tags in a somewhat random order:


PersonInImage                   : Betsy Loar, Graham Canzanello, Connor Canzanello
RegionPersonDisplayName         : Betsy Loar, Graham Canzanello, Connor Canzanello
RegionRectangle                 : 0.219883, 0.0620888, 0.111011, 0.109923, 0.152668, 0.234649, 0.0693537, 0.095943, 0.52916, 0.482593, 0.0680027, 0.106086
LastKeywordXMP                  : People/Graham Canzanello, People/Connor Canzanello, People/Betsy Loar
Categories                      : <Categories><Category Assigned="0">People<Category Assigned="1">Betsy Loar</Category><Category Assigned="1">Connor Canzanello</Category><Category Assigned="1">Graham Canzanello</Category></Category></Categories>
Creator                         : Picasa
Description                     : 2016-06-28 040 Betsy Graham Connor Mohle Austin
Subject                         : Betsy Loar, Graham Canzanello, Connor Canzanello
TagsList                        : People/Graham Canzanello, People/Connor Canzanello, People/Betsy Loar
HierarchicalSubject             : People|Graham Canzanello, People|Connor Canzanello, People|Betsy Loar
CatalogSets                     : People|Graham Canzanello, People|Connor Canzanello, People|Betsy Loar
RegionAppliedToDimensionsH      : 3648
RegionAppliedToDimensionsUnit   : pixel
RegionAppliedToDimensionsW      : 4441
RegionAreaH                     : 0.109923, 0.095943, 0.106086
RegionAreaUnit                  : normalized, normalized, normalized
RegionAreaW                     : 0.111011, 0.0693537, 0.0680027
RegionAreaX                     : 0.219883, 0.152668, 0.52916
RegionAreaY                     : 0.0620888, 0.234649, 0.482593
RegionName                      : Betsy Loar, Graham Canzanello, Connor Canzanello
RegionType                      : Face, Face, Face
CreatorTool                     : digiKam-5.0.0
CurrentIPTCDigest               : 79f74a270e4c84c66104e3c609b11572
EnvelopeRecordVersion           : 4
CodedCharacterSet               : UTF8
ApplicationRecordVersion        : 4
Keywords                        : Betsy Loar, Graham Canzanello, Connor Canzanello


I'm not sure this should be important to an application, but according to digiKam it is, claiming that metadata ordering is significant.  For example, with the example above, digiKam will not display some of the face rectangles.  Does an exiftool dump show some kind of ordering, and if so, is there any way to alter the sequencing with WriteInfo?

Phil Harvey

Ordering within lists is sometimes significant, and the hierarchy of structured XMP tags is significant, but the ordering of different XMP tags at the same level is not significant, and ExifTool writes these in alphabetical order of tag ID.  Take a look at the raw XMP with "-exiftool -b -xmp FILE" if you want to see the raw XMP and the actual way the tags are written.

- Phil
...where DIR is the name of a directory/folder containing the images.  On Mac/Linux/PowerShell, use single quotes (') instead of double quotes (") around arguments containing a dollar sign ($).

Tom Cunningham

Thanks, Phil.  In a post from last year you said:


If you are writing XMP, then ExifTool will sort the XMP tags into alphabetical order when writing.  You can't avoid this.


So is there no way to override the alphabetical ordering, even though it shouldn't matter?

The reason I ask is, digiKam seems pretty happy with this fragment (exiftool -b -xmp):


            <mwg-rs:Regions rdf:parseType="Resource">
                <mwg-rs:AppliedToDimensions stDim:w="4441" stDim:h="3648" stDim:unit="pixel"/>
                <mwg-rs:RegionList>
                    <rdf:Bag>
                        <rdf:li>
                            <rdf:Description mwg-rs:Name="Betsy Loar" mwg-rs:Type="Face">
                                <mwg-rs:Area stArea:x="0.219883" stArea:y="0.0620888" stArea:w="0.111011" stArea:h="0.109923" stArea:unit="normalized"/>
                            </rdf:Description>
                        </rdf:li>
                        <rdf:li>
                            <rdf:Description mwg-rs:Name="Graham Canzanello" mwg-rs:Type="Face">
                                <mwg-rs:Area stArea:x="0.152668" stArea:y="0.234649" stArea:w="0.0693537" stArea:h="0.095943" stArea:unit="normalized"/>
                            </rdf:Description>
                        </rdf:li>
                        <rdf:li>
                            <rdf:Description mwg-rs:Name="Connor Canzanello" mwg-rs:Type="Face">
                                <mwg-rs:Area stArea:x="0.52916" stArea:y="0.482593" stArea:w="0.0680027" stArea:h="0.106086" stArea:unit="normalized"/>
                            </rdf:Description>
                        </rdf:li>
                    </rdf:Bag>
                </mwg-rs:RegionList>
            </mwg-rs:Regions>


but not AT ALL happy with this fragment (after performing various SetNewValues followed by one WriteInfo):


  <mwg-rs:Regions rdf:parseType='Resource'>
   <mwg-rs:AppliedToDimensions rdf:parseType='Resource'>
    <stDim:h>3648</stDim:h>
    <stDim:unit>pixel</stDim:unit>
    <stDim:w>4441</stDim:w>
   </mwg-rs:AppliedToDimensions>
   <mwg-rs:RegionList>
    <rdf:Bag>
     <rdf:li rdf:parseType='Resource'>
      <mwg-rs:Area rdf:parseType='Resource'>
       <stArea:h>0.109923</stArea:h>
       <stArea:unit>normalized</stArea:unit>
       <stArea:w>0.111011</stArea:w>
       <stArea:x>0.219883</stArea:x>
       <stArea:y>0.0620888</stArea:y>
      </mwg-rs:Area>
      <mwg-rs:Name>Betsy Loar</mwg-rs:Name>
      <mwg-rs:Type>Face</mwg-rs:Type>
     </rdf:li>
     <rdf:li rdf:parseType='Resource'>
      <mwg-rs:Name>Graham Canzanello</mwg-rs:Name>
      <mwg-rs:Type>Face</mwg-rs:Type>
     </rdf:li>
     <rdf:li rdf:parseType='Resource'>
      <mwg-rs:Name>Connor Canzanello</mwg-rs:Name>
      <mwg-rs:Type>Face</mwg-rs:Type>
     </rdf:li>
     <rdf:li rdf:parseType='Resource'>
      <mwg-rs:Area rdf:parseType='Resource'>
       <stArea:h>0.095943</stArea:h>
       <stArea:unit>normalized</stArea:unit>
       <stArea:w>0.0693537</stArea:w>
       <stArea:x>0.152668</stArea:x>
       <stArea:y>0.234649</stArea:y>
      </mwg-rs:Area>
     </rdf:li>
     <rdf:li rdf:parseType='Resource'>
      <mwg-rs:Area rdf:parseType='Resource'>
       <stArea:h>0.106086</stArea:h>
       <stArea:unit>normalized</stArea:unit>
       <stArea:w>0.0680027</stArea:w>
       <stArea:x>0.52916</stArea:x>
       <stArea:y>0.482593</stArea:y>
      </mwg-rs:Area>
     </rdf:li>
    </rdf:Bag>
   </mwg-rs:RegionList>
  </mwg-rs:Regions>


I'm just shooting in the dark here, there may be some other reason digiKam can't locate these rectangles, it just seemed the most obvious.

Phil Harvey

I think this is a question for digiKam.  Both XMP fragments look fine to me.

You could try changing the order manually, then putting the XMP back into the file to see if digiKam likes it:

exiftool "-xmp<=out.xmp" FILE

I would be surprised if it is just an ordering issue, but if this is the case then a bug should be filed with digiKam.

- Phil
...where DIR is the name of a directory/folder containing the images.  On Mac/Linux/PowerShell, use single quotes (') instead of double quotes (") around arguments containing a dollar sign ($).

Tom Cunningham

I did try changing it manually by replacing the entire XMP block with the one digiKam likes (e.g. the first example above).  And that was fine with digiKam.  Then I looked more closely and saw this (from the second example that digiKam does not like):


        <rdf:Description rdf:about='' xmlns:mwg-rs='http://www.metadataworkinggroup.com/schemas/regions/' xmlns:stArea='http://ns.adobe.com/xmp/sType/Area#' xmlns:stDim='http://ns.adobe.com/xap/1.0/sType/Dimensions#'>
            <mwg-rs:Regions rdf:parseType='Resource'>
                <mwg-rs:AppliedToDimensions rdf:parseType='Resource'>
                    <stDim:h>3648</stDim:h>
                    <stDim:unit>pixel</stDim:unit>
                    <stDim:w>4441</stDim:w>
                </mwg-rs:AppliedToDimensions>
                <mwg-rs:RegionList>
                    <rdf:Bag>
                        <rdf:li rdf:parseType='Resource'>
                            <mwg-rs:Area rdf:parseType='Resource'>
                                <stArea:h>0.109923</stArea:h>
                                <stArea:unit>normalized</stArea:unit>
                                <stArea:w>0.111011</stArea:w>
                                <stArea:x>0.219883</stArea:x>
                                <stArea:y>0.0620888</stArea:y>
                            </mwg-rs:Area>
                            <mwg-rs:Name>Betsy Loar</mwg-rs:Name>
                            <mwg-rs:Type>Face</mwg-rs:Type>
                        </rdf:li>
                        <rdf:li rdf:parseType='Resource'>
                            <mwg-rs:Name>Graham Canzanello</mwg-rs:Name>
                            <mwg-rs:Type>Face</mwg-rs:Type>
                        </rdf:li>
                        <rdf:li rdf:parseType='Resource'>
                            <mwg-rs:Name>Connor Canzanello</mwg-rs:Name>
                            <mwg-rs:Type>Face</mwg-rs:Type>
                        </rdf:li>
                        <rdf:li rdf:parseType='Resource'>
                            <mwg-rs:Area rdf:parseType='Resource'>
                                <stArea:h>0.095943</stArea:h>
                                <stArea:unit>normalized</stArea:unit>
                                <stArea:w>0.0693537</stArea:w>
                                <stArea:x>0.152668</stArea:x>
                                <stArea:y>0.234649</stArea:y>
                            </mwg-rs:Area>
                        </rdf:li>
                        <rdf:li rdf:parseType='Resource'>
                            <mwg-rs:Area rdf:parseType='Resource'>
                                <stArea:h>0.106086</stArea:h>
                                <stArea:unit>normalized</stArea:unit>
                                <stArea:w>0.0680027</stArea:w>
                                <stArea:x>0.52916</stArea:x>
                                <stArea:y>0.482593</stArea:y>
                            </mwg-rs:Area>
                        </rdf:li>
                    </rdf:Bag>
                </mwg-rs:RegionList>
            </mwg-rs:Regions>
        </rdf:Description>


Note that Betsy's mwg-rs:Name and mwg-rs:Type tags are associated with the rdf:li containing her mwg-rs:Area.  Graham's and Connor's are in separate rdf:li, apart from their mwg-rs:Area.  In digiKam only Betsy's face rectangle is displayed, so I moved Graham's and Connor's into the rdf:li containing their mwg-rs:Area and voila, digiKam displayed their rectangles as well.

I have no idea why this is happening.  Does the sequence of calls to SetNewValue matter?  I definitely set the region name and type apart from XMP:RegionRectangle.  Here is the code where all of that is happening (sorry for any atrocious Perl):


    if (defined $$info{RegionName}) {
        my @regionRect = [];
        if (ref $$info{RegionName} eq 'ARRAY') {  # more than one face in picture
            my @regionName = [];
            my @regionType = [];
            my @regionAreaH = [];
            my @regionAreaW = [];
            my @regionAreaX = [];
            my @regionAreaY = [];
            @regionName = uniq(@{$$info{RegionName}});
            # if RegionName exists, assume corresponding RegionType exists as well
            @regionType = @{$$info{RegionType}};
            if (@regionName < @{$$info{RegionName}}) {
                splice @regionType, 1, @{$$info{RegionName}} - @regionName;
            }
            # fill in RegionRectangle coordinates
            @regionAreaH = @{$$info{RegionAreaH}};
            @regionAreaW = @{$$info{RegionAreaW}};
            @regionAreaX = @{$$info{RegionAreaX}};
            @regionAreaY = @{$$info{RegionAreaY}};
            for my $i (0 .. $#regionAreaX)
            {
                push(@regionRect, $regionAreaX[$i]);
                push(@regionRect, $regionAreaY[$i]);
                push(@regionRect, $regionAreaW[$i]);
                push(@regionRect, $regionAreaH[$i]);
            }
            # use the regionName as the basis for Keywords, Subject, etc.
            ($success, $errStr) = $exifTool->SetNewValue('XMP:RegionName' =>
                                                         \@regionName,
                                                         Replace => 1) ||
                                  errExit($file, $errStr);
            ($success, $errStr) = $exifTool->SetNewValue('XMP:RegionType' =>
                                                         \@regionType,
                                                         Replace => 1) ||
                                  errExit($file, $errStr);
            ($success, $errStr) = $exifTool->SetNewValue('IPTC:Keywords' =>
                                                         \@regionName,
                                                         Replace => 1) ||
                                  errExit($file, $errStr);
            ($success, $errStr) = $exifTool->SetNewValue('XMP-dc:Subject' =>
                                                         \@regionName,
                                                         Replace => 1) ||
                                  errExit($file, $errStr);
            ($success, $errStr) = $exifTool->SetNewValue('XMP-iptcExt:PersonInImage' =>
                                                         \@regionName,
                                                         Replace => 1) ||
                                  errExit($file, $errStr);
            ($success, $errStr) = $exifTool->SetNewValue('XMP:RegionPersonDisplayName' =>
                                                         \@regionName,
                                                         Replace => 1) ||
                                  errExit($file, $errStr);
        }
        else {  #only one face in picture
            my $regionName = $$info{RegionName};
            my $regionType = $$info{RegionType};
            my $regionAreaH = $$info{RegionAreaH};
            my $regionAreaW = $$info{RegionAreaW};
            my $regionAreaX = $$info{RegionAreaX};
            my $regionAreaY = $$info{RegionAreaY};

            push(@regionRect, $regionAreaX);
            push(@regionRect, $regionAreaY);
            push(@regionRect, $regionAreaW);
            push(@regionRect, $regionAreaH);

            ($success, $errStr) = $exifTool->SetNewValue('XMP-mwg-rs:RegionList' => {
                                                  'Name' => $regionName,
                                                  'Type' => $regionType
                                              },
                                              Replace => 1) ||
                                  errExit($file, $errStr);
            ($success, $errStr) = $exifTool->SetNewValue('XMP-mwg-kw:HierarchicalKeywords' => {
                                                  'Keyword' => $regionName
                                              },
                                              Replace => 1) ||
                                  errExit($file, $errStr);
            ($success, $errStr) = $exifTool->SetNewValue('XMP-dc:Subject' => $regionName,
                                              Replace => 1) ||
                                  errExit($file, $errStr);
            ($success, $errStr) = $exifTool->SetNewValue('XMP-iptcExt:PersonInImageWDetails' => {
                                                  'PersonName' => $regionName
                                              },
                                              Replace => 1) ||
                                  errExit($file, $errStr);
            ($success, $errStr) = $exifTool->SetNewValue('XMP:RegionPersonDisplayName' =>
                                              $regionName,
                                              Replace => 1) ||
                                  errExit($file, $errStr);
        }
        ($success, $errStr) = $exifTool->SetNewValue('XMP:RegionRectangle' =>
                                                     \@regionRect,
                                                     Replace => 1) ||
                              errExit($file, $errStr);
    }


Phil Harvey

Quote from: Tom Cunningham on September 28, 2016, 03:59:16 PM
Note that Betsy's mwg-rs:Name and mwg-rs:Type tags are associated with the rdf:li containing her mwg-rs:Area.  Graham's and Connor's are in separate rdf:li, apart from their mwg-rs:Area.

Ah yes.  I should have noticed this.  I don't have time to look at your code in detail right now, but if you are just copying the regions then you should use the ExifTool Struct option to copy as structures.  This will preserve the proper structure hierarchy.  See this page for some help with this.

- Phil
...where DIR is the name of a directory/folder containing the images.  On Mac/Linux/PowerShell, use single quotes (') instead of double quotes (") around arguments containing a dollar sign ($).

Tom Cunningham

I'm actually trying to replace the regions (or add them if they are not there).  I tried the following code:


            for my $i (0 .. $#regionName)
            {
                ($success, $errStr) = $exifTool->SetNewValue('XMP-mwg-rs:RegionList' => {
                                                                 'Name' => $regionName[$i],
                                                                 'Type' => $regionType[$i],
                                                                 'Area' => {
                                                                     'H'    => $regionAreaH[$i],
                                                                     'Unit' => "normalized",
                                                                     'W'    => $regionAreaW[$i],
                                                                     'X'    => $regionAreaX[$i],
                                                                     'Y'    => $regionAreaY[$i]
                                                                 }
                                                             },
                                                             Replace => 1) ||
                                      errExit($file, $errStr);
            }


This writes out only the last region in the list.  If I change "Replace" to "AddValue", I get two copies of each region.

Phil Harvey

You've got the hang of the structures.  Very good.

The Replace option replaces previously queued new values, which is not what you want.  To replace an existing region, then you need to use DelValue to delete the old region, then AddValue to add back a new one, like this:

for my $i (0 .. $#regionName)
    {
        ($success, $errStr) = $exifTool->SetNewValue('XMP-mwg-rs:RegionList' => {
                                                         'Name' => $regionName[$i],
                                                     },
                                                     DelValue => 1) ||
                              errExit($file, $errStr);
        ($success, $errStr) = $exifTool->SetNewValue('XMP-mwg-rs:RegionList' => {
                                                         'Name' => $regionName[$i],
                                                         'Type' => $regionType[$i],
                                                         'Area' => {
                                                             'H'    => $regionAreaH[$i],
                                                             'Unit' => "normalized",
                                                             'W'    => $regionAreaW[$i],
                                                             'X'    => $regionAreaX[$i],
                                                             'Y'    => $regionAreaY[$i]
                                                         }
                                                     },
                                                     AddValue => 1) ||
                              errExit($file, $errStr);
    }


- Phil
...where DIR is the name of a directory/folder containing the images.  On Mac/Linux/PowerShell, use single quotes (') instead of double quotes (") around arguments containing a dollar sign ($).

Tom Cunningham

DigiKam is happy now, thanks again, Phil.  8)  Concerning this statement:


        ($success, $errStr) = $exifTool->SetNewValue('XMP-mwg-rs:RegionList' => {
                                                         'Name' => $regionName[$i],
                                                     },
                                                     DelValue => 1) ||
                              errExit($file, $errStr);


Does this delete the entire structure associated with $regionName[$i] (e.g. Name, Type, and Area), or is it only deleting the 'Name' tag?

I'm still kinda confused about Replace.  In most cases I use ImageInfo to retrieve all the tags for an image, SetNewValue with Replace=1 to add or modify the tag value, followed by a WriteInfo.  This seems to work with flattened tags but clearly doesn't with structured ones.  Is there a general, reliable, single operation that will 1) add a tag value if it doesn't exist in the image info and 2) replace the tag value if it does exist?

Phil Harvey

Quote from: Tom Cunningham on September 29, 2016, 10:32:57 AM
Does this delete the entire structure associated with $regionName[$i] (e.g. Name, Type, and Area)

Yes.  This is explained in the Deleting section of the Structured tag page.

QuoteI'm still kinda confused about Replace.

Setting Replace=>1 overrides values you assigned in previous calls to SetNewValue() to be replaced.

This writes 2 keywords:
my $et = new Image::ExifTool;
$et->SetNewValue(Keywords => 'one');
$et->SetNewValue(Keywords => 'two');
$et->WriteInfo($file);


This writes only 1 keyword ("two"):
my $et = new Image::ExifTool;
$et->SetNewValue(Keywords => 'one');
$et->SetNewValue(Keywords => 'two', Replace => 1);
$et->WriteInfo($file);


Both of the above examples will delete any keywords that already existed in the file.

QuoteIs there a general, reliable, single operation that will 1) add a tag value if it doesn't exist in the image info and 2) replace the tag value if it does exist?

I don't know if I understand exactly what you want.  For list-type tags you should use DelValue then AddValue as in my last post.  For non-list-type tags you should do an additional DelValue call with an empty value to trigger a conditional write in the case where the tag doesn't previously exist.  eg)

$et->SetNewValue(Description => '', DelValue => 1);
$et->SetNewValue(Description => 'old value', DelValue => 1);
$et->SetNewValue(Description => 'new value');


This will write "new value" to the tag if it didn't exist before or had the value "old value".

This set of calls would also work for list-type tags since deleting a null list entry won't have any effect.

- Phil
...where DIR is the name of a directory/folder containing the images.  On Mac/Linux/PowerShell, use single quotes (') instead of double quotes (") around arguments containing a dollar sign ($).

Tom Cunningham

Quote

$et->SetNewValue(Description => '', DelValue => 1);
$et->SetNewValue(Description => 'old value', DelValue => 1);
$et->SetNewValue(Description => 'new value');


This will write "new value" to the tag if it didn't exist before or had the value "old value".

What if Description has an 'old value' but you don't know or care what it is?  Is it incumbent to check whether Description is defined, and if so, fetch the value before executing the second statement above?  Sorry I'm being so thick.  :(

Phil Harvey

Sorry, I don't know exactly what you want.  99% of the time you can avoid having to make access the image file twice.

If you don't care what the tag value was before, then just calling SetNewValue once normally (ie. no DelValue) is what you should do.

- Phil
...where DIR is the name of a directory/folder containing the images.  On Mac/Linux/PowerShell, use single quotes (') instead of double quotes (") around arguments containing a dollar sign ($).