Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import de.stefan_oltmann.kim.format.jpeg.JpegImageParser
import de.stefan_oltmann.kim.format.jpeg.iptc.IptcTypes
import de.stefan_oltmann.kim.format.tiff.GPSInfo
import de.stefan_oltmann.kim.format.tiff.constant.ExifTag
import de.stefan_oltmann.kim.format.tiff.constant.FujiFilmTag
import de.stefan_oltmann.kim.format.tiff.constant.TiffConstants
import de.stefan_oltmann.kim.format.tiff.constant.TiffTag
import de.stefan_oltmann.kim.format.xmp.XmpReader
Expand Down Expand Up @@ -79,6 +80,9 @@ public object MetadataSummaryConverter {
val fNumber = mediaMetadata.findDoubleValue(ExifTag.EXIF_TAG_FNUMBER)
val focalLength = mediaMetadata.findDoubleValue(ExifTag.EXIF_TAG_FOCAL_LENGTH)

/* Extract Fujifilm film simulation from MakerNote */
val filmSimulation = extractFilmSimulation(mediaMetadata)

val keywords = xmpMetadata?.keywords?.ifEmpty {
extractKeywordsFromIptc(mediaMetadata)
} ?: extractKeywordsFromIptc(mediaMetadata)
Expand Down Expand Up @@ -128,6 +132,7 @@ public object MetadataSummaryConverter {
exposureTime = exposureTime,
fNumber = fNumber,
focalLength = focalLength,
filmSimulation = filmSimulation,
title = title,
description = description,
flagged = xmpMetadata?.flagged ?: false,
Expand Down Expand Up @@ -270,6 +275,28 @@ public object MetadataSummaryConverter {
country = iptcCountry
)
}

@JvmStatic
private fun extractFilmSimulation(metadata: MediaMetadata): String? {

/* Only check for Fujifilm cameras */
val cameraMake = metadata.findStringValue(TiffTag.TIFF_TAG_MAKE)
?: return null

if (!cameraMake.contains("FUJIFILM", ignoreCase = true))
return null

/* Try to read from MakerNote directory */
val makerNoteDir = metadata.exif?.makerNoteDirectory
?: return null

val filmModeField = makerNoteDir.findField(FujiFilmTag.FILM_MODE)
?: return null

val filmModeValue = filmModeField.toShort()?.toInt() ?: return null
Comment thread
StefanOltmann marked this conversation as resolved.

return FujiFilmTag.getFilmModeName(filmModeValue)
}
}

public fun MediaMetadata.convertToSummary(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,7 @@ public class TiffDirectory(
TiffConstants.TIFF_DIRECTORY_INTEROP -> "InteropIFD"
TiffConstants.TIFF_MAKER_NOTE_CANON -> "MakerNoteCanon"
TiffConstants.TIFF_MAKER_NOTE_NIKON -> "MakerNoteNikon"
TiffConstants.TIFF_MAKER_NOTE_FUJIFILM -> "MakerNoteFujiFilm"
else -> "Unknown type $type"
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import kotlin.jvm.JvmStatic
public object TiffReader {

internal const val NIKON_MAKER_NOTE_SIGNATURE = "Nikon\u0000"
internal const val FUJIFILM_MAKER_NOTE_SIGNATURE = "FUJIFILM"

private val offsetFields = listOf(
ExifTag.EXIF_TAG_EXIF_OFFSET,
Expand Down Expand Up @@ -530,11 +531,13 @@ public object TiffReader {

return makerNoteDirectory

} catch (ignore: Exception) {
} catch (e: Exception) {
/*
* Be silent here.
* MakerNote support is experimental.
*/
println("DEBUG: Exception parsing FujiFilm MakerNote: ${e.message}")
e.printStackTrace()
Comment thread
StefanOltmann marked this conversation as resolved.
}
}

Expand Down Expand Up @@ -600,6 +603,49 @@ public object TiffReader {
addDirectory = addDirectory
)
}

if (make != null && make.contains("FUJIFILM", ignoreCase = true)) {

try {
byteReader.reset()
byteReader.skipBytes("offset", makerNoteValueOffset)

val fujiSignature = byteReader.readBytes(
fieldName = "FujiFilm MakerNote signature",
count = FUJIFILM_MAKER_NOTE_SIGNATURE.length
).decodeToString()

val fujiSignatureMatched = fujiSignature == FUJIFILM_MAKER_NOTE_SIGNATURE

if (!fujiSignatureMatched)
return

/*
* Skip version (4 bytes).
* The IFD starts immediately after the version bytes.
* Fuji MakerNote IFD uses little-endian byte order.
*/
byteReader.skipBytes("version", 4)

/* IFD starts at offset 12 from the beginning of MakerNote data */
val ifdOffset = 12

readDirectory(
byteReader = byteReader,
byteOrder = ByteOrder.LITTLE_ENDIAN,
directoryOffset = makerNoteValueOffset + ifdOffset,
directoryType = TiffConstants.TIFF_MAKER_NOTE_FUJIFILM,
visitedOffsets = mutableListOf<Int>(),
readTiffImageBytes = false,
addDirectory = addDirectory
)
} catch (ignore: Exception) {
/*
* Be silent here.
* MakerNote support is experimental.
*/
}
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package de.stefan_oltmann.kim.format.tiff
import de.stefan_oltmann.kim.format.tiff.constant.CanonTag
import de.stefan_oltmann.kim.format.tiff.constant.ExifTag
import de.stefan_oltmann.kim.format.tiff.constant.ExifTag.EXIF_DIRECTORY_UNKNOWN
import de.stefan_oltmann.kim.format.tiff.constant.FujiFilmTag
import de.stefan_oltmann.kim.format.tiff.constant.GeoTiffTag
import de.stefan_oltmann.kim.format.tiff.constant.GpsTag
import de.stefan_oltmann.kim.format.tiff.constant.NikonTag
Expand All @@ -35,6 +36,7 @@ internal object TiffTags {
private val GPS_TAGS_MAP = GpsTag.ALL.groupByTo(mutableMapOf()) { it.tag }
private val CANON_TAGS_MAP = CanonTag.ALL.groupByTo(mutableMapOf()) { it.tag }
private val NIKON_TAGS_MAP = NikonTag.ALL.groupByTo(mutableMapOf()) { it.tag }
private val FUJIFILM_TAGS_MAP = FujiFilmTag.ALL.groupByTo(mutableMapOf()) { it.tag }

fun getTag(directoryType: Int, tag: Int): TagInfo? {

Expand All @@ -46,6 +48,7 @@ internal object TiffTags {
TiffConstants.TIFF_DIRECTORY_GPS -> GPS_TAGS_MAP[tag]
TiffConstants.TIFF_MAKER_NOTE_CANON -> CANON_TAGS_MAP[tag]
TiffConstants.TIFF_MAKER_NOTE_NIKON -> NIKON_TAGS_MAP[tag]
TiffConstants.TIFF_MAKER_NOTE_FUJIFILM -> FUJIFILM_TAGS_MAP[tag]
else -> TIFF_AND_EXIF_TAGS_MAP[tag]
} ?: return null

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/*
* Copyright 2026 Gnod
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.stefan_oltmann.kim.format.tiff.constant

import de.stefan_oltmann.kim.format.tiff.taginfo.TagInfo
import de.stefan_oltmann.kim.format.tiff.taginfo.TagInfoAscii
import de.stefan_oltmann.kim.format.tiff.taginfo.TagInfoShort
import de.stefan_oltmann.kim.format.tiff.taginfo.TagInfoUndefineds

/**
* Fujifilm MakerNote Tags
*
* See https://exiftool.org/TagNames/FujiFilm.html
*/
@Suppress("MagicNumber", "LargeClass", "StringLiteralDuplication")
public object FujiFilmTag {

/*
* TODO This list is incomplete
*/

public val MAKER_NOTE_VERSION: TagInfoUndefineds = TagInfoUndefineds(
0x0000, "MakerNoteVersion", 4,
TiffDirectoryType.EXIF_DIRECTORY_MAKER_NOTE_FUJIFILM
)

public val INTERNAL_SERIAL_NUMBER: TagInfoAscii = TagInfoAscii(
0x0010, "InternalSerialNumber", TagInfo.LENGTH_UNKNOWN,
TiffDirectoryType.EXIF_DIRECTORY_MAKER_NOTE_FUJIFILM
)

public val QUALITY: TagInfoAscii = TagInfoAscii(
0x1000, "Quality", TagInfo.LENGTH_UNKNOWN,
TiffDirectoryType.EXIF_DIRECTORY_MAKER_NOTE_FUJIFILM
)

public val SHARPNESS: TagInfoShort = TagInfoShort(
0x1001, "Sharpness",
TiffDirectoryType.EXIF_DIRECTORY_MAKER_NOTE_FUJIFILM
)

public val WHITE_BALANCE: TagInfoShort = TagInfoShort(
0x1002, "WhiteBalance",
TiffDirectoryType.EXIF_DIRECTORY_MAKER_NOTE_FUJIFILM
)

public val SATURATION: TagInfoShort = TagInfoShort(
0x1003, "Saturation",
TiffDirectoryType.EXIF_DIRECTORY_MAKER_NOTE_FUJIFILM
)

public val CONTRAST: TagInfoShort = TagInfoShort(
0x1004, "Contrast",
TiffDirectoryType.EXIF_DIRECTORY_MAKER_NOTE_FUJIFILM
)

/**
* Film Simulation / Film Mode
*
* See https://exiftool.org/TagNames/FujiFilm.html#FilmMode
*/
public val FILM_MODE: TagInfoShort = TagInfoShort(
0x1401, "FilmMode",
TiffDirectoryType.EXIF_DIRECTORY_MAKER_NOTE_FUJIFILM
)

public const val FILM_MODE_PROVIA_STANDARD: Int = 0x000
public const val FILM_MODE_STUDIO_PORTRAIT: Int = 0x100
public const val FILM_MODE_ASTIA_SOFT: Int = 0x120
public const val FILM_MODE_VELVIA_VIVID: Int = 0x200
public const val FILM_MODE_VELVIA: Int = 0x400
public const val FILM_MODE_PRO_NEG_STD: Int = 0x500
public const val FILM_MODE_PRO_NEG_HI: Int = 0x501
public const val FILM_MODE_CLASSIC_CHROME: Int = 0x600
public const val FILM_MODE_ETERNA: Int = 0x700
public const val FILM_MODE_CLASSIC_NEG: Int = 0x800
public const val FILM_MODE_BLEACH_BYPASS: Int = 0x900
public const val FILM_MODE_NOSTALGIC_NEG: Int = 0xA00
public const val FILM_MODE_REALA_ACE: Int = 0xB00

public val DYNAMIC_RANGE: TagInfoShort = TagInfoShort(
0x1400, "DynamicRange",
TiffDirectoryType.EXIF_DIRECTORY_MAKER_NOTE_FUJIFILM
)

public val DYNAMIC_RANGE_SETTING: TagInfoShort = TagInfoShort(
0x1402, "DynamicRangeSetting",
TiffDirectoryType.EXIF_DIRECTORY_MAKER_NOTE_FUJIFILM
)

public val ALL: List<TagInfo> = listOf(
MAKER_NOTE_VERSION, INTERNAL_SERIAL_NUMBER, QUALITY,
SHARPNESS, WHITE_BALANCE, SATURATION, CONTRAST,
FILM_MODE, DYNAMIC_RANGE, DYNAMIC_RANGE_SETTING
)

/**
* Returns the display name for a film mode value.
*/
public fun getFilmModeName(value: Int): String? =
when (value) {
FILM_MODE_PROVIA_STANDARD -> "Provia/Standard"
FILM_MODE_STUDIO_PORTRAIT -> "Studio Portrait"
FILM_MODE_ASTIA_SOFT -> "Astia/Soft"
FILM_MODE_VELVIA_VIVID -> "Velvia/Vivid"
FILM_MODE_VELVIA -> "Velvia"
FILM_MODE_PRO_NEG_STD -> "Pro Neg. Std"
FILM_MODE_PRO_NEG_HI -> "Pro Neg. Hi"
FILM_MODE_CLASSIC_CHROME -> "Classic Chrome"
FILM_MODE_ETERNA -> "Eterna"
FILM_MODE_CLASSIC_NEG -> "Classic Negative"
FILM_MODE_BLEACH_BYPASS -> "Bleach Bypass"
FILM_MODE_NOSTALGIC_NEG -> "Nostalgic Neg"
FILM_MODE_REALA_ACE -> "Reala ACE"
else -> null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ public object TiffConstants {
/* Artificial MakerNote directores */
public const val TIFF_MAKER_NOTE_CANON: Int = -101
public const val TIFF_MAKER_NOTE_NIKON: Int = -102
public const val TIFF_MAKER_NOTE_FUJIFILM: Int = -103

public const val FIELD_TYPE_BYTE_INDEX: Int = 1
public const val FIELD_TYPE_ASCII_INDEX: Int = 2
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ public enum class TiffDirectoryType(
),
EXIF_DIRECTORY_MAKER_NOTE_NIKON(
TiffConstants.TIFF_MAKER_NOTE_NIKON, "MakerNoteNikon", false
),
EXIF_DIRECTORY_MAKER_NOTE_FUJIFILM(
TiffConstants.TIFF_MAKER_NOTE_FUJIFILM, "MakerNoteFujiFilm", false
);

override fun toString(): String =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ public data class MetadataSummary(
val fNumber: Double? = null,
val focalLength: Double? = null,

/* Film simulation (Fujifilm specific) */
val filmSimulation: String? = null,

/* Title & Description */
val title: String? = null,
val description: String? = null,
Expand Down Expand Up @@ -140,6 +143,9 @@ public data class MetadataSummary(
fNumber = fNumber ?: other.fNumber,
focalLength = focalLength ?: other.focalLength,

/* Film simulation (Fujifilm specific) */
filmSimulation = filmSimulation ?: other.filmSimulation,

/* Title & Description */
title = title ?: other.title,
description = description ?: other.description,
Expand Down
Loading
Loading