Javascript fetch gif using POST and display - javascript

I was scouring through numerous other questions exactly asking the same. But couldnt get any of them to work in my case.
Basically what I need is to fetch a gif file from an API and display it. The image returned is a base64 encoded gif image built as shown below;
with io.BytesIO() as newfp:
....logic....
buf = base64.b64encode(newfp.getvalue()).decode()
return {
"statusCode": 200,
"headers": {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type",
"Content-Type": 'image/gif',
},
"body": buf,
"isBase64Encoded": True,
}
What I have come up is as shown below based on this link,
fetch('https://xxxxxxx.execute-api.us-west-2.amazonaws.com/v1/xxxx', {
method: 'POST',
body: ""
}).then((response) => {
response.arrayBuffer().then((buffer) => {
var base64Flag = 'data:image/gif;base64,';
var imageStr = arrayBufferToBase64(buffer);
var image = new Image();
image.src = base64Flag + imageStr;
document.body.appendChild(image);
});
});
function arrayBufferToBase64(buffer) {
console.log(buffer);
var binary = '';
var bytes = [].slice.call(new Uint8Array(buffer));
bytes.forEach((b) => binary += String.fromCharCode(b));
return window.btoa(binary);
};

Solved the problem with the below code.
var xhr = new XMLHttpRequest();
xhr.open("POST", "https://xxxxx.execute-api.us-west-2.amazonaws.com/v1/xxxx");
xhr.responseType = "blob";
xhr.onload = response;
xhr.send();
function response(e) {
(this.response.text().then(imageStr => {
var base64Flag = 'data:image/gif;base64,';
var image = new Image();
image.src = base64Flag + imageStr;
document.body.appendChild(image);
}));
}

Related

"InvalidImageSize", "message": "Image size is too small."

Trying to use Microsoft's Face API in Node.js but I am not able to load local images. What am I doing wrong? Thanks
I'm interfacing with a webcam and drawing the video out onto a canvas tag.
var canvas = document.getElementById("myCanvas"); // get the canvas from the page
var ctx = canvas.getContext("2d");
I have checked that I am getting an image using
var filename = new Date();
var imgData = canvas.toDataURL('image/jpeg');
var link = document.getElementById('saveImg');
link.href = imgData;
link.download = filename;
link.click();
and the image is saved fine...but I then try to do the following:
sendRequest(makeblob(imgData));
function sendRequest(imageURL) {
var returnData;
const request = require('request');
const subscriptionKey = '...';
const uriBase = 'https://eastus.api.cognitive.microsoft.com/face/v1.0/detect';
// Request parameters.
const params = {
'returnFaceId': 'true',
'returnFaceLandmarks': 'false',
'returnFaceAttributes': ''
};
const options = {
uri: uriBase,
qs: params,
body: '"' + imageURL + '"',
headers: {
'Content-Type': 'application/octet-stream',
'Ocp-Apim-Subscription-Key': subscriptionKey
}
};
request.post(options, (error, response, body) => {
if (error) {
console.log('Error: ', error);
return;
}
let jsonResponse = JSON.stringify(JSON.parse(body), null, ' ');
returnData = jsonResponse;
});
return returnData;
}
makeblob = function (dataURL) {
var BASE64_MARKER = ';base64,';
if (dataURL.indexOf(BASE64_MARKER) == -1) {
var parts = dataURL.split(',');
var contentType = parts[0].split(':')[1];
var raw = decodeURIComponent(parts[1]);
return new Blob([raw], { type: contentType });
}
var parts = dataURL.split(BASE64_MARKER);
var contentType = parts[0].split(':')[1];
var raw = window.atob(parts[1]);
var rawLength = raw.length;
var uInt8Array = new Uint8Array(rawLength);
for (var i = 0; i < rawLength; ++i) {
uInt8Array[i] = raw.charCodeAt(i);
}
return new Blob([uInt8Array], { type: contentType });
}
And this simply returns
{
"error": {
"code": "InvalidImageSize",
"message": "Image size is too small."
}
}
How else am I supposed to de/encode the image?
“InvalidImageSize”, “message”: “Image size is too small.”
According to Face API - V1.0, we could know that Faces are detectable when its size is 36x36 to 4096x4096 pixels. If need to detect very small but clear faces, please try to enlarge the input image. If your image with clear faces, you could enlarge the local image with online tool.
Higher face image quality means better detection and recognition precision. Please consider high-quality faces: frontal, clear, and face size is 200x200 pixels (100 pixels between eyes) or bigger.
JPEG, PNG, GIF (the first frame), and BMP format are supported. The allowed image file size is from 1KB to 6MB.
Faces are detectable when its size is 36x36 to 4096x4096 pixels. If need to detect very small but clear faces, please try to enlarge the input image.
I know I am late here, but I have find something which surely helps you.
Sadly, the Emotion and Face APIs do not support chunked transfers, as noted here. The 'workaround' is to load the image bits synchronously prior to making the web request. The code snippet for that should be like this:
const request = require('request');
const fs = require('fs');
function sendRequest(imageData) {
const uriBase = 'https://eastus.api.cognitive.microsoft.com/face/v1.0/detect';
// Request parameters.
const params = {
'returnFaceId': 'true',
'returnFaceLandmarks': 'false',
'returnFaceAttributes': ''
};
const options = {
uri: uriBase,
qs: params,
body: fs.readFileSync(imgData),
headers: {
'Content-Type': 'application/octet-stream',
'Ocp-Apim-Subscription-Key': subscriptionKey
}
};
request.post(options, (error, response, body) => {
if (error) {
console.log('Error: ', error);
return;
}
let jsonResponse = JSON.stringify(JSON.parse(body), null, ' ');
returnData = jsonResponse;
});
return returnData;
}

Uploading JavaScript Blob Issue

I'm trying to record a video (already working) using HTML5 video tag, "getUserMedia" to access the device camera and MediaRecorder API to capture the frames and Angular1 to handle the file uploading. Now I'm having trouble uploading the Blob to my PHP server which is running on Laravel, I currently have 2 ways to upload the video, first is by "ng-click" this works fine but when I programmatically upload the Blob using the same function which "ng-click" run it seems to break the mimeType of my Blob here's how my code looks.
$scope.uploader = function() {
let fData = new FormData();
let blob = new Blob($scope.chunk, { type: 'video/webm' });
fData.append('vid', blob)
$http.post(url, fData, {
transformRequest: angular.identity,
headers: {'Content-Type': undefined},
}, success, error)
})
$timeout(function() {
$scope.uploader();
}, 10000)
This issue here is when the "$scope.uploader()" is called using "ng-click" it works fine but when calling the "uploader" method using the "$timeout" it seems to change the mimeType to "application/octet-stream" which causes the issue.
Hello Try this code,
function base64ToBlob(base64Data, contentType) {
contentType = contentType || '';
var sliceSize = 1024;
var byteCharacters = atob(base64Data);
var bytesLength = byteCharacters.length;
var slicesCount = Math.ceil(bytesLength / sliceSize);
var byteArrays = new Array(slicesCount);
for (var sliceIndex = 0;sliceIndex <slicesCount;++sliceIndex) {
var begin = sliceIndex * sliceSize;
var end = Math.min(begin + sliceSize, bytesLength);
var bytes = new Array(end - begin);
for (var offset = begin, i = 0;offset <end;++i, ++offset) {
bytes[i] = byteCharacters[offset].charCodeAt(0);
}
byteArrays[sliceIndex] = new Uint8Array(bytes);
}
return new Blob(byteArrays, {
type: contentType});
}
Define scope
$scope.onFile = function(blob) {
Cropper.encode((file = blob)).then(function(dataUrl) {
$scope.dataUrl = dataUrl;
$scope.odataUrl = dataUrl;
$timeout(showCropper); // wait for $digest to set image's src
});
};
Submit method
$scope.uploadImage = function () {
if ($scope.myCroppedImage === '')
{
}
$scope.msgtype = "";
$scope.msgtxt = "";
var fd = new FormData();
var imgBlob = dataURItoBlob($scope.myCroppedImage);
fd.append('clogo', imgBlob);
fd.append('actionfile', 'editimage');
$http.post(
'../user/user_EditCompany.php',
fd, {
transformRequest: angular.identity,
headers: {
'Content-Type': undefined
}
}
)
.success(function (response) {
// console.log(response);
if (response.status == 'success')
{
//your code
}else{
//your code
}
})
.error(function (response) {
console.log('error', response);
});
};
function dataURItoBlob(dataURI) {
var binary = atob(dataURI.split(',')[1]);
var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];
var array = [];
for (var i = 0; i < binary.length; i++) {
array.push(binary.charCodeAt(i));
}
return new Blob([new Uint8Array(array)], {
type: mimeString
});
}
Thanks, the issue was caused by upload and post limit in my php.ini.

Image Blob - Pop up window not showing content

I am working on displaying an image in blob
const file = new Blob([response.data], { type: 'image/png' });
const fileURL = URL.createObjectURL(file);
window.open(fileURL);
console.log('fileURL', fileURL);
const image = document.createElement('img');
I am getting this response
PNG
IHDR2²OósRGB®ÎégAMA±üa pHYsÃÃÇo¨dã¼IDATx^ìý=ô<Ó¶Þ;Ðزg
cõ-EVÙÚÜòfã·#cV0¡\¡5È·Eüü$
]EGâ¹AD" èî÷½þûeü÷_ý×ð\¸³µ°ñ F\¸³µ°ñ F\¸³µ°ñ F\¸³µ°ñ FáË¥ßçòüù«&Þv~ÑTU?ÿ^Ý}öòóÜï×'sµùÔ¤|¼þåúE8ñp
ÎÛ3Î0
¹Þuv]áÌäZ×G#¬Ïv\f.ý{ý>ã¦ñï÷õÈõÁÛÅØã·n6«ÿÞpÙ.2ßƹod
ÕEk+bv|_Hß5yÏçnØ1C´ìOÑÞøVÛ|¯
¤=ûsÔ_}BKíWÊó÷¿ç¯×ò_Þòu¸ÈÜ\dF¾a
¯FXí¸.Í\"øãñXÈ|8°àmÇýØa)#
F;L æ Øñæá_ðɾ´o»ìÔq­#ºMxVÍ_±S.|íËs¿4×â7÷="/2GýÝù¹uyZ{c6Â×·°ãÛÆyqÎ_\ÁFøÎ)¼>a}¶àº¸r)Ý
ÄÛ® ÖáÌÁÁ\à"ó'â7|ú®HNë'/Õû
D ¶³c.!lÿȶËhê¾ü"sÆß4ò#^¦¬XxcvÒ׷̽ÀEf$.í7a}¶àº4s)úáñtøÖEÛ´´ve¬í`Ê"UWäÃÛèÁ¿X}½2få¯âH²×n£Îy)U|Íg­]O½{þ«}Ï®¹JÈ-ýðkÙʯóR¸/ZLBd[NN±¨ùÖÌ¥x}³è­:#"9¯yoL÷ÿBÎj¸Õvöú_æ´v8çï.òú©Cµ-/óÊõÑ _{¹³æb[.êòRÙÖ£'lµo¦¬aò/ÌÕ1>ÿ¶ª<hPù½nüä7Ûzéú]Ïúg­ó=ó´ÞÛúºÓ¾KéåSÀÅ [¥8ìµÛ¸üí®õFxOáõÑë³×¥KËG1ýÿF&Êw\c´Û6¶Éðqò&¹mYÐÜ#ÝÄØtÓYÇÈ>Ó'´«6"áW±5k>\¥ñ[ëñîùkö[1X}Íãq{1K_1o­­Óç÷1ãÕ8t*Þå+V±]/G´v5ZI¨Úɹ¼Æ5­
µç°½Ö¿cþîÄÊLò#¬1CƳ¾zrgô·mbÄjïøÛX9N½oû½â²ÖhÏZßlë]ßo=Ku³MâáôÙ;V¼W·õN}¹­¾ýíÛH¨>4ðÆ`>çyÐ9x¾¿¢_;GC[
¯FXí¸.¾\J;Ýu¼í2ñÃccçp^iL|CJ~\vv­obú<ª
Ê··á¾Þ9k¸ðÞùöwÆ ö£ôUcgû"þÏZ5Ú7xoìq*ò¡Zå#UÛySÞkc+{gsÛw¥Me;û²ÆS×_Ò&wzßwÉýçSoçÙÂ_#2Vïÿ¶3F¬FÏoýVì÷{¾YûÇï4WÍnÀ²]û·wãÖ;ÙëÅÂ>¯´g6®qÕüìåD·ñøëYë°®^°>Ûp]þ4´õáUíøH}{cø63}ccªXòâÙØ%yCZÛx6±óÉã/ýºkyïü
ûyncÖTYü¯b`Ì)|ÜrY÷9ÁÛªßA7úÖGSÿ]sßrSôÉöëÒË
¥Ms.Ù'ÕfÏ_ï¼
´<`Äï-¯­+f#|ÈÜÑsqä·1bõÖñßddÐ7k¿óø½å¨Gíêùéy7·wܾ5¦k\#.¢ÂSx}4ÂúlÀuùÓ\ÒHãÀí\Yîæªí£§ÞÄV±AiÞóñHÜÏgyBØR:ñîù«ö¶¦r®úF޳ث¦®=+°\Ö}Î0_µ4y§o
ÝXi(ßv}ïGDrkåËË%_íò}æÿnÙ<ìoÖväÉ _ÿâÛV1â°g|1\Ìñ{ë³cmZ߬ùÎí·½ß[¶Óóìßéy_onÏe!å%ïçÏq׸Æ<´ý¾÷týu¯õFxNáõÑë³×åOsI; ["ÅhçÛÈä¸qDüL´¡o¸cçCIïÚ¶ß;Ã~sMùsßÅÿ*¿9½»#Ç;°voõâFãÛ®|<(ùÁî³ãð=Úa±mÎå'Ù¬:HùÛAËN«×C¾úrgì·ÝÀùñ;hùNi¼ßóÍÚïöûlm},ÛÕóÓóðÆ[>çö|öKnÒ²/¿]ãqù
ìPx}4ÂúlÀuùÓ\j~àv»6²î :Ã61µ
ÛÄbÞ&;v>5}ÛoxrfMc1cvµ9;õặbZÞ/ëú: ßìÛ7VªøUúÉyNtN;iîÖþbÌÍ°|ÿÿÕ
®õ·V¯G|uæÎÐo»Sãwáv8¶Ý=߬ýîßud¯}n³úwvÇ×ÛsÙ¹pîÛs«~,&ïÄ寽\ëñúhõÙëò§¹¤Æ#Ñ ßF¹7ź{h×ßàmb_IðÐ6ä+ï£æì±­yPÞ=ï¦v$ÙðlóÃ7¦Ö. ÆX°q)Ù¾×ñó­àöm/v+ñ=ó5çz[=;+ùÀg1Ù+6,Û$¿Ö
÷·­6^ùêË=e_7ê·Ý¢õ=÷Ç/kXwíÚq~/8µ÷ö3ým½ëúÞÓo9ÇûW­¿bóÜ<¯7·çµ¿æ"+Ô×®qY,qì5æ
ßȺ¸üÝ£¡-×G#¬Ïv\?Í¥øᱬuðròKþè½Yl©´¥Â¸Mlû°ø¨mtÜNSÝùlvøØ÷Îß°ãÂ×´ö=ôIµòÃ1fDɯ}³îsMséÏÓ²Kö_í7È·o¬8Ú·(öóÂß­&õÖïV¬ñ,´q­95l¯¾üþÖmJm-ZyPèæ#fG}uä'¾ítÙ=ãù8óMä·+â+íê~&Zï"¿¹4«mfC]÷Ãó8¾ÞÜÏ~ªó¼I}÷¬igÜ3>Ø?»ßÉwähxOáõÑë³×¹ôÙèóBÞ°Ô|é"£¬-|æùóÅ|Ûú.gk?aâ¹ôáärÕUù3àïÅø©/|W¼È|Ýú&gk?a}¶àº ¾ñ+û¥X¾$|ÆÁ%/2o;Sßào°AáõÑë³×¹pfk?aâ¹pfk?aâ¹pfk?aâ¹pfk?aâ¹pfk?aâ¹pfk?aâ¹pfk?aâ¹pfk?aâ¢K?Ïð/á_ÿff¬á=þÑ´4?Ïsÿøz}Ä2ßç?þU{ß7?¾!׿ÏÈð´´ß;Ög;®K+ :¾¯¿Èhÿ¢ñZÈ¿Û±)ßèût9ýÿXc\dt¼>bO
.2À.2`#^°>Ûp]¾*þ½~>!zAþzþÆøóÅÇ¿ß× bó"S?K¾:sg¬ÎýcâÛà|¼)Ny>xóããóè¯÷àSà"6fk?aý«Ä'øh¾*¾úÀ_ÏÿSDlþMKåó"³x.A×ò_
±:÷OooðñVà"3¿ÞOØ­ýõ¯à£iå<TÒFÚÄÒÙÍcÿ(ë~j·|#¬¶Y»ô§¬tĨ5>ßÈ×vÄÊGî·NOÌ?À×p×ZãocXgÇjò¼û"Sæb%m®%gÔxr[æ8+¾G×?0ÂG¯º}oOYñδ+¦[ï"Ü6¿ÐµÁÉïµHÝ1#Û)~Br{,>imø÷×9Ã×qoÝbkWç*¾ÎqÏÚ­ü×÷¹6­ñ×wlîjxæîÍpKB^Px}4ÂúlÀuiåR}øäd]5lÞê&IýÃ&Ê4Mp²
V{7ïÖm|2bWósE´çÖÔK·¿>¾§ó$;ÆÒ0ÆwÛtÄ¢G¯Ñ¶6oÇÔ~Js®|Äÿ3s_·->¦g¥Möëí)ó,´ÞIÛʳ¦
When I reffer some stackoverflow answers I understood my response is wrong. my questions is how to get correct binary response ..
You can use XMLHttpRequest() or fetch() to get response as Blob
let url, w;
let request = fetch("/path/to/image");
request.then(function(response) {
return response.blob() // return `Promise` which resolves to `Blob` of response
})
.then(function(blob) {
url = URL.createObjectURL(blob);
w = window.open(url);
})
.catch(function(err) {
console.log(err)
});
let url, w;
let request = new XMLHttpRequest();
request.open("GET", "/path/to/image");
request.responseType = "blob"; // set `responseType` to `"blob"`
request.onload = function() {
url = URL.createObjectURL(blob);
w = window.open(url);
}
request.onerror = function(e) {
console.log(e);
}
request.send(null);
$http.get(ur1, {
responseType: 'blob'
}).then(function (res) {
this.download(res)
}, function (err) {
conseole.log(err)
})
}
};
this.download = function (data) {
var type = data.headers('Content-Type'),
content = data.data,
fileName = data.headers('x-ms-meta-originalname'),//Get the file name
blob = new Blob([content], {type: type}),
url = window.URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.target="_self";
a.download = fileName;
a.click();
a = undefined;
}
Additinal Details here: http://angularjstricks.blogspot.in/2017/03/download-file-from-server-with-http-in.html

Convert Blob string URL to Blob then to base64

I have an image element where I get the blob string URL from and I'm trying to convert it to a blob first then to base64 string. so that I can send the base64 string (this is stored in #originalImage) to server side.
JS
onFinished: function (event, currentIndex) {
var form = $(this);
if ($('#image').attr('src').length) {
var selectedFile = $('#image').attr('src');
var blob;
var reader = new window.FileReader();
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
if (this.readyState == 4 && this.status == 200) {
blob = this.response;
console.log(this.response, typeof this.response);
if (blob != undefined) {
reader.readAsDataURL(blob);
}
}
}
xhr.open('GET', selectedFile);
xhr.responseType = 'blob';
xhr.send();
}
reader.onloadend = function () {
base64data = reader.result;
console.log(base64data);
if (base64data != undefined) {
$("#originalImage").val(base64data);
form.submit();
}
}
}
Controller
[HttpPost]
public ActionResult Action(Model blah, string croppedImage, string originalImage){
// Code here...
}
It works as expected but my only concern is that where I submit the form which is inside reader.onloadend. is there any problem with this approach or is there any better approach than this?
I appreciate any help on this, Thanks!
Don't use base64, send the binary to the server, save time, process, memory and bandwidth
onFinished(event, currentIndex) {
let src = $('#image').attr('src')
if (src.length) {
fetch(src)
.then(res =>
res.ok && res.blob().then(blob =>
fetch(uploadUrl, {method: 'post', body: blob})
)
)
}
}
What you could also do is using canvas and avoid another request (but this will convert all image to png)
onFinished(event, currentIndex) {
let img = $('#image')[0]
if (!img.src) return
let canvas = document.createElement('canvas')
let ctx = canvas.getContext('2d')
canvas.width = img.width
canvas.height = img.height
ctx.drawImage(img, 0, 0)
canvas.toBlob(blob => {
fetch(uploadUrl, {method: 'post', body: blob})
})
}

Converting image dataUrl to Blob image for AJAX POST with javascript

I have the following code which will take an image, allow the user to crop (with other code not shown or necessary for this question), and then render the image in base64 from canvas.
I need to be able to convert the image to binary, as the API endpoint its being submitted to can't take base64. I have functionality to convert to a Blob, but I'm not sure how to implement it correctly:
$(function () {
var fileInput = document.getElementById("file"),
renderButton = $("#renderButton"),
submit = $(".submit"),
imgly = new ImglyKit({
container: "#container",
ratio: 1 / 1
});
// As soon as the user selects a file...
fileInput.addEventListener("change", function (event) {
var file;
var fileToBlob = event.target.files[0];
var blob = new Blob([fileToBlob], {
"type": fileToBlob.type
});
// do stuff with blob
console.log(blob);
// Find the selected file
if (event.target.files) {
file = event.target.files[0];
} else {
file = event.target.value;
}
// Use FileReader to turn the selected
// file into a data url. ImglyKit needs
// a data url or an image
var reader = new FileReader();
reader.onload = (function (file) {
return function (e) {
data = e.target.result;
// Run ImglyKit with the selected file
try {
imgly.run(data);
} catch (e) {
if (e.name == "NoSupportError") {
alert("Your browser does not support canvas.");
} else if (e.name == "InvalidError") {
alert("The given file is not an image");
}
}
};
})(file);
reader.readAsDataURL(file);
});
// As soon as the user clicks the render button...
// Listen for "Render final image" click
renderButton.click(function (event) {
var dataUrl;
imgly.renderToDataURL("image/jpeg", {
size: "1200"
}, function (err, dataUrl) {
// `dataUrl` now contains a resized rendered image
//Convert DataURL to Blob to send over Ajax
function dataURItoBlob(dataUrl) {
// convert base64 to raw binary data held in a string
var byteString = atob(dataUrl.split(',')[1]);
// separate out the mime component
var mimeString = dataUrl.split(',')[0].split(':')[1].split(';')[0];
// write the bytes of the string to an ArrayBuffer
var ab = new ArrayBuffer(byteString.length);
var ia = new Uint8Array(ab);
for (var i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
// write the ArrayBuffer to a blob, and you're done
//var bb = new BlobBuilder();
//bb.append(ab);
//return bb.getBlob(mimeString);
return new Blob([ab], {
type: 'image/jpeg'
});
}
var blob = dataURItoBlob(dataUrl);
//console.log("var blob: " + blob);
//var fd = new FormData(document.forms[0]);
var image = $("<img><br>").attr({
src: dataUrl
});
image.appendTo($(".result"))
$removeButton = $('<button class="btn btn-default remove">')
.text('Remove ' + imageid.value).appendTo($(".result"))
.on('click', function () {
panel.remove();
$(this).remove();
return false;
});
$submitButton = $('<div class="btn btn-default submit"></div>')
.text('Submit ' + imageid.value).appendTo($(".result"))
.on('click', function () {
var fd = new FormData;
fd.append('file', blob, 'image.png');
//console.log("var fd: " + fd);
var xhr = new XMLHttpRequest();
var saveImage = encodeURIComponent(dataUrl);
//console.log("SAVE IMAGE: " + saveImage);
//console.log(saveImage);
fd.append("myFile", blob);
xhr.open('POST', 'http://url.com/rest/v1/utils/guid/encode?' + saveImage + '&imageid=' + imageid.value, true);
xhr.send(fd);
});
});
});
});
On Submit, I get the following in the console:
http://url.com/rest/v1/utils/guid/encode?data%3Aimage%2Fjpeg%3Bbase64%2C%2F…CiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAP%2FZ
The current version in jsFiddle: LINK

Categories