Skip to content

Instantly share code, notes, and snippets.

@CivBase
Created March 12, 2021 04:46
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save CivBase/92f1b387c4c98d24481ec28ea2fccc0d to your computer and use it in GitHub Desktop.
Save CivBase/92f1b387c4c98d24481ec28ea2fccc0d to your computer and use it in GitHub Desktop.
Make Your Photo 16x9 (plus other aspect ratios)
// Original code from einaregilsson:
// https://github.com/einaregilsson/photo16x9/tree/6ae29823e8bc57e15627795ce9c056197d8eb98f
<!DOCTYPE html>
<html>
<head>
<title>Make Your Photo 16x9</title>
<meta charset="utf8">
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<meta name="apple-mobile-web-app-title" content='Make Your Photo 16x9' />
<meta name="apple-mobile-web-app-capable" content="yes" />
<link rel="icon" href="favicon32.png" sizes="32x32" />
<link rel="icon" href="favicon192.png" sizes="192x192" />
<link rel="apple-touch-icon" href="icon.png" />
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:creator" content="@einaregilsson">
<meta name="twitter:title" content="Make your photo's aspect ratio 16x9 for Twitter">
<meta name="twitter:description" content="To avoid your short and wide photo being cropped badly on Twitter we pad it so it fits Twitter's 16x9 aspect ratio.">
<meta name="twitter:image" content="https://photo16x9.com/share.png">
<meta property="og:title" content="Make Your Photo 16x9" />
<meta property="og:description" content="To avoid your short and wide photo being cropped badly on Twitter we pad it so it fits Twitter's 16x9 aspect ratio." />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://photo16x9.com/" />
<meta property="og:image" content="https://photo16x9.com/share.png" />
<style>
body {
text-align: center;
font-family: Arial, sans-serif;
background:white;
color:#646369 ;
padding: 0px;
margin: 0px;
}
a, a:visited {
color: #06f;
}
html {
padding: 0px;
}
#canvas-wrapper {
display: inline-block;
position: relative;
width: auto;
max-width: 80%;
margin: auto;
}
h1 {
font-size: 30px;
margin-top: 0px;
padding: 30px 0px;
color:white;
background:linear-gradient(#007bff, #04f);
}
h3 {
font-size: 60px;
line-height: 60px;
margin-top: 20%;
}
h4 {
padding: 0px 10px;
font-size: 14px;
font-weight: normal;
}
h5 {
margin-top: -5%;
font-size: 20px;
}
#instructions {
width: 100%;
height: 100%;
position: absolute;
top: 0px;
left: 0px;
pointer-events: none;
}
canvas {
position: relative;
display: block;
border:solid 7px white;
box-shadow: gray 0 0 5px;
background:#e3e3e3;
border-radius: 1px;
max-width: 100%;
box-sizing: border-box;
}
button {
width: 120px;
height: 30px;
border-radius: 5px;
background: #007bff;
color: white;
margin: 5px;
border: solid 0px darkblue;
border-bottom-width: 2px;
font-size: 14px;
}
button:hover {
background: #05f;
}
button:focus {outline:0;}
button:active {
height: 29px;
border-bottom-width: 1px;
outline: none;
position: relative;
top: 1px;
}
#download-wrapper {
height: 30px;
}
#download-image {
display: block;
margin: 20px auto;
width: 160px;
font-weight: bold;
}
input {
font-size:20px;
width:60px;
}
label {
font-size:20px;
}
p {
max-width: 600px;
margin: 20px auto;
text-align: left;
font-size: 14px;
padding: 0px 10px;
}
#controls {
max-width: 90%;
margin: auto;
}
#upload-file {
display: none;
}
#tp {
display: none;
}
#canvas-wrapper span {
font-size:14px;
margin-top: 8px;
display: inline-block;
display:none;
}
#render {
position: relative;
display: none;
border:solid 7px white;
box-shadow: gray 0 0 5px;
background:#e3e3e3;
border-radius: 1px;
max-width: 100%;
box-sizing: border-box;
}
@media (max-width: 700px) {
h3 {
font-size: 40px;
line-height: 40px;
}
h5 {
font-size: 16px;
}
}
@media (max-width: 500px) {
h1 {
padding: 10px 0;
font-size: 24px;
}
h3 {
font-size: 34px;
line-height: 34px;
}
h5 {
font-size: 14px;
}
}
@media (max-width: 359px) {
h3 {
font-size: 28px;
line-height: 28px;
}
h5 {
font-size: 12px;
}
}
@media (min-width: 850px) {
#canvas-wrapper {
max-width: 600px;
}
}
</style>
</head>
<body onload="setup()">
<h1>Make your photo 16x9</h1>
<h4>The easy way to make your photo look good in a tweet!</h4>
<div id="canvas-wrapper">
<canvas id="c" width="800" height="450"></canvas>
<img id="render" />
<div id="instructions">
<h3>Drop photo here</h3>
<h5>Or click to choose a photo</h5>
</div>
<span id="long-click">(Long-click on image to save directly to Photos...)</span>
</div>
<div id="download-wrapper">
<button id="download-image">Download Image</button>
</div>
<div id="controls">
<h4>Aspect Ratio:</h4>
<input
type="number"
min="1"
id="aspect-horizontal"
value="16" />
<span>x</span>
<input
type="number"
min="1"
id="aspect-vertical"
value="9" />
<h4>Fill empty space with:</h4>
<button id="detected-color">Detected color</button> </button><button id="black">Black</button> <button id="white">White</button> <button id="transparent">Transparency</button> <button id="blur">Blur</button>
</div>
<p>
Have you ever been tweeting out an image that's very short and wide, maybe it's just a couple of lines of text, and it gets cropped horribly so people have to
click on the image to even see the full text? Well, here you can pad that image out to the aspect ratio 16x9, which is what Twitter shows it as,
so people can read your image text immediately.
</p>
<p>
This page was made by <a href="https://twitter.com/einaregilsson">@einaregilsson</a>, give me a shout-out on Twitter if you find it useful.
You can also read the <a href="https://einaregilsson.com/make-your-photo-16x9/">blog post</a> about how it came to be if you're interested in the technical stuff.
</p>
<p>
<strong>Cookie policy:</strong> We don't use any cookies or tracking scripts, at all. In fact you can see the entire source code of this page
by clicking on View Source in your browser, it's all in this one file.
</p>
<div id="tp">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="32" height="32">
<rect fill="white" x="0" y="0" height="16" width="16" />
<rect fill="white" x="16" y="16" height="16" width="16" />
</svg>
</div>
<input type="file" id="upload-file"/>
<script>
//Globals, always apply to current image
var aspectHorizontal = 16;
var aspectVertical = 9;
var canvas, ctx, image, filename;
var dx, dy;
var isLandscape;
var currentType;
function changeAspect() {
aspectHorizontal = parseInt(document.getElementById('aspect-horizontal').value);
aspectVertical = parseInt(document.getElementById('aspect-vertical').value);
setDimensions();
drawImage();
}
function noImage() {
alert('Start by uploading an image.');
}
function downloadImage() {
if (!image) {
return noImage();
}
var dataURL = canvas.toDataURL('image/png');
var a = document.createElement('a');
a.href = dataURL;
a.target = '_blank';
var suffix = aspectHorizontal.toString() + 'x' + aspectVertical.toString() + '.png';
var downloadFilename = filename.replace(/\.[a-zA-Z]{3,4}$/, '_' + suffix);
//If it's an iOS generated guid name, change it to something better...
if (downloadFilename.match(/[A-F0-9]{8}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{12}/)) {
downloadFilename = 'photo_' + suffix;
}
a.setAttribute('download', downloadFilename);
a.click();
}
function fillBlack() {
currentType = 'black';
fillColor('black');
}
function fillWhite() {
currentType = 'white';
fillColor('white');
}
function fillTransparent() {
if (!image) {
return noImage();
}
currentType = 'transparent';
canvas.width = canvas.width;
var base64encoded = btoa(document.getElementById('tp').innerHTML);
document.getElementById('render').style.backgroundImage = 'url(data:image/svg+xml;base64,' + base64encoded + ')';
drawImage();
}
function rgbToHex(r, g, b) {
return ((r << 16) | (g << 8) | b).toString(16);
}
function fillDetectedColor() {
if (!image) {
return noImage();
}
currentType = 'detected';
var counter = {};
function addPixel(x, y) {
var pixel = ctx.getImageData(x, y, 1, 1).data;
var hexColor = '#' + rgbToHex(pixel[0], pixel[1], pixel[2]);
counter[hexColor] = (counter[hexColor] || 0) + 1;
}
if (isLandscape) {
for (var i=0; i < image.width; i++) {
addPixel(i, dy);
addPixel(i, dy + image.height - 1);
}
} else {
for (var i=0; i < image.height; i++) {
addPixel(dx, i);
addPixel(dx + image.width - 1, i);
}
}
var maxColor, maxCount = 0;
for (var c in counter) {
if (counter[c] > maxCount) {
maxColor = c;
maxCount = counter[c];
}
}
fillColor(maxColor);
}
var currentBlur = 8;
function fillBlur() {
if (!image) {
return noImage();
}
if (currentType === 'blur') {
currentBlur++;
if (currentBlur > 20) {
currentBlur = 8;
}
} else {
currentBlur = 8;
}
currentType = 'blur';
//Don't want checkered background here...
document.getElementById('render').style.backgroundImage = '';
canvas.width = canvas.width;
if (typeof ctx.filter !== 'undefined') {
ctx.filter = 'blur(' + currentBlur + 'px)';
ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
ctx.filter = 'none';
} else { //For Safari...
ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
blur(canvas, currentBlur);
}
drawImage();
}
function clickCanvas() {
document.getElementById('upload-file').click();
}
function fillColor(color) {
if (!image) {
return noImage();
}
ctx.fillStyle = color;
ctx.fillRect(0, 0, canvas.width, canvas.height);
drawImage();
}
function drawImage() {
if (image) {
ctx.drawImage(image, dx, dy, image.width, image.height);
}
var img = document.getElementById('render');
img.style.display = 'block';
canvas.style.display = 'none';
img.src = canvas.toDataURL();
}
function setup() {
canvas = document.getElementById('c');
ctx = canvas.getContext('2d');
var render = document.getElementById('render');
render.addEventListener('drop', drop);
render.addEventListener('dragover', function(ev) {
ev.preventDefault();
});
render.addEventListener('click', clickCanvas);
var fileUpload = document.getElementById('upload-file');
fileUpload.addEventListener('change', fileSelectedInDialog);
//Set up fill styles...
document.getElementById('black').addEventListener('click', fillBlack);
document.getElementById('white').addEventListener('click', fillWhite);
document.getElementById('transparent').addEventListener('click', fillTransparent);
document.getElementById('blur').addEventListener('click', fillBlur);
document.getElementById('detected-color').addEventListener('click', fillDetectedColor);
document.getElementById('download-image').addEventListener('click', downloadImage);
document.getElementById('aspect-horizontal').addEventListener('change', changeAspect);
document.getElementById('aspect-vertical').addEventListener('change', changeAspect);
drawImage();
}
function drop(ev) {
ev.stopPropagation();
ev.preventDefault();
var files = ev.dataTransfer.files;
if(files && typeof FileReader !== "undefined") {
readFile(files[0]);
} else {
alert("Can't read file 🙁");
}
}
function fileLoaded(e) {
image = new Image();
image.src = e.target.result;
image.onload = imageLoaded;
currentBlur = 8;
}
function imageLoaded(e) {
image = e.target;
document.getElementById('instructions').style.display = 'none';
if (navigator.userAgent.match(/iPhone|Android/)) {
document.getElementById('long-click').style.display = 'inline-block';
}
setDimensions();
fillTransparent();
}
function setDimensions() {
var w = 800;
var h = 450;
if (image) {
var w = image.width;
var h = image.height;
}
//Now, see which way it is...
var calculatedHeight = Math.round((w / aspectHorizontal) * aspectVertical);
var calculatedWidth = Math.round((h / aspectVertical) * aspectHorizontal);
dx = 0;
dy = 0;
//Need to pad above and below
if (calculatedHeight > h) {
isLandscape = true;
dy = Math.round((calculatedHeight - h) / 2);
canvas.width = w;
canvas.height = calculatedHeight;
} else {
isLandscape = false;
dx = Math.round((calculatedWidth - w) / 2);
canvas.width = calculatedWidth;
canvas.height = h;
}
}
function fileSelectedInDialog(e) {
readFile(e.target.files[0]);
}
function readFile(file) {
if (!/image/i.test(file.type)) {
alert('Only image files are supported!');
return;
}
filename = file.name;
var reader = new FileReader();
reader.onload = fileLoaded;
reader.readAsDataURL(file);
}
//Taken from https://github.com/forceuser/canvas-box-blur/ because Safari doesn't support the .filter property
function blur(canvas, radius) {
if (radius < 1) {
return;
}
var iterations = 3;
var width = canvas.width;
var height = canvas.height;
var context = canvas.getContext("2d");
var imageData = context.getImageData(0, 0, width, height);
var pixels = imageData.data;
var rsum; var gsum; var bsum; var asum;
var p; var p1; var p2;
var yp; var yi; var yw; var pa;
var wm = width - 1;
var hm = height - 1;
var rad1 = radius + 1;
var space = Math.pow(radius * 2, 2);
var r = [];
var g = [];
var b = [];
var a = [];
var vmin = [];
var vmax = [];
function getPx(i) {
var a = pixels[i + 3];
return [
a * pixels[i] / 255,
a * pixels[i + 1] / 255,
a * pixels[i + 2] / 255,
a,
];
}
while (iterations > 0) {
iterations--;
yw = yi = 0;
for (var y = 0; y < height; y++) {
var px = getPx(yw);
rsum = px[0] * rad1;
gsum = px[1] * rad1;
bsum = px[2] * rad1;
asum = px[3] * rad1;
for (var i = 1; i <= radius; i++) {
var px = getPx(yw + (((i > wm ? wm : i)) << 2));
rsum += px[0];
gsum += px[1];
bsum += px[2];
asum += px[3];
}
for (var x = 0; x < width; x++) {
r[yi] = rsum;
g[yi] = gsum;
b[yi] = bsum;
a[yi] = asum;
if (y == 0) {
p = x + rad1;
vmin[x] = (p < wm ? p : wm) << 2;
p = x - radius;
vmax[x] = p > 0 ? p << 2 : 0;
}
p1 = yw + vmin[x];
p2 = yw + vmax[x];
var px1 = getPx(p1);
var px2 = getPx(p2);
rsum += px1[0] - px2[0];
gsum += px1[1] - px2[1];
bsum += px1[2] - px2[2];
asum += px1[3] - px2[3];
yi++;
}
yw += (width << 2);
}
for (var x = 0; x < width; x++) {
yp = x;
rsum = r[yp] * rad1;
gsum = g[yp] * rad1;
bsum = b[yp] * rad1;
asum = a[yp] * rad1;
for (var i = 1; i <= radius; i++) {
yp += (i > hm ? 0 : width);
rsum += r[yp];
gsum += g[yp];
bsum += b[yp];
asum += a[yp];
}
yi = x << 2;
for (var y = 0; y < height; y++) {
pixels[yi + 3] = pa = Math.round(asum / space);
if (pa > 0) {
pa = 255 / pa;
pixels[yi] = Math.round(rsum / space * pa);
pixels[yi + 1] = Math.round(gsum / space * pa);
pixels[yi + 2] = Math.round(bsum / space * pa);
}
else {
pixels[yi] = pixels[yi + 1] = pixels[yi + 2] = 0;
}
if (x == 0) {
p = y + rad1;
vmin[y] = (p < hm ? p : hm) * width;
p = y - radius;
vmax[y] = p > 0 ? p * width : 0;
}
p1 = x + vmin[y];
p2 = x + vmax[y];
rsum += r[p1] - r[p2];
gsum += g[p1] - g[p2];
bsum += b[p1] - b[p2];
asum += a[p1] - a[p2];
yi += width << 2;
}
}
}
context.putImageData(imageData, 0, 0);
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment