2022-03-09 10:56:50 +00:00
|
|
|
{ Implements access to XMP metadata.
|
|
|
|
Reads the XMP segment into an internal string field.
|
|
|
|
Use LoadFromStream and SaveToStream methods to read or write this field.
|
|
|
|
There is also a simple "tag-like" interface, however, it is implemented only
|
|
|
|
for reading the meta data.
|
|
|
|
|
|
|
|
File good for testing:
|
|
|
|
https://commons.wikimedia.org/wiki/File:Metadata_test_file_-_includes_data_in_IIM,_XMP,_and_Exif.jpg
|
|
|
|
}
|
|
|
|
|
2022-03-07 12:10:52 +00:00
|
|
|
unit fpeXMPData;
|
|
|
|
|
2023-10-28 23:37:19 +00:00
|
|
|
{$IFDEF FPC}
|
2022-03-07 12:10:52 +00:00
|
|
|
{$mode objfpc}{$H+}
|
2023-10-28 23:37:19 +00:00
|
|
|
{$ENDIF}
|
2022-03-07 12:10:52 +00:00
|
|
|
|
|
|
|
interface
|
|
|
|
|
|
|
|
uses
|
|
|
|
Classes, SysUtils, contnrs,
|
|
|
|
{$IFDEF FPC}
|
|
|
|
laz2_dom, laz2_xmlread,
|
2023-11-06 16:42:17 +00:00
|
|
|
{$ELSE}
|
|
|
|
XMLDoc, XMLIntf,
|
2022-03-07 12:10:52 +00:00
|
|
|
{$ENDIF}
|
|
|
|
fpeGlobal, fpeTags;
|
|
|
|
|
|
|
|
type
|
|
|
|
TXMPData = class
|
|
|
|
private
|
2023-11-06 16:42:17 +00:00
|
|
|
FData: AnsiString;
|
|
|
|
{$IFDEF FPC}
|
2022-03-07 12:10:52 +00:00
|
|
|
FDoc: TXMLDocument;
|
2023-11-06 16:42:17 +00:00
|
|
|
{$ELSE}
|
|
|
|
FDoc: IXMLDocument;
|
|
|
|
{$ENDIF}
|
2022-03-07 12:10:52 +00:00
|
|
|
FTags: TStringList;
|
|
|
|
function GetTagByIndex(AIndex: Integer): String;
|
|
|
|
function GetTagByName(ATagName: String): String;
|
2022-03-07 21:03:56 +00:00
|
|
|
function GetTagName(AIndex: Integer): String;
|
2022-03-07 12:10:52 +00:00
|
|
|
function GetTagCount: Integer;
|
|
|
|
protected
|
2023-11-06 16:42:17 +00:00
|
|
|
procedure Create_RDFDescription_Tags(ANode: {$IFDEF FPC}TDOMNode{$ELSE}IXMLNode{$ENDIF});
|
2022-03-07 12:10:52 +00:00
|
|
|
procedure CreateTags;
|
|
|
|
public
|
|
|
|
constructor Create;
|
|
|
|
destructor Destroy; override;
|
2023-11-08 17:56:29 +00:00
|
|
|
procedure ExportToStrings(AList: TStrings; AOptions: TExportOptions; ASeparator: String = '=');
|
2022-03-07 21:03:56 +00:00
|
|
|
procedure LoadFromStream(AStream: TStream; ASize: Integer = -1);
|
2022-03-07 12:10:52 +00:00
|
|
|
procedure SaveToStream(AStream: TStream);
|
|
|
|
property TagByIndex[AIndex: Integer]: String read GetTagByIndex;
|
|
|
|
property TagByName[ATagName: String]: String read GetTagByName;
|
2022-03-07 21:03:56 +00:00
|
|
|
property TagName[AIndex: Integer]: String read GetTagName;
|
2022-03-07 12:10:52 +00:00
|
|
|
property TagCount: Integer read GetTagCount;
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
|
|
implementation
|
|
|
|
|
|
|
|
{ TXMPData }
|
|
|
|
|
|
|
|
constructor TXMPData.Create;
|
|
|
|
begin
|
|
|
|
inherited;
|
2022-03-07 21:03:56 +00:00
|
|
|
FTags := TStringList.Create;
|
2022-03-07 12:10:52 +00:00
|
|
|
end;
|
|
|
|
|
|
|
|
destructor TXMPData.Destroy;
|
|
|
|
begin
|
|
|
|
{$IFDEF FPC}
|
|
|
|
FDoc.Free;
|
|
|
|
{$ENDIF}
|
|
|
|
FTags.Free;
|
|
|
|
inherited;
|
|
|
|
end;
|
|
|
|
|
2023-11-06 16:42:17 +00:00
|
|
|
procedure TXMPData.Create_RDFDescription_Tags(ANode: {$IFDEF FPC}TDOMNode{$ELSE}IXMLNode{$ENDIF});
|
2022-03-07 12:10:52 +00:00
|
|
|
var
|
2023-11-06 16:42:17 +00:00
|
|
|
{$IFDEF FPC}
|
2022-03-07 12:10:52 +00:00
|
|
|
node: TDOMNode;
|
|
|
|
attr: TDOMNode;
|
2023-11-06 16:42:17 +00:00
|
|
|
{$ELSE}
|
|
|
|
node: IXMLNode;
|
|
|
|
attr: IXMLNode;
|
|
|
|
{$ENDIF}
|
|
|
|
nodeName: String;
|
2023-11-08 17:56:29 +00:00
|
|
|
i: Integer;
|
2022-03-07 21:03:56 +00:00
|
|
|
lTagName, lTagValue: String;
|
2022-03-07 12:10:52 +00:00
|
|
|
begin
|
|
|
|
while ANode <> nil do begin
|
|
|
|
nodeName := ANode.NodeName;
|
2023-11-06 16:42:17 +00:00
|
|
|
{$IFDEF FPC}
|
2022-03-07 12:10:52 +00:00
|
|
|
for i := 0 to ANode.Attributes.Length-1 do
|
|
|
|
begin
|
|
|
|
attr := ANode.Attributes.Item[i];
|
2022-03-07 21:03:56 +00:00
|
|
|
lTagName := attr.NodeName;
|
2023-11-06 16:42:17 +00:00
|
|
|
lTagValue := attr.nodeValue;
|
2022-03-07 21:03:56 +00:00
|
|
|
FTags.Add(lTagName + '=' + lTagValue);
|
2022-03-07 12:10:52 +00:00
|
|
|
end;
|
|
|
|
if ANode.HasChildNodes then
|
|
|
|
begin
|
|
|
|
node := ANode.FirstChild;
|
2023-11-06 16:42:17 +00:00
|
|
|
while node <> nil do
|
|
|
|
begin
|
|
|
|
nodeName := node.NodeName;
|
|
|
|
{$IFDEF FPC}
|
|
|
|
lTagValue := node.TextContent;
|
|
|
|
{$ELSE}
|
|
|
|
lTagValue := node.NodeValue;
|
|
|
|
{$ENDIF}
|
|
|
|
if lTagName <> '' then
|
|
|
|
FTags.Add(nodeName + '=' + lTagValue);
|
|
|
|
node := node.NextSibling;
|
|
|
|
end;
|
|
|
|
end;
|
|
|
|
ANode := ANode.NextSibling;
|
|
|
|
{$ELSE}
|
|
|
|
for i := 0 to ANode.AttributeNodes.Count-1 do
|
|
|
|
begin
|
|
|
|
attr := ANode.AttributeNodes[i];
|
|
|
|
lTagName := attr.NodeName;
|
|
|
|
lTagValue := attr.NodeValue;
|
|
|
|
FTags.Add(lTagName + '=' + lTagValue);
|
|
|
|
end;
|
|
|
|
if ANode.HasChildNodes then
|
|
|
|
begin
|
|
|
|
node := ANode.ChildNodes.First;
|
2022-03-07 12:10:52 +00:00
|
|
|
while node <> nil do
|
|
|
|
begin
|
|
|
|
nodeName := node.NodeName;
|
2023-11-06 16:42:17 +00:00
|
|
|
{$IFDEF FPC}
|
2022-03-07 21:03:56 +00:00
|
|
|
lTagValue := node.TextContent;
|
2023-11-06 16:42:17 +00:00
|
|
|
{$ELSE}
|
|
|
|
lTagValue := node.NodeValue;
|
|
|
|
{$ENDIF}
|
2022-03-07 21:03:56 +00:00
|
|
|
if lTagName <> '' then
|
|
|
|
FTags.Add(nodeName + '=' + lTagValue);
|
2022-03-07 12:10:52 +00:00
|
|
|
node := node.NextSibling;
|
|
|
|
end;
|
|
|
|
end;
|
|
|
|
ANode := ANode.NextSibling;
|
2023-11-06 16:42:17 +00:00
|
|
|
{$ENDIF}
|
2022-03-07 12:10:52 +00:00
|
|
|
end;
|
|
|
|
end;
|
|
|
|
|
|
|
|
procedure TXMPData.CreateTags;
|
|
|
|
var
|
2023-11-06 16:42:17 +00:00
|
|
|
{$IFDEF FPC}
|
2022-03-07 12:10:52 +00:00
|
|
|
node: TDOMNode;
|
2023-11-06 16:42:17 +00:00
|
|
|
{$ELSE}
|
|
|
|
node: IXMLNode;
|
|
|
|
{$ENDIF}
|
2022-03-07 12:10:52 +00:00
|
|
|
nodeName: String;
|
2023-11-06 16:42:17 +00:00
|
|
|
stream: TStringStream;
|
2022-03-07 12:10:52 +00:00
|
|
|
begin
|
2023-11-06 16:42:17 +00:00
|
|
|
{$IFDEF FPC}
|
2022-03-07 12:10:52 +00:00
|
|
|
FDoc.Free;
|
2023-11-06 16:42:17 +00:00
|
|
|
{$ENDIF}
|
2022-03-07 12:10:52 +00:00
|
|
|
stream := TStringStream.Create(FData);
|
|
|
|
try
|
2023-11-06 16:42:17 +00:00
|
|
|
{$IFDEF FPC}
|
2022-03-07 12:10:52 +00:00
|
|
|
ReadXMLFile(FDoc, stream);
|
2023-11-06 16:42:17 +00:00
|
|
|
{$ELSE}
|
|
|
|
FDoc := TXMLDocument.Create(nil);
|
|
|
|
FDoc.Options := FDoc.Options - [doNodeAutoCreate];
|
|
|
|
FDoc.LoadFromStream(stream, xetUTF_8);
|
|
|
|
{$ENDIF}
|
2022-03-07 12:10:52 +00:00
|
|
|
finally
|
|
|
|
stream.Free;
|
|
|
|
end;
|
|
|
|
|
|
|
|
FTags.Clear;
|
|
|
|
try
|
2023-11-06 16:42:17 +00:00
|
|
|
{$IFDEF FPC}
|
2022-03-07 12:10:52 +00:00
|
|
|
node := FDoc.DocumentElement.FindNode('rdf:RDF');
|
|
|
|
if node = nil then exit;
|
|
|
|
node := node.FirstChild;
|
2023-11-06 16:42:17 +00:00
|
|
|
{$ELSE}
|
|
|
|
|
|
|
|
node := FDoc.DocumentElement;
|
|
|
|
if node.ChildNodes.Count = 0 then
|
|
|
|
exit;
|
|
|
|
node := node.ChildNodes.First;
|
|
|
|
nodename :=node.NodeName;
|
|
|
|
if nodeName <> 'rdf:RDF' then
|
|
|
|
exit;
|
|
|
|
node := node.ChildNodes.First;
|
|
|
|
{$ENDIF}
|
|
|
|
while node <> nil do
|
2022-03-07 12:10:52 +00:00
|
|
|
begin
|
|
|
|
nodeName := node.NodeName;
|
|
|
|
if nodeName = 'rdf:Description' then
|
|
|
|
Create_RDFDescription_Tags(node);
|
|
|
|
node := node.NextSibling;
|
|
|
|
end;
|
|
|
|
except
|
|
|
|
FTags.Clear;
|
2023-11-06 16:42:17 +00:00
|
|
|
{$IFDEF FPC}
|
2022-03-07 12:10:52 +00:00
|
|
|
FreeAndNil(FDoc);
|
2023-11-06 16:42:17 +00:00
|
|
|
{$ENDIF}
|
2022-03-07 12:10:52 +00:00
|
|
|
raise;
|
|
|
|
end;
|
|
|
|
end;
|
|
|
|
|
2023-11-08 17:56:29 +00:00
|
|
|
procedure TXMPData.ExportToStrings(AList: TStrings; AOptions: TExportOptions;
|
|
|
|
ASeparator: String = '=');
|
|
|
|
var
|
|
|
|
i: Integer;
|
|
|
|
tagnam: String;
|
|
|
|
tagval: String;
|
|
|
|
usedExportOptions: TExportOptions;
|
|
|
|
begin
|
|
|
|
Assert(AList <> nil);
|
|
|
|
|
|
|
|
if TagCount = 0 then
|
|
|
|
exit;
|
|
|
|
|
|
|
|
if AList.Count > 0 then
|
|
|
|
AList.Add('');
|
|
|
|
AList.Add('*** XMP ***');
|
|
|
|
|
|
|
|
for i := 0 to TagCount-1 do begin
|
|
|
|
tagNam := TagName[i];
|
|
|
|
tagVal := TagByIndex[i];
|
|
|
|
{
|
|
|
|
usedExportOptions := AOptions * [eoShowDecimalTagID, eoShowHexTagID];
|
|
|
|
if usedExportOptions = [eoShowDecimalTagID] then
|
|
|
|
nam := Format('[%d] %s', [tag.TagID, tag.Description])
|
|
|
|
else
|
|
|
|
if usedExportOptions = [eoShowHexTagID] then
|
|
|
|
nam := Format('[$%.4x] %s', [tag.TagID, tag.Description])
|
|
|
|
else
|
|
|
|
nam := tag.Description;
|
|
|
|
tagval := tag.AsString;
|
|
|
|
}
|
|
|
|
if tagval <> '' then
|
|
|
|
AList.Add(tagnam + ASeparator + tagval);
|
|
|
|
end;
|
|
|
|
end;
|
|
|
|
|
2022-03-07 12:10:52 +00:00
|
|
|
function TXMPData.GetTagByIndex(AIndex: Integer): String;
|
|
|
|
begin
|
2022-03-09 10:56:50 +00:00
|
|
|
Result := FTags.ValueFromIndex[AIndex];
|
2022-03-07 12:10:52 +00:00
|
|
|
end;
|
|
|
|
|
|
|
|
function TXMPData.GetTagByName(ATagName: String): String;
|
|
|
|
begin
|
|
|
|
Result := FTags.Values[ATagName];;
|
|
|
|
end;
|
|
|
|
|
|
|
|
function TXMPData.GetTagCount: Integer;
|
|
|
|
begin
|
|
|
|
Result := FTags.Count;
|
|
|
|
end;
|
|
|
|
|
2022-03-07 21:03:56 +00:00
|
|
|
function TXMPData.GetTagName(AIndex: Integer): String;
|
|
|
|
begin
|
|
|
|
Result := FTags.Names[AIndex];
|
|
|
|
end;
|
|
|
|
|
|
|
|
procedure TXMPData.LoadFromStream(AStream: TStream; ASize: Integer = -1);
|
2022-03-07 12:10:52 +00:00
|
|
|
var
|
|
|
|
p: Int64;
|
2023-11-06 16:42:17 +00:00
|
|
|
i: Cardinal;
|
2022-03-07 12:10:52 +00:00
|
|
|
begin
|
|
|
|
if ASize = -1 then
|
|
|
|
ASize := AStream.Size;
|
|
|
|
SetLength(FData, ASize);
|
|
|
|
p := AStream.Position;
|
|
|
|
AStream.Read(FData[1], ASize);
|
2023-10-28 23:14:27 +00:00
|
|
|
|
|
|
|
// Sometimes there are incomplete xml files, missing the initial '<'.
|
|
|
|
// https://superuser.com/questions/1389971/error-0x80070057-the-parameter-is-incorrect-when-editing-jpeg-metadata
|
|
|
|
// Fixing this is better than rejecting the file...
|
|
|
|
if pos('?xpacket', FData) = 1 then
|
|
|
|
begin
|
|
|
|
SetLength(FData, ASize+1);
|
|
|
|
for i := ASize downto 1 do
|
|
|
|
FData[i+1] := FData[i];
|
|
|
|
FData[1] := '<';
|
|
|
|
end;
|
|
|
|
|
2023-11-13 17:35:49 +00:00
|
|
|
// The xml parser does not like zero bytes at the end --> remove them
|
|
|
|
i := Length(FData);
|
|
|
|
while (FData[i] = #0) and (i > 0) do
|
|
|
|
begin
|
|
|
|
dec(i);
|
|
|
|
SetLength(FData, i);
|
|
|
|
end;
|
|
|
|
|
2022-03-07 12:10:52 +00:00
|
|
|
AStream.Position := p;
|
|
|
|
CreateTags;
|
|
|
|
end;
|
|
|
|
|
|
|
|
procedure TXMPData.SaveToStream(AStream: TStream);
|
|
|
|
begin
|
|
|
|
if FData <> '' then
|
|
|
|
AStream.Write(FData[1], Length(FData));
|
|
|
|
end;
|
|
|
|
|
|
|
|
end.
|
|
|
|
|