Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add tag GPS_TAG_GPS_IMG_DIRECTION_REF and GPS_TAG_GPS_IMG_DIRECTION #107

Closed
ravenfeld opened this issue Dec 22, 2024 · 8 comments
Closed

Comments

@ravenfeld
Copy link

Hello, I'd like to add the following tags:

  • GPS_TAG_GPS_IMG_DIRECTION_REF
  • GPS_TAG_GPS_IMG_DIRECTION

It's in the project, they're in GpsTag, but I can't see how to enter this value.

Thank you for your help.

@StefanOltmann
Copy link
Collaborator

Hi,

do you have samples values for this?

@ravenfeld
Copy link
Author

ravenfeld commented Dec 22, 2024

In the OpenCamera application it does this, it seems to work fine but I don't use ExifInterface as I'm trying to do KMP. That's why I discovered your lib.

    private void setGPSDirectionExif(ExifInterface exif, boolean store_geo_direction, double geo_direction) {
        if( MyDebug.LOG )
            Log.d(TAG, "setGPSDirectionExif");
        if( store_geo_direction ) {
            float geo_angle = (float)Math.toDegrees(geo_direction);
            if( geo_angle < 0.0f ) {
                geo_angle += 360.0f;
            }
            if( MyDebug.LOG )
                Log.d(TAG, "save geo_angle: " + geo_angle);
            // see http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/GPS.html
            String GPSImgDirection_string = Math.round(geo_angle*100) + "/100";
            if( MyDebug.LOG )
                Log.d(TAG, "GPSImgDirection_string: " + GPSImgDirection_string);
            // fine to ignore request.remove_device_exif, as this is a separate user option
            exif.setAttribute(ExifInterface.TAG_GPS_IMG_DIRECTION, GPSImgDirection_string);
            exif.setAttribute(ExifInterface.TAG_GPS_IMG_DIRECTION_REF, "M");
        }
    }

So for an azimuth of 45.5°, the value should be formatted as "4550/100", I have a feeling

@StefanOltmann
Copy link
Collaborator

Hi,

I added a new example that shows how you can achieve that:

fun updateGpsImgDirection() {
val inputFile = File("testphoto.jpg")
val outputFile = File("testphoto_gps_img_direction.jpg")
val metadata = Kim.readMetadata(inputFile) ?: return
val outputSet: TiffOutputSet = metadata.exif?.createOutputSet() ?: TiffOutputSet()
val gpsDirectory = outputSet.getOrCreateGPSDirectory()
val roundedGeoAngle: Long = 1337
gpsDirectory.add(
GpsTag.GPS_TAG_GPS_IMG_DIRECTION,
RationalNumber.create(roundedGeoAngle, 100)
)
gpsDirectory.add(
GpsTag.GPS_TAG_GPS_IMG_DIRECTION_REF,
"M"
)
OutputStreamByteWriter(outputFile.outputStream()).use { outputStreamByteWriter ->
JpegRewriter.updateExifMetadataLossless(
byteReader = JvmInputStreamByteReader(inputFile.inputStream(), inputFile.length()),
byteWriter = outputStreamByteWriter,
outputSet = outputSet
)
}
}

It's a variant of the previous example that changes the date taken.
You can change any field this way.

You can drop testphoto_gps_img_direction.jpg into https://stefan-oltmann.de/exif-viewer to check it:

image

Does that help you?

Kind regards,

Stefan

@ravenfeld
Copy link
Author

Thanks a lot, I was using Kim.update and couldn't find it.
I'll test it right away.

@StefanOltmann
Copy link
Collaborator

Kim.update() is just an convenience API for the common use cases like updating the date taken, GPS locations and so on. For more special stuff you need to use the low level API, but with that you can change just any possible field.

@ravenfeld
Copy link
Author

ravenfeld commented Dec 22, 2024

Here's the code I use. I haven't managed to use all the functions you shared with me because I'm trying to do it in commonMain so that Android and iOS do the same thing.

    private fun updateGpsImgDirection(relativePath:String, bearingInDegree:Double) {
        Path(relativePath)
            .takeIf(Path::exists)
            ?.run {
                val metadata = Kim.readMetadata(this) ?: return

                val outputSet: TiffOutputSet = metadata.exif?.createOutputSet() ?: TiffOutputSet()

                val gpsDirectory = outputSet.getOrCreateGPSDirectory()

                var geoAngle: Double = bearingInDegree
                if (geoAngle < 0.0f) {
                    geoAngle += 360.0f
                }

                gpsDirectory.add(
                    GpsTag.GPS_TAG_GPS_IMG_DIRECTION,
                    RationalNumber.create((geoAngle* 100).toLong(), 100)
                )

                gpsDirectory.add(
                    GpsTag.GPS_TAG_GPS_IMG_DIRECTION_REF,
                    GpsTag.GPS_TAG_GPS_IMG_DIRECTION_REF_VALUE_MAGNETIC_NORTH
                )

                val byteArrayByteWriter = ByteArrayByteWriter()

                JpegRewriter.updateExifMetadataLossless(
                    byteReader = ByteArrayByteReader(readBytes()),
                    byteWriter = byteArrayByteWriter,
                    outputSet = outputSet
                )
                val result=byteArrayByteWriter.toByteArray()

                writeBytes(result)
            }
    }

Thank you very much for your help and responsiveness.
Your lib is really nice.

@StefanOltmann
Copy link
Collaborator

StefanOltmann commented Dec 22, 2024

Thank you! :)

Just a hint: There are multiple implementations of ByteReader & ByteWriter available and you could even provide your own.

For working with kotlinx-io on Android & iOS you might want to take a look at KotlinIoSourceByteReader & KotlinIoSinkByteWriter.

That's not available right now for JS, because I need to refactor that since Ktor 3 came out - see #108.

Also: If you want to update existing fields, you may need to remove the old values first. You could do that every time for safety.

@ravenfeld
Copy link
Author

I'm sharing my final file in case anyone else is interested.

I didn't manage to use KotlinIoSinkByteWriter as it's on the same file that I want to add the tags. I had an error with the move of a temporary file.

rivate const val FULL_ROTATION_DEG = 360
private const val DIVISOR_DEGREE = 100L
object KimExt {

    fun updateGpsImgDirection(
        path: Path,
        userHeadingInDegree: Float
    ) {
        updateTag(path = path) { outputSet ->
            val gpsDirectory = outputSet.getOrCreateGPSDirectory()

            val gpsImgDirection = if (userHeadingInDegree < 0.0f) {
                userHeadingInDegree + FULL_ROTATION_DEG
            } else {
                userHeadingInDegree
            }.run { this * DIVISOR_DEGREE }.toLong()

            gpsDirectory.add(
                GpsTag.GPS_TAG_GPS_IMG_DIRECTION,
                RationalNumber.create(gpsImgDirection, DIVISOR_DEGREE)
            )

            gpsDirectory.add(
                GpsTag.GPS_TAG_GPS_IMG_DIRECTION_REF,
                GpsTag.GPS_TAG_GPS_IMG_DIRECTION_REF_VALUE_MAGNETIC_NORTH
            )
            outputSet
        }
    }

    private fun updateTag(path: Path, block: (outputSet: TiffOutputSet) -> TiffOutputSet) {
        path
            .takeIf(Path::exists)
            ?.run {
                val metadata = Kim.readMetadata(this) ?: return

                val outputSet: TiffOutputSet = metadata.exif?.createOutputSet() ?: TiffOutputSet()

                block(outputSet)

                val byteArrayByteWriter = ByteArrayByteWriter()

                KotlinIoSourceByteReader.read(this) { byteReader ->
                    byteReader?.let {
                        JpegRewriter.updateExifMetadataLossless(
                            byteReader = it,
                            byteWriter = byteArrayByteWriter,
                            outputSet = outputSet
                        )
                    }
                }

                val result = byteArrayByteWriter.toByteArray()

                writeBytes(result)
            }
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants