#------------------------------------------------------------------------------
# File:         CaptureOne.pm
#
# Description:  Read Capture One EIP and COS files
#
# Revisions:    2009/11/01 - P. Harvey Created
#
# Notes:        The EIP format is a ZIP file containing an image (IIQ or TIFF)
#               and some settings files (COS).  COS files are XML based.
#------------------------------------------------------------------------------

package Image::ExifTool::CaptureOne;

use strict;
use vars qw($VERSION);
use Image::ExifTool qw(:DataAccess :Utils);
use Image::ExifTool::XMP;
use Image::ExifTool::ZIP;

$VERSION = '1.05';

# CaptureOne COS XML tags
# - tags are added dynamically when encountered
# - this table is not listed in tag name docs
%Image::ExifTool::CaptureOne::Main = (
    GROUPS => { 0 => 'XML', 1 => 'XML', 2 => 'Image' },
    PROCESS_PROC => \&Image::ExifTool::XMP::ProcessXMP,
    VARS => { ID_FMT => 'none' },
    ColorCorrections => { ValueConv => '\$val', Hidden => 1 }, # (long list of floating point numbers)
);

#------------------------------------------------------------------------------
# We found an XMP property name/value
# Inputs: 0) attribute list ref, 1) attr hash ref,
#         2) property name ref, 3) property value ref
# Returns: true if value was changed
sub HandleCOSAttrs($$$$)
{
    my ($attrList, $attrs, $prop, $valPt) = @_;
    my $changed;
    if (not length $$valPt and defined $$attrs{K} and defined $$attrs{V}) {
        $$prop = $$attrs{K};
        $$valPt = $$attrs{V};
        # remove these attributes from the list
        my @attrs = @$attrList;
        @$attrList = ( );
        my $a;
        foreach $a (@attrs) {
            if ($a eq 'K' or $a eq 'V') {
                delete $$attrs{$a};
            } else {
                push @$attrList, $a;
            }
        }
        $changed = 1;
    }
    return $changed;
}

#------------------------------------------------------------------------------
# We found a COS property name/value
# Inputs: 0) ExifTool object ref, 1) tag table ref
#         2) reference to array of XMP property names (last is current property)
#         3) property value, 4) attribute hash ref (not used here)
# Returns: 1 if valid tag was found
sub FoundCOS($$$$;$)
{
    my ($et, $tagTablePtr, $props, $val, $attrs) = @_;

    my $tag = $$props[-1];
    unless ($$tagTablePtr{$tag}) {
        $et->VPrint(0, "  | [adding $tag]\n");
        my $name = ucfirst $tag;
        $name =~ tr/-_a-zA-Z0-9//dc;
        return 0 unless length $tag;
        my %tagInfo = ( Name => $tag );
        # try formatting any tag with "Date" in the name as a date
        # (shouldn't affect non-date tags)
        if ($name =~ /Date(?![a-z])/) {
            $tagInfo{Groups} = { 2 => 'Time' };
            $tagInfo{ValueConv} = 'Image::ExifTool::XMP::ConvertXMPDate($val,1)';
            $tagInfo{PrintConv} = '$self->ConvertDateTime($val)';
        }
        AddTagToTable($tagTablePtr, $tag, \%tagInfo);
    }
    # convert from UTF8 to ExifTool Charset
    $val = $et->Decode($val, "UTF8");
    # un-escape XML character entities
    $val = Image::ExifTool::XMP::UnescapeXML($val);
    $et->HandleTag($tagTablePtr, $tag, $val);
    return 0;
}

#------------------------------------------------------------------------------
# Extract information from a COS file
# Inputs: 0) ExifTool object reference, 1) dirInfo reference
# Returns: 1 on success, 0 if this wasn't a valid XML file
sub ProcessCOS($$)
{
    my ($et, $dirInfo) = @_;

    # process using XMP module, but override handling of attributes and tags
    $$dirInfo{XMPParseOpts} = {
        AttrProc => \&HandleCOSAttrs,
        FoundProc => \&FoundCOS,
    };
    my $tagTablePtr = GetTagTable('Image::ExifTool::CaptureOne::Main');
    my $success = $et->ProcessDirectory($dirInfo, $tagTablePtr);
    delete $$dirInfo{XMLParseArgs};
    return $success;
}

#------------------------------------------------------------------------------
# Extract information from a CaptureOne EIP file
# Inputs: 0) ExifTool object reference, 1) dirInfo reference
# Returns: 1
# Notes: Upon entry to this routine, the file type has already been verified
# and the dirInfo hash contains a ZIP element unique to this process proc:
#   ZIP     - reference to Archive::Zip object for this file
sub ProcessEIP($$)
{
    my ($et, $dirInfo) = @_;
    my $zip = $$dirInfo{ZIP};
    my ($file, $buff, $status, $member, %parseFile);

    $et->SetFileType('EIP');

    # must catch all Archive::Zip warnings
    local $SIG{'__WARN__'} = \&Image::ExifTool::ZIP::WarnProc;
    # find all manifest files
    my @members = $zip->membersMatching('^manifest\d*.xml$');
    # and choose the one with the highest version number (any better ideas?)
    while (@members) {
        my $m = shift @members;
        my $f = $m->fileName();
        next if $file and $file gt $f;
        $member = $m;
        $file = $f;
    }
    # get file names from our chosen manifest file
    if ($member) {
        ($buff, $status) = $zip->contents($member);
        if (not $status) {
            my $foundImage;
            while ($buff =~ m{<(RawPath|SettingsPath)>(.*?)</\1>}sg) {
                $file = $2;
                next unless $file =~ /\.(cos|iiq|jpe?g|tiff?)$/i;
                $parseFile{$file} = 1;    # set flag to parse this file
                $foundImage = 1 unless $file =~ /\.cos$/i;
            }
            # ignore manifest unless it contained a valid image
            undef %parseFile unless $foundImage;
        }
    }
    # extract meta information from embedded files
    my $docNum = 0;
    @members = $zip->members(); # get all members
    foreach $member (@members) {
        # get filename of this ZIP member
        $file = $member->fileName();
        next unless defined $file;
        $et->VPrint(0, "File: $file\n");
        # set the document number and extract ZIP tags
        $$et{DOC_NUM} = ++$docNum;
        Image::ExifTool::ZIP::HandleMember($et, $member);
        if (%parseFile) {
            next unless $parseFile{$file};
        } else {
            # reading the manifest didn't work, so look for image files in the
            # root directory and .cos files in the CaptureOne directory
            next unless $file =~ m{^([^/]+\.(iiq|jpe?g|tiff?)|CaptureOne/.*\.cos)$}i;
        }
        # extract the contents of the file
        # Note: this could use a LOT of memory here for RAW images...
        ($buff, $status) = $zip->contents($member);
        $status and $et->Warn("Error extracting $file"), next;
        if ($file =~ /\.cos$/i) {
            # process Capture One Settings files
            my %dirInfo = (
                DataPt => \$buff,
                DirLen => length $buff,
                DataLen => length $buff,
            );
            ProcessCOS($et, \%dirInfo);
        } else {
            # set HtmlDump error if necessary because it doesn't work with embedded files
            if ($$et{HTML_DUMP}) {
                $$et{HTML_DUMP}{Error} = "Sorry, can't dump images embedded in ZIP files";
            }
            # process IIQ, JPEG and TIFF images
            $et->ExtractInfo(\$buff, { ReEntry => 1 });
        }
        undef $buff;    # (free memory now)
    }
    delete $$et{DOC_NUM};
    return 1;
}

1;  # end

__END__

=head1 NAME

Image::ExifTool::CaptureOne - Read Capture One EIP and COS files

=head1 SYNOPSIS

This module is used by Image::ExifTool

=head1 DESCRIPTION

This module contains definitions required by Image::ExifTool to extract meta
information from Capture One EIP (Enhanced Image Package) and COS (Capture
One Settings) files.

=head1 NOTES

The EIP format is a ZIP file containing an image (IIQ or TIFF) and some
settings files (COS).

=head1 AUTHOR

Copyright 2003-2026, Phil Harvey (philharvey66 at gmail.com)

This library is free software; you can redistribute it and/or modify it
under the same terms as Perl itself.

=head1 SEE ALSO

L<Image::ExifTool::TagNames/ZIP Tags>,
L<Image::ExifTool(3pm)|Image::ExifTool>

=cut

