-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support for XMP DCMI license data via KHR_xmp_json_ld (#8)
- Loading branch information
1 parent
057d8b1
commit 4bb762e
Showing
7 changed files
with
461 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
This is free and unencumbered software released into the public domain. | ||
|
||
Anyone is free to copy, modify, publish, use, compile, sell, or | ||
distribute this software, either in source code form or as a compiled | ||
binary, for any purpose, commercial or non-commercial, and by any | ||
means. | ||
|
||
In jurisdictions that recognize copyright laws, the author or authors | ||
of this software dedicate any and all copyright interest in the | ||
software to the public domain. We make this dedication for the benefit | ||
of the public at large and to the detriment of our heirs and | ||
successors. We intend this dedication to be an overt act of | ||
relinquishment in perpetuity of all present and future rights to this | ||
software under copyright law. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | ||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | ||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. | ||
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR | ||
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, | ||
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR | ||
OTHER DEALINGS IN THE SOFTWARE. | ||
|
||
For more information, please refer to <https://unlicense.org> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
@tool | ||
extends EditorPlugin | ||
|
||
|
||
func _enter_tree() -> void: | ||
# NOTE: Be sure to also instance and register these at runtime if you want | ||
# the extensions at runtime. This editor plugin script won't run in games. | ||
var ext = GLTFDocumentExtensionKHR_XMP.new() | ||
GLTFDocument.register_gltf_document_extension(ext) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
[plugin] | ||
|
||
name="glTF KHR_xmp_json_ld Copyright" | ||
description="Godot implementation of the glTF KHR_xmp_json_ld extension for DCMI copyright metadata. Requires Godot 4.3 or newer." | ||
author="Aaron Franke" | ||
version="4.3" | ||
script="gltf_khr_xmp_plugin.gd" |
120 changes: 120 additions & 0 deletions
120
addons/gltf_khr_xmp_copyright/xmp/dcmi_license_metadata.gd
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
@tool | ||
## Stores DCMI XMP license info and metadata as defined by Khronos and DCMI. | ||
## List of elements: http://purl.org/dc/elements/1.1/ | ||
## https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_xmp_json_ld | ||
class_name DCMILicenseMetadata | ||
extends XMPMetadataBase | ||
|
||
# TODO: On export: format | ||
|
||
const CONTEXT_URL: String = "http://purl.org/dc/elements/1.1/" | ||
|
||
@export_group("License") | ||
## License name. Examples: "MIT License", "CC BY-SA 4.0", "All Rights Reserved" | ||
@export_placeholder("Ex: MIT, BSD, CC0, etc") var rights: String = "" | ||
## Primary authors. Example: John Smith <[email protected]> | ||
@export var creator: PackedStringArray = [] | ||
## Secondary authors. Example: John Smith <[email protected]> | ||
@export var contributor: PackedStringArray = [] | ||
## Company names. Example: Smith Co. | ||
@export var publisher: PackedStringArray = [] | ||
|
||
@export_group("Content") | ||
## Example: Rainbow Godette Model | ||
@export_placeholder("Name of this GLTF file") var title: String = "" | ||
## Example: Detailed Godette model with added sunshine, lollipops, and rainbows. | ||
@export_placeholder("Description of this GLTF file") var description: String = "" | ||
## Examples: Architecture, Automotive, Botany | ||
@export var subject: PackedStringArray = [] | ||
## Examples: Character, Building, Vehicle, Plant, Prop | ||
@export var type: PackedStringArray = [] | ||
|
||
@export_group("Locale") | ||
## Location. Example: Bay Area, CA, USA | ||
@export_placeholder("Location/area/city/country") var coverage: String = "" | ||
## Examples: 2014-02-09, 2014-02-09T22:10:30 | ||
@export_placeholder("YYYY-MM-DD and/or hh:mm:ss") var date: String = "" | ||
## ISO 639-2 code (ex: en), ISO 639-3 code (ex: eng), or culture ID (ex: en_US). | ||
@export_placeholder("en, eng, en_US, etc") var language: String = "" | ||
|
||
@export_group("Reference") | ||
## Example: ISBN-13:978-0802144423 | ||
@export_placeholder("DOI, ISBN, URN, etc") var identifier: String = "" | ||
## Example: https://your.website/file.glb | ||
@export_placeholder("URL to this GLTF on the web") var source: String = "" | ||
## Websites. Example: https://godotengine.org/ | ||
@export var relation: PackedStringArray = [] | ||
|
||
|
||
func to_dictionary() -> Dictionary: | ||
var dcmi_dict: Dictionary = _dcmi_properties_to_dictionary() | ||
if dcmi_dict.is_empty(): | ||
# If there was no data, don't add @context, just return an empty dict. | ||
return dcmi_dict | ||
var ret: Dictionary = { | ||
"@context": { | ||
"dc": "http://purl.org/dc/elements/1.1/", | ||
}, | ||
"@id": "", | ||
} | ||
ret.merge(dcmi_dict) | ||
return ret | ||
|
||
|
||
func _dcmi_properties_to_dictionary() -> Dictionary: | ||
var dcmi_dict: Dictionary = {} | ||
if not contributor.is_empty(): | ||
dcmi_dict["dc:contributor"] = { "@set": contributor } | ||
if not coverage.is_empty(): | ||
dcmi_dict["dc:coverage"] = coverage | ||
if not creator.is_empty(): | ||
dcmi_dict["dc:creator"] = { "@list": creator } | ||
if not date.is_empty(): | ||
dcmi_dict["dc:date"] = date | ||
if not description.is_empty(): | ||
dcmi_dict["dc:description"] = description | ||
if not identifier.is_empty(): | ||
dcmi_dict["dc:identifier"] = identifier | ||
if not language.is_empty(): | ||
dcmi_dict["dc:language"] = language | ||
if not publisher.is_empty(): | ||
dcmi_dict["dc:publisher"] = { "@set": publisher } | ||
if not relation.is_empty(): | ||
dcmi_dict["dc:relation"] = { "@set": relation } | ||
if not rights.is_empty(): | ||
dcmi_dict["dc:rights"] = rights | ||
if not source.is_empty(): | ||
dcmi_dict["dc:source"] = source | ||
if not subject.is_empty(): | ||
dcmi_dict["dc:subject"] = { "@set": subject } | ||
if not title.is_empty(): | ||
dcmi_dict["dc:title"] = title | ||
if not type.is_empty(): | ||
dcmi_dict["dc:type"] = { "@set": type } | ||
return dcmi_dict | ||
|
||
|
||
static func from_dictionary(xmp_packet: Dictionary) -> XMPMetadataBase: | ||
var context_url: String = xmp_packet["@context"]["dc"] | ||
if context_url != DCMILicenseMetadata.CONTEXT_URL: | ||
push_warning("GLTF KHR XMP: DCMI metadata had a URL of '" + context_url + "' but expected '" + DCMILicenseMetadata.CONTEXT_URL + "'. Attempting to parse anyway.") | ||
var ret := DCMILicenseMetadata.new() | ||
ret.xmp_json_ld = xmp_packet | ||
_dcmi_properties_from_dictionary(ret, xmp_packet) | ||
return ret | ||
|
||
|
||
const _SINGLE_VALUES: PackedStringArray = ["coverage", "date", "description", "identifier", "language", "rights", "source", "title"] | ||
const _ARRAY_VALUES: PackedStringArray = ["contributor", "creator", "publisher", "relation", "subject", "type"] | ||
|
||
static func _dcmi_properties_from_dictionary(dcmi_data: DCMILicenseMetadata, xmp_packet: Dictionary) -> void: | ||
for single_value_name in _SINGLE_VALUES: | ||
var dcmi_prefixed: String = "dc:" + single_value_name | ||
if xmp_packet.has(dcmi_prefixed): | ||
var extracted = extract_single_value_from_json_ld(xmp_packet[dcmi_prefixed]) | ||
dcmi_data.set(single_value_name, String(extracted)) | ||
for array_value_name in _ARRAY_VALUES: | ||
var dcmi_prefixed: String = "dc:" + array_value_name | ||
if xmp_packet.has(dcmi_prefixed): | ||
var extracted = extract_array_from_json_ld(xmp_packet[dcmi_prefixed]) | ||
dcmi_data.set(array_value_name, PackedStringArray(extracted)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,176 @@ | ||
@tool | ||
class_name GLTFDocumentExtensionKHR_XMP | ||
extends GLTFDocumentExtension | ||
|
||
|
||
## Applies to the whole document when exporting. | ||
@export var dcmi_license_metadata := DCMILicenseMetadata.new() | ||
|
||
|
||
# Import process. | ||
func _import_preflight(gltf_state: GLTFState, extensions_used: PackedStringArray) -> Error: | ||
if not extensions_used.has("KHR_xmp_json_ld"): | ||
return ERR_SKIP | ||
var state_json: Dictionary = gltf_state.json | ||
if not state_json.has("extensions"): | ||
return ERR_INVALID_DATA | ||
var doc_extensions: Dictionary = state_json["extensions"] | ||
if not doc_extensions.has("KHR_xmp_json_ld"): | ||
return ERR_INVALID_DATA | ||
var khr_xmp_ext: Dictionary = doc_extensions["KHR_xmp_json_ld"] | ||
if not khr_xmp_ext.has("packets"): | ||
return ERR_INVALID_DATA | ||
var khr_xmp_packets: Array = khr_xmp_ext["packets"] | ||
var parsed_khr_xmp_data: Array = [] | ||
for xmp_packet in khr_xmp_packets: | ||
var xmp: XMPMetadataBase = _parse_xmp_packet(gltf_state, xmp_packet) | ||
if xmp == null: | ||
return ERR_INVALID_DATA | ||
parsed_khr_xmp_data.append(xmp) | ||
gltf_state.set_additional_data(&"KHR_xmp_json_ld", parsed_khr_xmp_data) | ||
return OK | ||
|
||
|
||
func _parse_xmp_packet(gltf_state: GLTFState, xmp_packet: Dictionary) -> XMPMetadataBase: | ||
if not xmp_packet.has("@context"): | ||
return null | ||
var xmp_context: Dictionary = xmp_packet["@context"] | ||
var xmp_metadata: XMPMetadataBase = null | ||
for context_prefix in xmp_context: | ||
if context_prefix == "dc": | ||
xmp_metadata = DCMILicenseMetadata.from_dictionary(xmp_packet) | ||
elif context_prefix == "rdf": | ||
# RDF allows specifying language alternatives, it combines together | ||
# with another context, we don't need to do anything special here. | ||
pass | ||
else: | ||
# XMP is an open-ended standard, any data structure can be stored | ||
# in it. Show a warning when we encounter something unrecognized, | ||
# but the JSON data will still be available in an XMPMetadataBase. | ||
push_warning("GLTF KHR XMP: Unrecognized context prefix '" + context_prefix + "'.") | ||
if xmp_metadata == null: | ||
# No specific class wants to handle this, so just return the base class. | ||
xmp_metadata = XMPMetadataBase.from_dictionary(xmp_packet) | ||
return xmp_metadata | ||
|
||
|
||
func _get_supported_extensions() -> PackedStringArray: | ||
return PackedStringArray(["KHR_xmp_json_ld"]) | ||
|
||
|
||
func _import_post(gltf_state: GLTFState, root: Node) -> Error: | ||
var asset_json: Dictionary = gltf_state.json["asset"] | ||
if asset_json.has("extensions"): | ||
var asset_extensions: Dictionary = asset_json["extensions"] | ||
if asset_extensions.has("KHR_xmp_json_ld"): | ||
var asset_xmp: Dictionary = asset_extensions["KHR_xmp_json_ld"] | ||
if asset_xmp.has("packet"): | ||
var packet: int = int(asset_xmp["packet"]) | ||
var xmp_array: Array = gltf_state.get_additional_data("KHR_xmp_json_ld") | ||
root.set_meta("KHR_xmp_json_ld", xmp_array[packet]) | ||
return OK | ||
|
||
|
||
# Export process. | ||
func _export_preflight(gltf_state: GLTFState, root: Node) -> Error: | ||
var dcmi_dict: Dictionary = dcmi_license_metadata.to_dictionary() | ||
if dcmi_dict.is_empty(): | ||
return OK | ||
dcmi_dict["dc:format"] = "model/gltf" | ||
# Add the DCMI XMP JSON dictionary to the document extensions. | ||
var state_packets: Array = _get_or_create_state_packets_in_state(gltf_state) | ||
state_packets.append(dcmi_dict) | ||
return OK | ||
|
||
|
||
func _export_preserialize(gltf_state: GLTFState) -> Error: | ||
var state_packets = _get_state_packets_in_state_if_present(gltf_state) | ||
if state_packets == null or state_packets.is_empty(): | ||
return OK | ||
var first_packet: Dictionary = state_packets[0] | ||
if first_packet.has("dc:format"): | ||
if gltf_state.filename.ends_with(".gltf"): | ||
first_packet["dc:format"] = "model/gltf+json" | ||
else: # If .glb, it's binary. If empty, it's a buffer, also binary. | ||
first_packet["dc:format"] = "model/gltf-binary" | ||
return OK | ||
|
||
|
||
func _export_node(gltf_state: GLTFState, gltf_node: GLTFNode, node_json: Dictionary, node: Node) -> Error: | ||
if not node.has_meta(&"dcmi_license_metadata"): | ||
return OK | ||
var dcmi_data: DCMILicenseMetadata = node.get_meta(&"dcmi_license_metadata") | ||
var dcmi_dict = dcmi_data.to_dictionary() | ||
if dcmi_dict.is_empty(): | ||
return OK | ||
# Insert the DCMI dictionary in the state packets and reference it on the node. | ||
var state_packets: Array = _get_or_create_state_packets_in_state(gltf_state) | ||
var node_extensions: Dictionary | ||
# TODO: = node_json.get_or_set_default("extensions", {}) | ||
if node_json.has("extensions"): | ||
node_extensions = node_json["extensions"] | ||
else: | ||
node_extensions = {} | ||
node_json["extensions"] = node_extensions | ||
node_extensions["KHR_xmp_json_ld"] = { | ||
"packet": state_packets.size() | ||
} | ||
state_packets.append(dcmi_dict) | ||
return OK | ||
|
||
|
||
func _export_post(gltf_state: GLTFState) -> Error: | ||
var state_json: Dictionary = gltf_state.json | ||
var state_packets = _get_state_packets_in_state_if_present(gltf_state) | ||
if state_packets == null or state_packets.is_empty(): | ||
return OK | ||
if not state_packets[0].has("dc:format"): | ||
return OK | ||
# Reference the DCMI XMP JSON dictionary in the asset. | ||
var asset: Dictionary = state_json["asset"] | ||
var asset_extensions: Dictionary | ||
# TODO: = asset.get_or_set_default("extensions", {}) | ||
if asset.has("extensions"): | ||
asset_extensions = asset["extensions"] | ||
else: | ||
asset_extensions = {} | ||
asset["extensions"] = asset_extensions | ||
asset_extensions["KHR_xmp_json_ld"] = { | ||
"packet": 0 | ||
} | ||
return OK | ||
|
||
|
||
func _get_state_packets_in_state_if_present(gltf_state: GLTFState): # -> Array? | ||
var state_json: Dictionary = gltf_state.json | ||
if not state_json.has("extensions"): | ||
return null | ||
var state_extensions: Dictionary = state_json["extensions"] | ||
if not state_extensions.has("KHR_xmp_json_ld"): | ||
return null | ||
var khr_xmp_ext: Dictionary = state_extensions["KHR_xmp_json_ld"] | ||
return khr_xmp_ext["packets"] | ||
|
||
|
||
func _get_or_create_state_packets_in_state(gltf_state: GLTFState) -> Array: | ||
var state_json = gltf_state.get_json() | ||
var state_extensions: Dictionary | ||
if state_json.has("extensions"): | ||
state_extensions = state_json["extensions"] | ||
else: | ||
state_extensions = {} | ||
state_json["extensions"] = state_extensions | ||
var omi_physics_joint_doc_ext: Dictionary | ||
if state_extensions.has("KHR_xmp_json_ld"): | ||
omi_physics_joint_doc_ext = state_extensions["KHR_xmp_json_ld"] | ||
else: | ||
omi_physics_joint_doc_ext = {} | ||
state_extensions["KHR_xmp_json_ld"] = omi_physics_joint_doc_ext | ||
gltf_state.add_used_extension("KHR_xmp_json_ld", false) | ||
var state_packets: Array | ||
if omi_physics_joint_doc_ext.has("packets"): | ||
state_packets = omi_physics_joint_doc_ext["packets"] | ||
else: | ||
state_packets = [] | ||
omi_physics_joint_doc_ext["packets"] = state_packets | ||
return state_packets |
Oops, something went wrong.