{ Reader and writer for IPTC data (Adobe image resource blocks) NOTE: Data is in Big-Endian format. Adobe Image Resource Block: -------------------------- https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577409_pgfId-1037504 Length Description ------ ---------------------------------------------------------------- 4 Signature: '8BIM' 2 Unique identifier for the resource. (var) Name: Pascal string, padded to make the size even (a null name consists of two bytes of 0) 4 Actual size of resource data that follows (var) The resource data, described in the sections on the individual resource types. It is padded to make the size even. The image resource block with unique identifier $0404 is the IPTC block. https://www.iptc.org/std/IIM/4.2/specification/IIMV4.2.pdf The IPTC block consists of several "records". Every "record" constists of several "datasets". Every "dataset" consists of a unique tag and a data field. There are two types of tags: "Standard" tag: - 1 byte: tag "marker" ($1C) - 1 byte: record number - 1 byte: dataset number - 2 bytes: datafield byte count "Extended" tag (probably not used): - 1 byte: tag "marker" ($1C) - 1 byte: record number - 1 byte: dataset number - 2 bytes: count of bytes (n) used in "datafield byte count" field (always has highest bit set) - n bytes: datafield byte count } unit fpeIptcReadWrite; {$IFDEF FPC} //{$MODE objfpc}{$H+} {$MODE DELPHI} {$ENDIF} interface uses Classes, SysUtils, fpeGlobal, fpeUtils, fpeTags, fpeMetadata, fpeIptcData; type { TIPTCReader } TIPTCReader = class(TBasicMetadataReader) private function ExtractTag(const ABuffer: TBytes; var AStart: Integer): TTag; procedure ReadImageResourceBlock(AStream: TStream; out AID: Word; out AName: String; out AData: TBytes); protected public procedure ReadFromStream(AStream: TStream; AImgFormat: TImgFormat); override; procedure ReadIPTCData(const ABuffer: TBytes); end; { TIPTCWriter } TIPTCWriter = class(TBasicMetadataWriter) private FIPTCSegmentStartPos: Int64; protected procedure WriteEndOfDataResourceBlock(AStream: TStream); procedure WriteImageResourceBlockHeader(AStream: TStream; AResourceID: Integer; AResourceName: String); //; ABuffer: Pointer; ABufferSize: DWord); procedure WriteIPTCHeader(AStream: TStream); procedure WriteIPTCImageResourceBlock(AStream: TStream; AName: String); procedure WriteTag(AStream: TStream; ATag: TTag); overload; public constructor Create(AImgInfo: TImgInfo); override; procedure WriteToStream(AStream: TStream; AImgFormat: TImgFormat); override; end; implementation uses {$IFDEF FPC} lConvEncoding, {$ENDIF} fpeStrConsts; type // http://search.cpan.org/dist/Image-MetaData-JPEG/lib/Image/MetaData/JPEG/Structures.pod#Structure_of_an_IPTC_data_block TIptcTag = packed record TagMarker: Byte; // must be $1C RecordNumber: Byte; // this is the number before the colon in the tag listing DatasetNumber: Byte; // this is the number after the colon in the tag listing ("Tag") Size: Word; // Size of data if < 32768, otherwise size of datalength element // SizeOfDatasize: word --> if Size of data > 32767 // Data: variable end; const IPTC_SIGNATURE: ansistring = 'Photoshop 3.0'#0; RESOURCE_MARKER: ansistring = '8BIM'; IPTC_IMAGERESOURCEID = $0404; //------------------------------------------------------------------------------ // TIptcReader //------------------------------------------------------------------------------ function TIptcReader.ExtractTag(const ABuffer: TBytes; var AStart: Integer): TTag; var recordNo: Byte; datasetNo: Byte; len: DWord; tagdef: TTagDef; tagID: TTagID; s: String; w: Word; {$IFNDEF FPC} sa: ansistring; {$ENDIF} begin Result := nil; recordNo := ABuffer[AStart]; datasetNo := ABuffer[AStart+1]; len := BEtoN(PWord(@ABuffer[AStart+2])^); inc(AStart, 4); // Take care of highest bit which indicates an Extended Dataset if word(len) and $8000 <> 0 then begin len := word(len) and (not $8000); if len = 2 then begin len := BEtoN(PWord(@ABuffer[AStart])^); inc(AStart, 2); end else if len = 4 then begin len := BEtoN(PDWord(@ABuffer[AStart - len])^); inc(AStart, 4); end else Error(Format(rsIptcExtendedDataSizeNotSupported, [len])); end; if not (recordNo in [1, 2, 8]) then begin AStart := AStart + Integer(len); exit; end; tagID := (recordNo shl 8) or datasetNo or TAGPARENT_IPTC; tagdef := FindIPTCTagDef(tagID); if tagdef <> nil then begin Result := tagdef.TagClass.Create(tagdef, true); case tagdef.TagType of ttString: begin {$IFDEF FPC} SetLength(s, len); Move(ABuffer[AStart], s[1], len); s := ConvertEncoding(s, GuessEncoding(s), encodingUTF8); {$ELSE} SetLength(sa,len); Move(ABuffer[AStart], sa[1], len); s := UTF8Decode(sa); {$ENDIF} if Result is TIptcDateTag then with TIptcDateTag(Result) do begin FormatStr := IPTC_DATE_FORMAT; AsString := s; FormatStr := ''; end else if Result is TIptcTimeTag then with TIptcTimeTag(Result) do begin FormatStr := IPTC_TIME_FORMAT; AsString := s; FormatStr := ''; end else (Result as TStringTag).AsString := s; end; ttUInt16: begin w := BEtoN(PWord(@ABuffer[AStart])^); (Result as TIntegerTag).AsInteger := w; end; else Warning(Format(rsTagTypeNotSupported, [tagDef.Name])); end; end else begin // to do: create a dummy tag for the unknown tagdef end; AStart := AStart + Integer(len); end; procedure TIptcReader.ReadFromStream(AStream: TStream; AImgFormat: TImgFormat); const MARKER_SIZE = 4; var marker: packed array[1..MARKER_SIZE] of ansichar; lID: Word; // Image resoure ID lName: String; lData: TBytes; begin FImgFormat := AImgFormat; SetLength(lData, Length(IPTC_SIGNATURE)); // 'Photoshop 3.0' if AStream.Read(lData[0], Length(lData)) <> Length(lData) then begin Error(rsIncorrectFileStructure); exit; end; if not CompareMem(@lData[0], @IPTC_SIGNATURE[1], Length(lData)) then begin Error(rsNoValidIptcSignature); exit; end; while (AStream.Position < AStream.Size) do begin AStream.Read({%H-}marker[1], MARKER_SIZE); if AStream.Position >= AStream.Size then begin Error(rsIncorrectFileStructure); break; end; if not CompareMem(@marker[1], @RESOURCE_MARKER[1], MARKER_SIZE) then // '8BIM' break; ReadImageResourceBlock(AStream, lID, lName, lData); if lID = IPTC_IMAGERESOURCEID then begin // $0404 FImgInfo.IptcData.AddImageResourceBlock(lID, lName, nil); ReadIptcData(lData); end else FImgInfo.IptcData.AddImageResourceBlock(lID, lName, lData); end; end; procedure TIptcReader.ReadImageResourceBlock(AStream: TStream; out AID: Word; out AName: String; out AData: TBytes); var len: Byte; s: Ansistring; lSize: DWord; begin AID := BEtoN(ReadWord(AStream)); len := ReadByte(AStream); if len = 0 then begin ReadByte(AStream); AName := ''; end else begin SetLength(s, len); AStream.Read(s[1], len); if s[len] = #0 then SetLength(s, len-1); AName := s; end; lSize := BEToN(ReadDWord(AStream)); if lSize = 0 then exit; SetLength(AData, lSize); AStream.Read(AData[0], lSize); end; procedure TIptcReader.ReadIptcData(const ABuffer: TBytes); var tag, parentTag: TTag; start: Integer; begin FImgInfo.IptcData.Clear; if Length(ABuffer) = 0 then begin Error(rsIptcDataExpected); exit; end; start := 0; while (start < High(ABuffer) - 1) do begin if ABuffer[start] <> $1C then Error(rsNoValidIptcFile); inc(start); tag := ExtractTag(ABuffer, start); if tag is TIptcMultiStringTag then begin // if tag.Count = IPTC_MULTI_TAG_COUNT then begin parentTag := FImgInfo.IptcData.TagByID[tag.TagID]; if parentTag = nil then FImgInfo.IptcData.AddTag(tag) else begin FImgInfo.IptcData.AppendTagTo(tag, parentTag); tag.Free; end; end else FImgInfo.IptcData.AddTag(tag); end; end; //------------------------------------------------------------------------------ // TIptcWriter //------------------------------------------------------------------------------ constructor TIPTCWriter.Create(AImgInfo: TImgInfo); begin inherited; FIPTCSegmentStartPos := -1; end; procedure TIptcWriter.WriteEndOfDataResourceBlock(AStream: TStream); begin WriteImageResourceBlockHeader(AStream, $0B04, ''); //, nil, 0); end; //------------------------------------------------------------------------------ // Writes the IPTC header needed by JPEG files (Segment APP13 header) // Call WriteToStream immediately afterwards //------------------------------------------------------------------------------ procedure TIPTCWriter.WriteIPTCHeader(AStream: TStream); const SEGMENT_MARKER: array[0..1] of byte = ($FF, $ED); begin FIPTCSegmentStartPos := AStream.Position; AStream.WriteBuffer(SEGMENT_MARKER[0], 2); // Next two zero bytes are the size of the entire IPTC segiment, they will be // replaced when the segment is completely written. For this, we store the // offset to the begin of the IPTC segment in FIPTCSegmentStartPos. WriteWord(AStream, 0); AStream.WriteBuffer(IPTC_SIGNATURE[1], Length(IPTC_SIGNATURE)); end; procedure TIPTCWriter.WriteIPTCImageResourceBlock(AStream: TStream; AName: String); var i: Integer; tag: TTag; ms: TMemoryStream; dw: DWord; begin // Write the image resource header WriteImageResourceBlockHeader(AStream, IPTC_IMAGERESOURCEID, AName); // Now, we must write the length of the ImageResourceBlock. // Since we don't know this we write the tags to a memory stream first ms := TMemoryStream.Create; try // Write the tags to the temporary memory stream for i := 0 to FImgInfo.IptcData.TagCount-1 do begin tag := FImgInfo.IptcData.TagByIndex[i]; WriteTag(ms, tag); end; // Now the length of the data field is known (ms.Size). // Write the length field to "real" stream dw := ms.Size; WriteDWord(AStream, NtoBE(dw)); // Copy the tags from the memorystream to the "real" stream ms.Position := 0; AStream.Copyfrom(ms, ms.Size); finally ms.Free; end; end; procedure TIPTCWriter.WriteImageResourceBlockHeader(AStream: TStream; AResourceID: Integer; AResourceName: String); var dw: DWord; sa: ansistring; begin // Resource marker: 8BIM AStream.WriteBuffer(RESOURCE_MARKER[1], Length(RESOURCE_MARKER)); // Resource ID WriteWord(AStream, NtoBE(word(AResourceID))); // Resource name if Length(AResourceName) = 0 then WriteWord(AStream, 0) else begin sa := AResourceName; dw := Length(sa); if dw > 255 then begin dw := 255; SetLength(sa, dw); Warning(Format(rsImageResourceNameTooLong, [AResourceName])); end; if not odd(dw) then begin inc(dw); sa := sa + #0; end; WriteByte(AStream, byte(dw)); AStream.WriteBuffer(sa[1], dw); end; end; procedure TIptcWriter.WriteTag(AStream: TStream; ATag: TTag); const TAG_MARKER = $1C; var iptcTag: TIptcTag; len: DWord; begin iptcTag.TagMarker := byte(TAG_MARKER); iptcTag.RecordNumber := byte((ATag.TagID and $FF00) shr 8); iptctag.DatasetNumber := byte(ATag.TagID and $00FF); case ATag.TagType of ttUInt16: begin iptcTag.Size := NtoBE(2); AStream.WriteBuffer(iptcTag, SizeOf(iptcTag)); AStream.WriteBuffer(ATag.RawData[0], 2); end; ttString: begin len := Length(ATag.RawData); if odd(len) then begin inc(len); end; // "Standard" dataset if len < 32768 then begin iptcTag.Size := NtoBE(word(len)); AStream.WriteBuffer(iptcTag, SizeOf(iptcTag)); AStream.WriteBuffer(ATag.RawData[0], Length(ATag.RawData)); end else // "Extended" dataset if len < 65536 then begin // Size is 2, but we must set highest bit to mark tag as being extended. iptcTag.Size := NtoBE($8002); AStream.WriteBuffer(iptcTag, SizeOf(iptcTag)); WriteWord(AStream, NtoBE(word(len))); AStream.WriteBuffer(ATag.RawData[0], Length(ATag.RawData)); end else begin // Size is 4, but we must set highest bit to mark tag as being extended. iptcTag.Size := $8004; AStream.WriteBuffer(iptcTag, SizeOf(iptcTag)); WriteDWord(AStream, NtoBE(len)); AStream.WriteBuffer(ATag.RawData[0], Length(ATag.RawData)); end; if odd(Length(ATag.RawData)) then // zero-termination of string WriteByte(AStream, 0); end; else // I've never seen other tag types than USHORT and STRING... Error(Format(rsTagTypeNotSupported, [ATag.Name])); end; end; procedure TIptcWriter.WriteToStream(AStream: TStream; AImgFormat: TImgFormat); var i: Integer; lID: Word; lName: String; lData: TBytes; begin FImgFormat := AImgFormat; case FImgFormat of ifJpeg: WriteIptcHeader(AStream); else Error(rsImageFormatNotSupported); end; if (FImgInfo.IptcData.GetImageResourceBlockCount = 0) and (FImgInfo.IptcData.TagCount > 0) then FImgInfo.IptcData.AddImageResourceBlock(IPTC_IMAGERESOURCEID, '', nil); for i := 0 to FImgInfo.IptcData.GetImageResourceBlockCount-1 do begin FImgInfo.IptcData.GetImageResourceBlock(i, lID, lName, lData); if lID = IPTC_IMAGERESOURCEID then // Write the IPTC tags WriteIptcImageResourceBlock(AStream, lName) else begin // Write the other image resource blocks. WriteImageResourceBlockHeader(AStream, lID, lName); if odd(Length(lData)) then begin SetLength(lData, Length(lData) + 1); lData[High(lData)] := 0; end; WriteDWord(AStream, NtoBE(Length(lData))); AStream.Write(lData[0], Length(lData)); end; end; // If WriteToStream is called within a JPEG structure we must update the // size of the IPTC segment. UpdateSegmentSize(AStream, FIptcSegmentStartPos); end; end.