Spaces:
Running
on
T4
Running
on
T4
<html lang="en"> | |
<head> | |
<meta charset="utf-8" /> | |
<meta name="viewport" content="width=device-width, initial-scale=1" /> | |
<!-- Bootstrap CSS --> | |
<link | |
href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" | |
rel="stylesheet" | |
integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" | |
crossorigin="anonymous" | |
/> | |
<script | |
src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" | |
integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" | |
crossorigin="anonymous" | |
></script> | |
<link rel="stylesheet" href="style.css" type="text/css" /> | |
<script | |
type="module" | |
src="https://display.truepic.com/truepic_display.es.js" | |
></script> | |
<link rel="stylesheet" href="json_viewer.css" /> | |
<script src="https://unpkg.com/@peculiar/x509"></script> | |
<script type="text/javascript" src="json_viewer.js"></script> | |
</head> | |
<body> | |
<div class="container-fluid mt-2" id="head"> | |
<div class="container"> | |
<div class="row"> | |
<div class="col position-relative"> | |
<div class="row"> | |
<div class="col"> | |
<h1> | |
Watermarked Content Credentials | |
<div class="badge bg-secondary">experimental</div> | |
</h1> | |
<p> | |
Generate images with C2PA content credentials and Steg.ai | |
digital watermarking for enhanced AI label robustness | |
</p> | |
</div> | |
</div> | |
<div class="d-flex flex-row position-absolute bottom-0 tabs"> | |
<div id="generateTab" class="active"> | |
<img src="images/generate_icon.svg" /> Generate | |
</div> | |
<div id="verifyTab"> | |
<img src="images/verify_icon.svg" /> Verify | |
</div> | |
</div> | |
</div> | |
<div class="col right-column"> | |
<div class="alert alert-primary" role="alert"> | |
<h4>Support our work!</h4> | |
<p> | |
<img src="images/heart_icon.svg" /> Like this Space in the nav | |
above | |
</p> | |
<p> | |
<img src="images/wave_icon.svg" /> Joining the conversation in | |
the Community tab | |
</p> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div class="container-fluid" id="body"> | |
<div class="container"> | |
<div class="row"> | |
<div class="col" style="min-width: 0; padding-right: 0"> | |
<div class="display-generate"> | |
<div class="image-container"> | |
<img src="images/placeholder.png" class="placeholder" /> | |
<img | |
src="images/spinner.svg" | |
class="spinner" | |
style="display: none" | |
/> | |
</div> | |
<div class="alert alert-secondary action-menu" role="alert"> | |
<div class="row"> | |
<div class="col"> | |
<p class="mb-0"> | |
<strong | |
>Download the signed, watermarked image to share | |
it.</strong | |
> | |
</p> | |
<p class="mb-0"> | |
Use the verify tab two view the details for an image you | |
have or that's been shared with you. | |
</p> | |
</div> | |
<div class="col flex-grow-0"> | |
<a id="download-button" class="btn btn-outline-primary" | |
>Download image <img src="images/download_icon.svg" | |
/></a> | |
</div> | |
<div class="col flex-grow-0"> | |
<button | |
type="button" | |
id="goto-verify-button" | |
class="btn btn-outline-primary" | |
> | |
Go to verify tab <img src="images/link_icon.svg" /> | |
</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div class="display-verify"> | |
<div id="original-image"> | |
<div class="row mt-5"> | |
<div class="col mb-4"> | |
<h2>Uploaded image</h2> | |
</div> | |
</div> | |
<div class="row pb-4"> | |
<div class="col flex-grow-0"> | |
<img id="uploaded-image" class="thumbnail" /> | |
</div> | |
<div class="col"> | |
<div id="content-crendentials-icon"></div> | |
Content Credentials<br /> | |
<div id="digital-watermark-icon"></div> | |
Digital watermark | |
</div> | |
</div> | |
</div> | |
<div class="mt-3" id="resultLabel"> | |
<h2 class="mb-4">Result</h2> | |
<div | |
class="alert alert-secondary" | |
role="alert" | |
id="verifyResultDescription" | |
></div> | |
</div> | |
<div class="image-container"> | |
<img src="images/placeholder.png" class="placeholder" /> | |
<img | |
src="images/spinner.svg" | |
class="spinner" | |
style="display: none" | |
/> | |
</div> | |
<div class="output-container mt-5" style="display: none"> | |
<h3 class="mt-4 mb-4">Verification output</h3> | |
<div id="verification-output"></div> | |
<h3 class="mt-4 pt-4 mb-4" id="certificates-h3"> | |
Certificates | |
</h3> | |
<div class="certificate" id="certificate-output"> | |
<h4>Certificate chain</h4> | |
<ul id="certificate-list"></ul> | |
<h4>Details</h4> | |
<div class="details"> | |
<strong>Basic info</strong> | |
<div> | |
<label>Type</label> | |
X.509 Certificate | |
</div> | |
<div> | |
<label>Serial Number</label> | |
<div class="serialNumber"></div> | |
</div> | |
<div> | |
<label>Issued</label> | |
<div class="issued"></div> | |
</div> | |
<div> | |
<label>Expired</label> | |
<div class="expired"></div> | |
</div> | |
<strong>Subject</strong> | |
<div> | |
<label>Common Name</label> | |
<div class="subjectCommonName"></div> | |
</div> | |
<div> | |
<label>Organization</label> | |
<div class="subjectOrganization"></div> | |
</div> | |
<div> | |
<label>Organization Unit</label> | |
<div class="subjectOrganizationUnit"></div> | |
</div> | |
<div> | |
<label>Country</label> | |
<div class="subjectCountry"></div> | |
</div> | |
<strong>Issuer</strong> | |
<div> | |
<label>Common Name</label> | |
<div class="issuerCommonName"></div> | |
</div> | |
<div> | |
<label>Organization</label> | |
<div class="issuerOrganization"></div> | |
</div> | |
<div> | |
<label>Organization Unit</label> | |
<div class="issuerOrganizationUnit"></div> | |
</div> | |
<div> | |
<label>Country</label> | |
<div class="issuerCountry"></div> | |
</div> | |
<strong>Public Key Info</strong> | |
<div> | |
<label>Algorithm</label> | |
<div class="algorithm"></div> | |
</div> | |
<div id="modulusContainer"> | |
<label>Modulus</label> | |
<div class="modulus"></div> | |
</div> | |
<div id="curveContainer"> | |
<label>Curve</label> | |
<div class="namedCurve"></div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div class="col mt-4 pt-4" id="logo"> | |
<img src="images/logo.png" /> | |
<p class="small mt-3 mb-5"> | |
This is a conceptual application meant for demonstration | |
purposes only. | |
</p> | |
</div> | |
</div> | |
<div class="col right-column"> | |
<div class="display-generate"> | |
<form class="image-gen-form"> | |
<div class="form-group mb-3"> | |
<label for="prompt">Image prompt</label> | |
<textarea id="prompt" class="form-control"></textarea> | |
</div> | |
<div class="form-group mb-3"> | |
<label>Model</label> | |
<div class="custom-select"> | |
<select id="model" class="form-control"> | |
<option disabled selected value>Select</option> | |
<option value="runwayml/stable-diffusion-v1-5,1.5"> | |
runwayml/stable-diffusion-v1-5 | |
</option> | |
<option value="CompVis/stable-diffusion-v1-4,1.4"> | |
CompVis/stable-diffusion-v1-4 | |
</option> | |
<option value="stabilityai/stable-diffusion-2-1,2.1"> | |
stabilityai/stable-diffusion-2-1 | |
</option> | |
</select> | |
</div> | |
</div> | |
<div class="form-check mb-3"> | |
<input | |
class="form-check-input" | |
type="checkbox" | |
value="" | |
id="generate-terms" | |
/> | |
<label class="form-check-label" for="defaultCheck1"> | |
By using this demo you agree to the terms and conditions of | |
<a | |
href="https://truepic.com/terms-and-conditions/" | |
target="_blank" | |
>Truepic</a | |
> | |
and | |
<a href="https://steg.ai/tos" target="_blank">Steg.ai</a> | |
</label> | |
</div> | |
<button | |
type="submit" | |
class="btn btn-primary" | |
id="generate-button" | |
> | |
Submit | |
</button> | |
</form> | |
<div class="how-it-works"> | |
<p><strong>How it works</strong></p> | |
<p> | |
When an image is generated and signed with C2PA Content | |
Credentials, an imperceptible digital watermark, powered by | |
Steg.ai, is also added to the image pixels. The watermark | |
serves as a backup in case the Content Credentials are lost, | |
such as when sharing the image between currently incompatible | |
services like text messaging. By using the watermark, it is | |
possible to retrieve a restored, signed version of the image | |
from before the data was decoupled, which you can try in the | |
verify tab. | |
</p> | |
</div> | |
</div> | |
<div class="display-verify"> | |
<form class="verify-upload-form" enctype="multipart/form-data"> | |
<div class="form-group mb-3"> | |
<label>Upload image</label> | |
<input | |
type="file" | |
class="form-control" | |
name="fileUpload" | |
id="fileUpload" | |
/> | |
</div> | |
<div class="form-check mb-3"> | |
<input | |
class="form-check-input" | |
type="checkbox" | |
value="" | |
id="verify-terms" | |
/> | |
<label class="form-check-label" for="defaultCheck1"> | |
By using this demo you agree to the terms and conditions of | |
<a | |
href="https://truepic.com/terms-and-conditions/" | |
target="_blank" | |
>Truepic</a | |
> | |
and | |
<a href="https://steg.ai/tos" target="_blank">Steg.ai</a> | |
</label> | |
</div> | |
<button | |
type="submit" | |
class="btn btn-primary" | |
id="verify-button" | |
> | |
Submit | |
</button> | |
</form> | |
<div class="how-it-works"> | |
<p><strong>How it works</strong></p> | |
<p> | |
When an image is generated and signed with C2PA Content | |
Credentials, an imperceptible digital watermark, powered by | |
Steg.ai, is also added to the image pixels. The watermark | |
serves as a backup in case the Content Credentials are lost, | |
such as when sharing the image between currently incompatible | |
services like text messaging. By using the watermark, it is | |
possible to retrieve a restored, signed version of the image | |
from before the data was decoupled, which you can try in the | |
verify tab. | |
</p> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</body> | |
<script> | |
const generateTab = document.querySelector("#generateTab"); | |
const verifyTab = document.querySelector("#verifyTab"); | |
const displayVerify = document.querySelectorAll(".display-verify"); | |
const displayGenerate = document.querySelectorAll(".display-generate"); | |
const uploadForm = document.querySelector(".verify-upload-form"); | |
const imageGenForm = document.querySelector(".image-gen-form"); | |
const imagePrompt = document.getElementById("prompt"); | |
const model = document.getElementById("model"); | |
const generateTerms = document.getElementById("generate-terms"); | |
const generateButton = document.getElementById("generate-button"); | |
const fileUpload = document.getElementById("fileUpload"); | |
const verifyButton = document.getElementById("verify-button"); | |
const verifyTerms = document.getElementById("verify-terms"); | |
const generateImageContainer = document.querySelector( | |
".display-generate .image-container" | |
); | |
const generateActionMenu = document.querySelector(".action-menu"); | |
const verifyImageContainer = document.querySelector( | |
".display-verify .image-container" | |
); | |
const uploadedImageContainer = document.querySelector("#original-image"); | |
const downloadButton = document.getElementById("download-button"); | |
const verifyResultDescription = document.getElementById( | |
"verifyResultDescription" | |
); | |
const verificationOutput = document.getElementById("verification-output"); | |
const certificateList = document.getElementById("certificate-list"); | |
const outputContainer = document.querySelector(".output-container"); | |
const contentCredentialsIcon = document.getElementById( | |
"content-crendentials-icon" | |
); | |
const digitalWatermarkIcon = document.getElementById( | |
"digital-watermark-icon" | |
); | |
var certificates = []; | |
[imagePrompt, model, generateTerms].forEach((item) => { | |
item.addEventListener("change", async (event) => { | |
setGenerateButtonStatus(); | |
}); | |
}); | |
const setGenerateButtonStatus = () => { | |
if (imagePrompt.value && model.value && generateTerms.checked) | |
generateButton.classList.add("active"); | |
else generateButton.classList.remove("active"); | |
}; | |
[fileUpload, verifyTerms].forEach((item) => { | |
item.addEventListener("change", async (event) => { | |
setVerifyButtonStatus(); | |
}); | |
}); | |
const setVerifyButtonStatus = () => { | |
if (fileUpload.value && verifyTerms.checked) | |
verifyButton.classList.add("active"); | |
else verifyButton.classList.remove("active"); | |
}; | |
generateTab.addEventListener("click", (event) => { | |
event.target.classList.add("active"); | |
verifyTab.classList.remove("active"); | |
setGenerateElementsDisplay("block"); | |
setVerifyElementsDisplay("none"); | |
}); | |
verifyTab.addEventListener("click", (event) => { | |
event.target.classList.add("active"); | |
generateTab.classList.remove("active"); | |
setGenerateElementsDisplay("none"); | |
setVerifyElementsDisplay("block"); | |
}); | |
document | |
.getElementById("goto-verify-button") | |
.addEventListener("click", (event) => { | |
verifyTab.classList.add("active"); | |
generateTab.classList.remove("active"); | |
setVerifyElementsDisplay("block"); | |
setGenerateElementsDisplay("none"); | |
}); | |
function setGenerateElementsDisplay(displayStatus) { | |
displayGenerate.forEach((item) => { | |
item.style.display = displayStatus; | |
}); | |
} | |
function setVerifyElementsDisplay(displayStatus) { | |
displayVerify.forEach((item) => { | |
item.style.display = displayStatus; | |
}); | |
} | |
uploadForm.addEventListener("submit", (e) => { | |
e.preventDefault(); | |
submitForm(); | |
}); | |
function submitForm() { | |
if (!fileUpload.value || !verifyTerms.checked) | |
return | |
const file = document.getElementById("fileUpload").files[0]; | |
let fileReader = new FileReader(); | |
fileReader.readAsDataURL(file); | |
fileReader.onload = function () { | |
document | |
.getElementById("uploaded-image") | |
.setAttribute("src", fileReader.result); | |
}; | |
placeholder = document.querySelector(".display-verify .placeholder"); | |
spinner = document.querySelector(".display-verify .spinner"); | |
if (placeholder) placeholder.remove(); | |
if (document.getElementById("verifryresult")) | |
document.getElementById("verifryresult").remove(); // JCL make sure to remove the correct one | |
spinner.style.display = "block"; | |
outputContainer.style.display = "none"; | |
document.getElementById("resultLabel").style.display = "none"; | |
uploadedImageContainer.style.display = "none"; | |
const formData = new FormData(uploadForm); | |
// Add additional form data as needed | |
//formData.append('additionalData', 'additionalValue'); | |
// Call function to submit form data | |
submitFormData(formData); | |
} | |
function submitFormData(formData) { | |
fetch("verify", { | |
method: "POST", | |
body: formData, | |
}) | |
.then((response) => response.json()) | |
.then((data) => { | |
console.log(data); | |
if (data.contains_c2pa == "true") { | |
contentCredentialsIcon.setAttribute("class", "check-icon"); | |
} else { | |
contentCredentialsIcon.setAttribute("class", "x-icon"); | |
} | |
if (data.contains_watermark == "true") { | |
digitalWatermarkIcon.setAttribute("class", "check-icon"); | |
} else { | |
digitalWatermarkIcon.setAttribute("class", "x-icon"); | |
} | |
if ( | |
data.contains_c2pa == "true" && | |
data.contains_watermark == "true" | |
) { | |
verifyResultDescription.innerHTML = | |
"Your image contains content credentials, which are displayed below."; | |
} else if ( | |
data.contains_c2pa == "false" && | |
data.contains_watermark == "true" | |
) { | |
verifyResultDescription.innerHTML = | |
"The watermark was found, but image modifications were also detected. The last untampered, signed version on file is displayed."; | |
} else if ( | |
data.contains_c2pa == "true" && | |
data.contains_watermark == "false" | |
) { | |
verifyResultDescription.innerHTML = | |
"Your image contains content credentials, which are displayed below. A watermark was not detected."; | |
} else if ( | |
data.contains_c2pa == "false" && | |
data.contains_watermark == "false" | |
) { | |
verifyResultDescription.innerHTML = | |
"Nothing to show: this image contains neither content credentials, nor a watermark."; | |
} | |
uploadedImageContainer.style.display = "flex"; | |
document.getElementById("resultLabel").style.display = "block"; | |
document.querySelector(".display-verify .spinner").style.display = | |
"none"; | |
if (data.result_media != "n/a") { | |
const path = "/" + data.result_media; | |
var truepicDisplay = document.createElement("truepic-display"); | |
truepicDisplay.addEventListener( | |
"validate", | |
setVerificationOutputFromValidation | |
); | |
truepicDisplay.addEventListener( | |
"validate", | |
setCertificateOutputFromValidation | |
); | |
truepicDisplay.setAttribute("id", "verifryresult"); | |
truepicDisplay.setAttribute("active", ""); | |
var truepic = document.createElement("img"); | |
truepic.src = path; | |
truepicDisplay.appendChild(truepic); | |
verifyImageContainer.appendChild(truepicDisplay); | |
outputContainer.style.display = "block"; | |
} | |
// Handle response data | |
}) | |
.catch((error) => { | |
console.log(error); | |
// Handle errors | |
}); | |
} | |
const generateImage = async (text, model) => { | |
const inferResponse = await fetch( | |
`generate?prompt=${text}&model=${model}` | |
); | |
const inferJson = await inferResponse.json(); | |
return inferJson.response; | |
}; | |
imageGenForm.addEventListener("submit", async (event) => { | |
event.preventDefault(); | |
if (!imagePrompt.value || !model.value || !generateTerms.checked) return; | |
placeholder = document.querySelector(".display-generate .placeholder"); | |
spinner = document.querySelector(".display-generate .spinner"); | |
generateActionMenu.style.display = "none"; | |
// parameters.style.display = "none"; | |
try { | |
if (placeholder) placeholder.remove(); | |
if (document.getElementById("genresult")) | |
document.getElementById("genresult").remove(); // JCL make sure to remove the correct one | |
spinner.style.display = "block"; | |
const resp = await generateImage(imagePrompt.value, model.value); | |
const path = "/" + resp; | |
var truepicDisplay = document.createElement("truepic-display"); | |
truepicDisplay.setAttribute("id", "genresult"); | |
truepicDisplay.setAttribute("active", ""); | |
var truepic = document.createElement("img"); | |
truepic.src = path; | |
truepicDisplay.appendChild(truepic); | |
spinner.style.display = "none"; | |
generateImageContainer.appendChild(truepicDisplay); | |
downloadButton.href = path; | |
downloadButton.download = resp; | |
generateActionMenu.style.display = "block"; | |
/* | |
modelParam.innerHTML = model.value; | |
promptParam.innerHTML = textGenInput.value; | |
parameters.style.display = "block"; | |
*/ | |
} catch (err) { | |
console.error(err); | |
} | |
}); | |
function setVerificationOutputFromValidation(event) { | |
//verificationDetails.style.display = "block"; | |
return setVerificationOutput(event.detail.manifestStore.toJSON()); | |
} | |
function setCertificateOutputFromValidation(event) { | |
return setCertificateOutput(event.detail.manifestStore); | |
} | |
function setVerificationOutput(output = null) { | |
verificationOutput.innerHTML = ""; | |
if (!output) { | |
return; | |
} | |
const viewer = new JSONViewer(); | |
verificationOutput.appendChild(viewer.getContainer()); | |
viewer.showJSON(output); | |
} | |
function setCertificateOutput(manifestStore = null) { | |
const certificate = manifestStore?.activeManifest?.certificate; | |
if (!certificate) { | |
return; | |
} | |
certificates = [ | |
{ | |
der: certificate.der, | |
name: certificate.subjectName, | |
decoded: new x509.X509Certificate(certificate.der), | |
}, | |
...certificate.chain.map((certificate) => ({ | |
der: certificate.der, | |
decoded: new x509.X509Certificate(certificate.der), | |
})), | |
]; | |
certificates.forEach((certificate) => { | |
certificate.transformed = transformCert(certificate.decoded); | |
}); | |
console.log("certificates", certificates); | |
certificateList.innerHTML = ""; | |
certificates.forEach((certificate, index) => { | |
var li = document.createElement("li"); | |
if (index == 0) li.classList.add("active"); | |
li.appendChild( | |
document.createTextNode(certificate.transformed.subjectCommonName) | |
); | |
li.addEventListener("click", function (e) { | |
setCertificate(index); | |
const lis = document.querySelectorAll("#certificate-list li"); | |
lis.forEach((element) => { | |
element.classList.remove("active"); | |
}); | |
this.classList.add("active"); | |
}); | |
certificateList.appendChild(li); | |
}); | |
setCertificate(0); | |
} | |
function transformCert(certificate) { | |
const { | |
issuer, | |
subject, | |
notAfter: expired, | |
notBefore: issued, | |
serialNumber, | |
publicKey: { | |
algorithm: { | |
name: algorithm, | |
modulusLength: modulus, | |
namedCurve: namedCurve, | |
}, | |
}, | |
} = certificate; | |
const parsedSubject = parseCertificateValues(subject); | |
const parsedIssuer = parseCertificateValues(issuer); | |
return { | |
issuerCommonName: parsedIssuer["CN"], | |
issuerOrganizationUnit: parsedIssuer["OU"], | |
issuerOrganization: parsedIssuer["O"], | |
issuerCountry: parsedIssuer["C"], | |
subjectCommonName: parsedSubject["CN"], | |
subjectOrganizationUnit: parsedSubject["OU"], | |
subjectOrganization: parsedSubject["O"], | |
subjectCountry: parsedSubject["C"], | |
issued, | |
expired, | |
serialNumber, | |
algorithm, | |
modulus, | |
namedCurve, | |
}; | |
} | |
function parseCertificateValues(input) { | |
const params = new URLSearchParams(input.replaceAll(",", "&")); | |
const responses = {}; | |
for (const entry of params.entries()) { | |
responses[entry[0].trim()] = entry[1]; | |
} | |
return responses; | |
} | |
function setCertificate(ind) { | |
const certificate = certificates[ind].transformed; | |
document.querySelector(".details .issuerCommonName").innerHTML = | |
certificate.issuerCommonName; | |
document.querySelector(".details .issuerOrganizationUnit").innerHTML = | |
certificate.issuerOrganizationUnit; | |
document.querySelector(".details .issuerOrganization").innerHTML = | |
certificate.issuerOrganization; | |
document.querySelector(".details .issuerCountry").innerHTML = | |
certificate.issuerCountry; | |
document.querySelector(".details .subjectCommonName").innerHTML = | |
certificate.subjectCommonName; | |
document.querySelector(".details .subjectOrganizationUnit").innerHTML = | |
certificate.subjectOrganizationUnit; | |
document.querySelector(".details .subjectOrganization").innerHTML = | |
certificate.subjectOrganization; | |
document.querySelector(".details .subjectCountry").innerHTML = | |
certificate.subjectCountry; | |
document.querySelector(".details .issued").innerHTML = certificate.issued; | |
document.querySelector(".details .expired").innerHTML = | |
certificate.expired; | |
document.querySelector(".details .serialNumber").innerHTML = | |
certificate.serialNumber; | |
document.querySelector(".details .algorithm").innerHTML = | |
certificate.algorithm; | |
/* | |
if (certificate.modulus !== undefined) { | |
document.querySelector(".details .modulus").innerHTML = | |
certificate.modulus; | |
document.querySelector("#modulusContainers").style.display = "block"; | |
} else { | |
document.querySelector("#modulusContainers").style.display = "none"; | |
} | |
*/ | |
if (certificate.namedCurve !== undefined) { | |
document.querySelector(".details .namedCurve").innerHTML = | |
certificate.namedCurve; | |
document.querySelector("#curveContainer").style.display = "block"; | |
} else { | |
document.querySelector("#curveContainer").style.display = "none"; | |
} | |
} | |
</script> | |
</html> | |