// ==UserScript==
// @name Danbooru 2 Note Assist
// @description For danbooru.donmai (2) - experimental text-detection script to automatically fit notes to text
// @author itsonlyaname
// @namespace itsonlyaname
// @include http://*.donmai.us/posts/*
// @include https://*.donmai.us/posts/*
// @include http://donmai.us/posts/*
// @include https://donmai.us/posts/*
// @version 1.2
// @downloadURL https://raw.githubusercontent.com/Lightforger/Note-Assist/master/Note-Assist.user.js
// @grant none
// ==/UserScript==
if (NA !== undefined && NA.error !== undefined) { NA.error.write('namespace conflict between noteAssist script and another script, variable "NA" already taken, this most likely means you have installed noteAssist twice'); }
// Global NA object & sub-sections
var NA = {};
NA.debug = {};
NA.error = {};
NA.styleNote = {};
// MAJOR note: having debug open (inspect element/console/firebug/...) makes the script a lot slower
//==========================================================
// Script settings (defaults, once you save with the menu, it will always use the saved settings)
//==========================================================
NA.defaultSettings = {
uiLeft: true, // UI position //true = left side, over the tags list //false = top-right corner
alwaysResize: true, // true = Dragged notes will always resize, except if shift is held //false = only resize if shift if held
clickResizeActive: true, // A click with the selected combination of ctrl & shift will resize the note
clickResizeCtrl: true, // fitting it to text inside the note
clickResizeShift: false,
clickResizeRectangleActive: true, // A click with the selected combination of ctrl & shift will resize the note
clickResizeRectangleCtrl: false, // fitting it to a square textbox inside OR outside the note
clickResizeRectangleShift: true,
// Advanced settings
forceEnd: 20000, // number of miliseconds to let the code run before it's considered as "stuck".
// on very large images, using 'generate all' you may hit this limit (gives a warning message, then force-aborts)
debug: false //show debug text & images
};
//==========================================================
// Assist functions - mostly copy/pasted from other sources, and globals
//==========================================================
NA.globals = {
startTime: null, // startTime = Date.now();
benchStart: null, // benchStart = Date.now();
benchStop: null, // benchStop = Date.now();
eyedropperTarget: null,
sampleRatio: null,
fitToScreenRatio: 1 //Scale for when "Fit images to window" (official danbooru settings) is enabled
};
NA.benchmark = function (s) {
if (!NA.settings.debug) { return; }
if (s === 'start') {
NA.globals.benchStart = Date.now();
NA.globals.benchStop = Date.now();
NA.debug.write('----------------------
');
}
else {
var previousStop = NA.globals.benchStop;
NA.globals.benchStop = Date.now();
NA.debug.write(s + (NA.globals.benchStop - previousStop) + ' (' + (NA.globals.benchStop - NA.globals.benchStart) + ')');
}
};
NA.addGlobalStyle = function (css) {
try {
var elmHead, elmStyle;
elmHead = document.getElementsByTagName('head')[0];
elmStyle = document.createElement('style');
elmStyle.type = 'text/css';
elmHead.appendChild(elmStyle);
elmStyle.innerHTML = css;
}
catch (e) {
if (!document.styleSheets.length) {
document.createStyleSheet();
}
document.styleSheets[0].cssText += css;
}
};
// NA.$c('div', { id: 'id',class: 'class' })
NA.$c = function (type, params) {
if (type === '#text') {
return document.createTextNode(params);
}
var node = document.createElement(type);
for (var i in params) {
if (i == 'kids') {
for (var j in params[i]) {
if (typeof (params[i][j]) == 'object') {
node.appendChild(params[i][j]);
}
}
} else if (i == 'style') {
if (typeof (params[i]) == 'string') {
node.style.cssText = params[i];
} else {
for (var j in params[i]) {
node.style[j] = params[i][j];
}
}
} else if (i == 'class') {
node.className = params[i];
} else if (i == '#text') {
node.appendChild(document.createTextNode(params[i]));
} else {
node.setAttribute(i, params[i]);
}
}
return node;
};
// major errors that break the script, text tacked on bottom of the page, more user-friendly than alert()
// probably should make a setting to be able to disable it, but if these show up, the script won't work at all
NA.error.write = function (a) {
var el = document.getElementById('noteAssist_ErrorLog');
if (!el) {
document.body.appendChild(NA.$c('div', { id: 'noteAssist_ErrorLog' }));
el = document.getElementById('noteAssist_ErrorLog');
}
if (el) {
el.innerHTML += a + '\n
';
}
};
// writes debug text in the sidebar
NA.debug.write = function (a) {
if (!NA.settings.debug) { return; }
var el = document.getElementById('debug_log');
if (el) {
el.innerHTML += a + '\n
';
}
};
NA.debug.bwslider = function () { // debug function for 'convertToBlackWhite'
//var slider = document.getElementById('bwslider');
////console.log("slider changed to");
//var img = document.getElementById('image');
//var allCanvases = document.getElementsByTagName('canvas');
//if (allCanvases && allCanvases.length > 0) {
// var color = 'black';
// if (document.getElementById('noteAssist_ui').getElementsByClassName('group1')[1].checked) color = 'white';
// var lastCanvas = allCanvases[allCanvases.length - 1];
// document.getElementById('bwsliderValue').innerHTML = slider.value;
// var context = lastCanvas.getContext('2d');
// context.drawImage(img, 0, 0, lastCanvas.width, lastCanvas.height, 0, 0, lastCanvas.width, lastCanvas.height);
// var imageData = context.getImageData(0, 0, lastCanvas.width, lastCanvas.height);
// NA.convertToBlackWhite(imageData, color, slider.value);
// context.putImageData(imageData, 0, 0);
// }
};
NA.getMetaContents = function (name) {
var el = document.getElementsByName(name)[0];
if (el) { return el.content; }
else {
NA.debug.write('Could not read meta-content of: "' + name + '"');
return false;
}
};
NA.saveAllNotes = function () {
$('.unsaved').each(function (i, el) {
var parentEl = $(el).parent();
if (!parentEl.hasClass('ghostNote')) {
var dataId = parentEl.attr('data-id');
var note_body = Danbooru.Note.Body.find(dataId);
Danbooru.Note.Edit.show(note_body); // open the edit dialog
$(".note-edit-dialog .ui-dialog-buttonpane button:contains('Save')").click(); // press the save button
}
});
};
//==========================================================
// Custom objects
//==========================================================
// A shape, blob of connected pixels, could be anything
// Properties:
// .pixels = array containing each pixel's position in the imageData's Uint8ClampedArray
// .pixels.length = number of pixels in the shape
//
// .left, .top, .right, .bottom = bounding box positions of the shape on the image
// .width .height = width/height of the bounding box
// .size = size of the bounding box of the shape in pixels
NA.shape = function (pixels, imageDataWidth, imageDataHeight) {
this.pixels = pixels;
//this.size = pixels.length;
if (imageDataWidth) {
this.tempimageDataWidth = imageDataWidth;
}
if (imageDataHeight) {
this.tempimageDataHeight = imageDataHeight;
}
// init split from creation for easier merging & benchmark purposes
this.init = function () {
var pixels = this.pixels;
var width4 = this.tempimageDataWidth * 4;
var smallest_left = 999999; // start with a very large value so the loop is sure to adjust it
var largest_right = 0;
var smallest_top = 999999;
var largest_bottom = 0;
for (var i = 0; i < pixels.length; i++) {
var x = (pixels[i] % width4) / 4;
var y = Math.floor(pixels[i] / width4);
if (x < smallest_left) smallest_left = x;
else if (x > largest_right) largest_right = x;
if (y < smallest_top) smallest_top = y;
else if (y > largest_bottom) largest_bottom = y;
}
this.left = smallest_left;
this.top = smallest_top;
this.right = largest_right;
this.bottom = largest_bottom;
this.width = (this.right + 1) - this.left; // +1 needed to be correct (shapes that are 1px wide have same left & right value => width would be 0)
this.height = (this.bottom + 1) - this.top;
this.size = this.width * this.height;
};
};
// A shapeGroup, a group of shapes with similar area sizes that are close to eachother, almost all text ends up in a group, but has a fair amount of false positives
// Properties:
// .shapes = array of shape objects
// .shapes.length = number of shapes in the group
//
// .left, .top, .right, .bottom = bounding box positions of the shapegroup on the image
// .width .height = width/height of the bounding box
// .size = size of the bounding box of the shapegroup in pixels
NA.shapeGroup = function (shapes) {
this.shapes = shapes; // store shape objects, in case merging of shapegroups is needed
var totalShapeSize = 0;
for (var i = 0; i < shapes.length; i++) {
totalShapeSize += shapes[i].size;
}
this.averageShapeSize = (totalShapeSize / shapes.length);
//this.init = function () { }
var smallest_left = 999999; // start with a very large value so the loop is sure to adjust it
var largest_right = 0;
var smallest_top = 999999;
var largest_bottom = 0;
for (var i = 0; i < shapes.length; i++) {
if (shapes[i].left < smallest_left) smallest_left = shapes[i].left;
if (shapes[i].right > largest_right) largest_right = shapes[i].right;
if (shapes[i].top < smallest_top) smallest_top = shapes[i].top;
if (shapes[i].bottom > largest_bottom) largest_bottom = shapes[i].bottom;
}
this.left = smallest_left;
this.right = largest_right;
this.top = smallest_top;
this.bottom = largest_bottom;
this.width = (this.right + 1) - this.left; // +1 needed to be correct (shapes that are 1px wide have same left & right value => width would be 0)
this.height = (this.bottom + 1) - this.top;
this.size = this.width * this.height;
};
//==========================================================
// Text detection
//==========================================================
NA.detectTextColor = function (imageData) {
//=========================================
// First check if the override isn't on
//=========================================
var textColorCheckboxes = document.getElementById('noteAssist_ui').getElementsByClassName('group1');
if (textColorCheckboxes[0].checked) { return 'black'; } // Black/dark text, never invert
else if (textColorCheckboxes[1].checked) { return 'white'; } // White/light text, always invert
//=========================================
// take the average brightness (luma) of the selected area, dark images are likely to have light text
//=========================================
var average_luma = 0;
var pixelData = imageData.data;
for (var i = pixelData.length - 4; i >= 0; i -= 4) {
average_luma += pixelData[i]; // temporally use the variable to count total
average_luma += pixelData[i + 1];
average_luma += pixelData[i + 2];
}
average_luma = average_luma / (pixelData.length * 0.75); //pixelData.length includes 4th channel: alpha, which we didn't count
// is now average
NA.debug.write('luma: ' + average_luma);
// higher than 130 => light image/area => likely to be dark text // much more accurate when just a single textbubble is selected
var textColor = (average_luma < 130) ? 'white' : 'black';
// add style to checkboxes's parent span element as visual feedback what the script detected
if (textColor == 'black') {
textColorCheckboxes[0].parentNode.style = "font-weight:bold";
textColorCheckboxes[1].parentNode.style = "";
}
else if (textColor == 'white') {
textColorCheckboxes[0].parentNode.style = "";
textColorCheckboxes[1].parentNode.style = "font-weight:bold";
}
return textColor;
};
NA.convertToBlackWhite = function (imageData, textColor, cutOff) {
var luma;
var pixelData = imageData.data;
if (textColor == "black") {
if (!cutOff) cutOff = 186;
for (var i = pixelData.length - 4; i >= 0; i -= 4) {
luma = ((pixelData[i] + pixelData[i + 1] + pixelData[i + 2]) / 3);
luma = luma < cutOff ? 0 : 255;
//if (pixelData[i+3] === 0) luma=255; // turns transparent background white, allowing detection of dark letters
//pixelData[i+3] = 255; // but text on transparent = very rare
pixelData[i] = luma;
//pixelData[i + 1] = 0; // not used atm, faster if we don't have to clean it, debugging is easier with it enabled
//pixelData[i + 2] = 0; // not used atm, faster if we don't have to clean it
}
}
else { //white // copy/pasting this entire block is faster then doing "if (reversed)" several 100,000 times inside the loop
if (!cutOff) cutOff = 100;
//NA.debug.write('cutOff: ' + cutOff);
for (var i = pixelData.length - 4; i >= 0; i -= 4) {
luma = ((pixelData[i] + pixelData[i + 1] + pixelData[i + 2]) / 3);
luma = luma > cutOff ? 0 : 255;
pixelData[i] = luma;
//pixelData[i + 1] = 0; // not used atm, faster if we don't have to clean it
//pixelData[i + 2] = 0; // not used atm, faster if we don't have to clean it
}
}
};
NA.fillborder = function (data) {
var width4 = data.width * 4;
var pixels = data.data;
var pixelsToCheck = [0]; // adding more starting points is slower for some reason
var pixelsToCheckNext = [];
var tar;
pixels[0] = 100; //starting pixel is guaranteed to be black
while (pixelsToCheck.length > 0) {
for (var index = 0, l = pixelsToCheck.length; index < l; index++) {
var i = pixelsToCheck[index];
tar = (i - width4) - 4; // UP-LEFT // putting the tar values in an array to get rid of the 7 extra "if()" calls is 25% slower
if (pixels[tar] === 0) { // if the target pixel is black
pixels[tar] = 100; // mark it
pixelsToCheckNext.push(tar); // add it to the array for next loop
}
tar = (i - width4); // UP
if (pixels[tar] === 0) {
pixels[tar] = 100;
pixelsToCheckNext.push(tar);
}
tar = (i - width4) + 4; // UP-RIGHT
if (pixels[tar] === 0) {
pixels[tar] = 100;
pixelsToCheckNext.push(tar);
}
tar = i - 4; // LEFT
if (pixels[tar] === 0) {
pixels[tar] = 100;
pixelsToCheckNext.push(tar);
}
tar = i + 4; // RIGHT
if (pixels[tar] === 0) {
pixels[tar] = 100;
pixelsToCheckNext.push(tar);
}
tar = (i + width4) - 4; // DOWN-LEFT
if (pixels[tar] === 0) {
pixels[tar] = 100;
pixelsToCheckNext.push(tar);
}
tar = (i + width4); // DOWN
if (pixels[tar] === 0) {
pixels[tar] = 100;
pixelsToCheckNext.push(tar);
}
tar = (i + width4) + 4; // DOWN-RIGHT
if (pixels[tar] === 0) {
pixels[tar] = 100;
pixelsToCheckNext.push(tar);
}
}
pixelsToCheck = pixelsToCheckNext;
pixelsToCheckNext = [];
//if ((Date.now() - NA.globals.startTime) > NA.settings.forceEnd) { // this code seems to run pretty fast, don't really need anti-stuck here
// if(confirm('NoteAssist - ' +(Date.now() - NA.globals.startTime) / 1000+' seconds passed, continue? (at "fillBorder"')) {
// NA.globals.startTime = Date.now();
// }
// else {
// break;
// }
// }
}
};
NA.drawBorder = function (imageData) {
var width4 = imageData.width * 4;
var height = imageData.height;
for (var i = 0; i < width4 ; i = i + 4) { // by row, pixel location
imageData.data[i] = 0; //top row
imageData.data[i + (imageData.data.length - width4)] = 0; //bottom row
}
for (var i = 1; i < (height - 1) ; i++) { // by column, can skip top & bottom
imageData.data[(i * width4)] = 0; // left column
imageData.data[(i * width4) + (width4 - 4)] = 0; // right column
}
};
NA.getShapes = function (imageData) {
var width4 = imageData.width * 4;
var pixels = imageData.data;
var pixelsAll = [];
var pixelsToCheck = [];
var pixelsToCheckNext = [];
var allShapes = [];
var tar;
for (var outerIndex = (width4 * 2), ll = pixels.length ; outerIndex < ll; outerIndex = outerIndex + 4) { // top and bottom 2 rows will never contain any black pixels due to fillBorder, so can be skipped
if (pixels[outerIndex] === 0) { // we find a black pixel, floodfill from it
pixelsAll = [outerIndex]; // all pixel locations found in this floodfill
pixelsToCheck = [outerIndex]; // reset all arrays so we don't have any data from last floodfill
pixelsToCheckNext = []; //
while (pixelsToCheck.length > 0) {
for (var index = 0, l = pixelsToCheck.length; index < l; index++) {
var i = pixelsToCheck[index]; // i = selected pixel position in the pixel array
tar = (i - width4) - 4; // UP-LEFT // putting the tar values in an array to get rid of the 7 extra "if()" calls is 25% slower
if (pixels[tar] === 0) { // if the target pixel is black
pixels[tar] = 10; // mark it
pixelsToCheckNext.push(tar); // add it to the array for next loop
pixelsAll.push(tar); // to store in shape object, not sure if needed, perhaps could be just a counter
}
tar = (i - width4); // UP
if (pixels[tar] === 0) {
pixels[tar] = 10;
pixelsToCheckNext.push(tar);
pixelsAll.push(tar);
}
tar = (i - width4) + 4; // UP-RIGHT
if (pixels[tar] === 0) {
pixels[tar] = 10;
pixelsToCheckNext.push(tar);
pixelsAll.push(tar);
}
tar = i - 4; // LEFT
if (pixels[tar] === 0) {
pixels[tar] = 10;
pixelsToCheckNext.push(tar);
pixelsAll.push(tar);
}
tar = i + 4; // RIGHT
if (pixels[tar] === 0) {
pixels[tar] = 10;
pixelsToCheckNext.push(tar);
pixelsAll.push(tar);
}
tar = (i + width4) - 4; // DOWN-LEFT
if (pixels[tar] === 0) {
pixels[tar] = 10;
pixelsToCheckNext.push(tar);
pixelsAll.push(tar);
}
tar = (i + width4); // DOWN
if (pixels[tar] === 0) {
pixels[tar] = 10;
pixelsToCheckNext.push(tar);
pixelsAll.push(tar);
}
tar = (i + width4) + 4; // DOWN-RIGHT
if (pixels[tar] === 0) {
pixels[tar] = 10;
pixelsToCheckNext.push(tar);
pixelsAll.push(tar);
}
}
pixelsToCheck = pixelsToCheckNext;
pixelsToCheckNext = [];
}
// floodfill is done
if (pixelsAll.length > 4) { // 1-4pixel dots are barely large enough to see, pretty safe to ignore
allShapes.push(new NA.shape(pixelsAll, imageData.width, imageData.height));
}
}
}
//loop done, we have all the shapes now
NA.debug.write('shapes: ' + allShapes.length);
return allShapes;
};
NA.getAverageShapeSize = function (allShapes) {
var startIndex = 0;
var endIndex = allShapes.length;
if (allShapes.length > 7) {
startIndex = Math.floor(allShapes.length * 0.25); // don't count bottom 25% & top 15%
endIndex = allShapes.length - Math.floor(allShapes.length * 0.15);
}
var totalShapeSize = 0;
var shapesCounted = 0;
for (var i = startIndex; i < endIndex; i++) {
if (allShapes[i].size > 20) { // don't count tiny shapes such as dots & background noise into the average // can't just delete them as some are part of real letters
totalShapeSize += allShapes[i].size;
shapesCounted++;
}
}
if (shapesCounted > 0) {
return (Math.round((totalShapeSize / shapesCounted) * 100) / 100);
}
};
NA.connectShapes = function (allShapes, mode) {
// ================================================================================================
// get the average letter size, while filtering as many other shapes as possible
// ================================================================================================
allShapes.sort(function shapeSort(a, b) { // sort collections from small to large, this makes it easy to
if (a.size < b.size) { return -1; } // filter extremes as they are grouped at the start & end of array now.
if (a.size > b.size) { return 1; }
return 0;
});
var averageShapeSize = NA.getAverageShapeSize(allShapes); // get average, ignoring the bottom 25% & top 15% (extremes)
if (averageShapeSize === undefined) { return; }
NA.debug.write('averageShapeSize1: ' + averageShapeSize);
// remove anything much larger than the average size
for (var i = allShapes.length - 1; i >= 0; i--) {
if (allShapes[i].size >= averageShapeSize * 10) {
allShapes.splice(i, 1);
}
}
// ==================================================================================
// merge overlapping shapes
// works quite well as most huge shapes such a textbubbles are already removed
// ==================================================================================)
var mergedIndexes = [];
for (var indexOuter = allShapes.length - 1; indexOuter >= 0; indexOuter--) {
if (allShapes[indexOuter] === undefined) { continue; }
var base_top = allShapes[indexOuter].top;
var base_bottom = allShapes[indexOuter].bottom;
var base_left = allShapes[indexOuter].left;
var base_right = allShapes[indexOuter].right;
for (var indexInner = allShapes.length - 1; indexInner >= 0; indexInner--) {
if (allShapes[indexInner] === undefined) { continue; }
if (indexInner == indexOuter) { continue; } // don't compare to self
if (base_top > allShapes[indexInner].bottom ||
base_bottom < allShapes[indexInner].top ||
base_left > allShapes[indexInner].right ||
base_right < allShapes[indexInner].left) {
// no overlap found
continue;
}
else { // overlapping shapes found, merge them (includes joined edges = 1px overlap)
mergedIndexes.push(indexOuter);
allShapes[indexOuter].pixels = allShapes[indexOuter].pixels.concat(allShapes[indexInner].pixels);
delete allShapes[indexInner]; // delete it so it can't merge into another shape
// note, leaves a gap in the array as intended!
//console.log('merged ' + indexInner + ' into ' + indexOuter);
}
}
}
// delete the gaps in the array, re-init the updated shapes
for (var i = allShapes.length - 1; i >= 0; i--) {
if (allShapes[i] === undefined) {
allShapes.splice(i, 1);
}
if (mergedIndexes.indexOf(i) != -1) {
allShapes[i].init();
}
}
//NA.benchmark('T-mergeShapes T:');
// =========================================
// recount average after removing large shapse & merging overlaps
// =========================================
averageShapeSize = NA.getAverageShapeSize(allShapes);
NA.debug.write('averageShapeSize2: ' + averageShapeSize);
// maxConnectDistance is the square root of the average shape's size ~= an average height or width of a letter
var maxConnectDistance = Math.round(Math.sqrt(averageShapeSize));
NA.debug.write('con: ' + maxConnectDistance);
// ==================================================================================
// calculate connectedHorizontal & connectedVertical, a more accurate average distance between 2 letters (based on distance between shapes)
// - maxConnectDistance is too inaccurate since spacing between letters varies greatly
// - maxConnectDistance can be used as "if 2 shapes are more than 1.5~2x maxConnectDistance apart, they are not likely the same shapeGroup"
// so as a more accurate guideline, we take the average distance between shapes that are likely in the same group
// ==================================================================================
var connectedHorizontal = [];
var connectedVertical = [];
for (var indexOuter = allShapes.length - 1; indexOuter >= 0; indexOuter--) {
// anti-stuck
if ((Date.now() - NA.globals.startTime) > NA.settings.forceEnd) { // anti-stuck
if (confirm('NoteAssist - ' + (Date.now() - NA.globals.startTime) / 1000 + ' seconds passed, continue? (at "calculate connected"')) { // anti-stuck
NA.globals.startTime = Date.now(); // anti-stuck
} // anti-stuck
else { // anti-stuck
return; // anti-stuck
} // anti-stuck
} // anti-stuck
var base_left = allShapes[indexOuter].left;
var base_top = allShapes[indexOuter].top;
var base_right = allShapes[indexOuter].right;
var base_bottom = allShapes[indexOuter].bottom;
var smallest_connectedHorizontal = 999999;
var smallest_connectedVertical = 999999;
for (var indexInner = allShapes.length - 1; indexInner >= 0; indexInner--) {
// for each shape, find the closest shape to the left, and the shape above it (all 4 directions would just generate doubles)
if (indexInner == indexOuter) { continue; } // don't compare to self
var comp_left = allShapes[indexInner].left;
var comp_top = allShapes[indexInner].top;
var comp_right = allShapes[indexInner].right;
var comp_bottom = allShapes[indexInner].bottom;
// horizontal distance between 2 selected shapes
var current_connectedHorizontal = (base_left - comp_right);
// to connect horizontally, shapes must be on the same row, diagonal not included
if (base_top < comp_bottom && // other shape shouldn't be completely above use -> other shape's bottom must be below our top (more to top = smaller)
base_bottom > comp_top && // other shape shouldn't be completely below us -> other shape's top must be above our bottom (more to bottom = larger)
current_connectedHorizontal > 1 && // shape is to our left
current_connectedHorizontal <= maxConnectDistance && // but not too far away
current_connectedHorizontal < smallest_connectedHorizontal) // the closest shape only
{
smallest_connectedHorizontal = current_connectedHorizontal; // (new) closest distance found
}
// vertical distance between 2 selected shapes
var current_connectedVertical = (base_top - comp_bottom);
// to connect vertical, shapes must be in the same column, diagonal not included
if (base_left < comp_right && // other shape shouldn't be completely to our left
base_right > comp_left && // other shape shouldn't be completely to our right
current_connectedVertical > 1 && // other shape is above us
current_connectedVertical <= maxConnectDistance && // but not too far away
current_connectedVertical < smallest_connectedVertical) // the closest shape only
{
smallest_connectedVertical = current_connectedVertical; // (new) closest distance found
}
} //end of inner
// if a connect was found, record it
if (smallest_connectedHorizontal < 999999) {
connectedHorizontal.push(smallest_connectedHorizontal);
}
if (smallest_connectedVertical < 999999) {
connectedVertical.push(smallest_connectedVertical);
}
}
//console.log(connectedHorizontal); //debug, logs the array of all connectedHorizontal's
//console.log(connectedVertical);
// calcuate the average connect distances
var connectedHorizontalAverage = 0; //init to 0, if the connectedHorizontal array is empty, it will stay 0
if (connectedHorizontal.length > 0) {
for (var i = connectedHorizontal.length - 1; i >= 0; i--) {
connectedHorizontalAverage += connectedHorizontal[i]; // temporally use the variable to calc the total
}
connectedHorizontalAverage = connectedHorizontalAverage / connectedHorizontal.length; // average
}
var connectedVerticalAverage = 0; //init to 0, if the connectedVertical array is empty, it will stay 0
if (connectedVertical.length > 0) {
for (var i = connectedVertical.length - 1; i >= 0; i--) {
connectedVerticalAverage += connectedVertical[i]; // temporally use the variable to calc the total
}
connectedVerticalAverage = connectedVerticalAverage / connectedVertical.length; // average
}
var connectHorizontalMax = connectedHorizontalAverage * 2; // the multiplier could be tweaked as needed
var connectVerticalMax = connectedVerticalAverage * 2; // any shapes further away than this won't be connected
connectHorizontalMax = (Math.round(connectHorizontalMax * 100) / 100); // round to 2 digits
connectVerticalMax = (Math.round(connectVerticalMax * 100) / 100);
NA.debug.write('con-H-Max: ' + connectHorizontalMax);
NA.debug.write('con-V-Max: ' + connectVerticalMax);
//NA.benchmark('T-findConnects T:');
// ==================================================================================
// Shapes to Groups: take a shape, put it in a new group, find nearby other shapes and put them in the group
//
// we start with that 1 shape in the group, we look all around it if there's a nearby shape
// if one is found, add it to the group
// example: LastChecked starts at 0, shapegroup length starts at 1
// check every shape at/beyond the LastChecked
// loop runs once, LastChecked is now at 1, 2 elements get added, length goes up to 3
// loop runs 2 times, LastChecked goes to 3, no more elements are found, loop breaks
//
// it might be possible to improve the performance by storing extra data on which shape is close to another shape in "calculate connectedHorizontal & connectedVertical"
// instead of comparing every shape to every other shape again (takes N*N time, which does become slow with 1000+ shapes)
// ==================================================================================
var allShapeGroups = [];
while (allShapes.length > 0) {
var currentShapeGroup = allShapeGroups.length; // index of the current shapeGroup in allShapeGroups array (at the end -> create new group)
var currentShapeGroupLastChecked = 0; // extra performance: only need to check shapes once
allShapeGroups[currentShapeGroup] = [allShapes.splice((allShapes.length - 1), 1)[0]]; // create a new shapegroup and put a shape in it (removed from allshapes)
// note that splice returns an *array* of the removed items
// allShapeGroups[currentShapeGroup].length = the number of shapes in our group
// currentShapeGroupLastChecked = (index of) the last shape we checked
// find more shapes that we can connect to, add them to the group
// will automatically stop once no more letters are found
while (currentShapeGroupLastChecked < allShapeGroups[currentShapeGroup].length) {
// anti-stuck
if ((Date.now() - NA.globals.startTime) > NA.settings.forceEnd) { // anti-stuck
if (confirm('NoteAssist - ' + (Date.now() - NA.globals.startTime) / 1000 + ' seconds passed, continue? (at "Shapes to Groups"')) { // anti-stuck
NA.globals.startTime = Date.now(); // anti-stuck
} // anti-stuck
else { // anti-stuck
return; // anti-stuck
} // anti-stuck
} // anti-stuck
for (var allShapeIndex = allShapes.length - 1; allShapeIndex >= 0; allShapeIndex--) {
//============================================================================================================================================================
// connect logic: check connectHorizontalMax/connectVerticalMax distance in each direction
// pro: works for comma's & appostrofe's
// con: connects even if only a few pixels match
//============================================================================================================================================================
//console.log('comparing the ' + currentShapeGroupLastChecked + 'th element of group ' + currentShapeGroup + ' with shape index ' + allShapeIndex);
var base_top = allShapeGroups[currentShapeGroup][currentShapeGroupLastChecked].top;
var base_bottom = allShapeGroups[currentShapeGroup][currentShapeGroupLastChecked].bottom;
var base_left = allShapeGroups[currentShapeGroup][currentShapeGroupLastChecked].left;
var base_right = allShapeGroups[currentShapeGroup][currentShapeGroupLastChecked].right;
var comp_top = allShapes[allShapeIndex].top;
var comp_bottom = allShapes[allShapeIndex].bottom;
var comp_left = allShapes[allShapeIndex].left;
var comp_right = allShapes[allShapeIndex].right;
// quite similar to "calculate connectedHorizontal & connectedVertical", but this does check in all directions
// horizontal align
if (base_top < comp_bottom && // other shape shouldn't be completely above use -> other shape's bottom must be below our top (more to top = smaller)
base_bottom > comp_top) { // other shape shouldn't be completely below us -> other shape's top must be above our bottom (more to bottom = larger)
//vertical align, check left
var distanceLeft = (base_left - comp_right);
if (distanceLeft > 0 && // shape is to our left
distanceLeft <= connectHorizontalMax) { // but not too far away
allShapeGroups[currentShapeGroup].push(allShapes.splice(allShapeIndex, 1)[0]); // take the shape out of the allShapes array and put it in this group
}
else {
//vertical align, check right
var distanceRight = (comp_left - base_right); // same as (base_right - comp_left ) * -1;
if (distanceRight > 0 && // shape is to our left
distanceRight <= connectHorizontalMax) { // but not too far away
allShapeGroups[currentShapeGroup].push(allShapes.splice(allShapeIndex, 1)[0]); // take the shape out of the allShapes array and put it in this group
}
}
}
// vertical align
else if (base_left < comp_right && // other shape shouldn't be completely to our left
base_right > comp_left) { // other shape shouldn't be completely to our right
var distanceUp = (base_top - comp_bottom);
if (distanceUp > 0 && // shape is above us
distanceUp <= connectVerticalMax) { // but not too far away
allShapeGroups[currentShapeGroup].push(allShapes.splice(allShapeIndex, 1)[0]); // take the shape out of the allShapes array and put it in this group
}
else {
var distanceDown = (comp_top - base_bottom); // same as (base_bottom - comp_top) * -1
if (distanceDown > 0 && // shape is above us
distanceDown <= connectVerticalMax) { // but not too far away
allShapeGroups[currentShapeGroup].push(allShapes.splice(allShapeIndex, 1)[0]); // take the shape out of the allShapes array and put it in this group
}
}
} // end vertical align
}
// at this point we checked allShapeGroups[currentShapeGroup][currentShapeGroupLastChecked]
// so increase the LastChecked by 1, and move on to the next shape in the group (if any)
currentShapeGroupLastChecked++;
}
// at this point we completed a group, if there any shapes left not in a groupn -> the loop will continue, creating more groups
}
//NA.benchmark('T-shapesToGroups T:');
NA.debug.write('groups: ' + allShapeGroups.length);
// at this point, all shapes are part of a group, the looping is done
// ===================================================================
// group cleanup, delete small groups & groups with just background stuff
// ===================================================================
for (var i = allShapeGroups.length - 1; i >= 0; i--) {
if (mode == 'full' && allShapeGroups[i].length <= 2) {
allShapeGroups.splice(i, 1); // if a group contain only 1 or 2 shapes, then it is not text, only active in full mode
}
else { // turn it into a shapeGroup object
allShapeGroups[i] = new NA.shapeGroup(allShapeGroups[i]);
}
}
// if a group's average shape size is much smaller than the global average shape size, it's most likely noise
if (mode == 'full') {
for (var i = allShapeGroups.length - 1; i >= 0; i--) {
//NA.debug.write('Group ' + i + ' average: ' + allShapeGroups[i].averageShapeSize); // pretty cool in combo with the draw number
if (allShapeGroups[i].averageShapeSize < (averageShapeSize / 8)) {
allShapeGroups.splice(i, 1);
}
}
}
// groups that are very flat or thin generally aren't text either (<10px in either dimension)
for (var i = allShapeGroups.length - 1; i >= 0; i--) {
if (mode == 'full') { // full mode will delete if either the width or height is below 7 & total size is <350
if ((allShapeGroups[i].height < 7 || allShapeGroups[i].width < 7) && allShapeGroups[i].size < 350) {
allShapeGroups.splice(i, 1);
}
}
else { // other modes require both to be below 8 before it's deleted
if (allShapeGroups[i].height < 8 && allShapeGroups[i].width < 8) {
allShapeGroups.splice(i, 1);
}
}
}
//NA.benchmark('T-groupCleanup T:');
// ================================================================================
// connect shape groups that are close to eachother (multiple lines in a single textbubble)
// ================================================================================
var toRemoveIndexes = [];
for (var indexOuter = allShapeGroups.length - 1; indexOuter >= 0; indexOuter--) {
if (toRemoveIndexes.indexOf(indexOuter) != -1) continue;
var base_top = allShapeGroups[indexOuter].top;
var base_bottom = allShapeGroups[indexOuter].bottom;
var base_left = allShapeGroups[indexOuter].left;
var base_right = allShapeGroups[indexOuter].right;
for (var indexInner = allShapeGroups.length - 1; indexInner >= 0; indexInner--) {
if (toRemoveIndexes.indexOf(indexInner) != -1 || indexInner == indexOuter) continue;
var comp_left = allShapeGroups[indexInner].left;
var comp_top = allShapeGroups[indexInner].top;
var comp_right = allShapeGroups[indexInner].right;
var comp_bottom = allShapeGroups[indexInner].bottom;
//console.log('indexOuter: '+indexOuter + ' connecting to '+indexInner+'\n'+
// base_top+' '+base_bottom+' '+base_left+' '+base_right+'\n'+
// allShapeGroups[indexInner].top + ' ' + allShapeGroups[indexInner].bottom + ' ' +
// allShapeGroups[indexInner].left + ' ' +allShapeGroups[indexInner].right + ' ' +
// (base_top > allShapeGroups[indexInner].bottom ||
// base_bottom < allShapeGroups[indexInner].top ||
// base_left > allShapeGroups[indexInner].right ||
// base_right < allShapeGroups[indexInner].left));
if ((base_top - (maxConnectDistance * 2.5)) > comp_bottom ||
(base_bottom + (maxConnectDistance * 2.5)) < comp_top ||
(base_left - (maxConnectDistance * 2.5)) > comp_right ||
(base_right + (maxConnectDistance * 2.5)) < comp_left) {
// first check, if they DON'T overlap with the added maxConnectDistance -> not in range
continue;
}
else { // shape group is within (maxConnectDistance * 2.5) range
if (!(base_top > comp_bottom ||
base_bottom < comp_top ||
base_left > comp_right ||
base_right < comp_left)) {
// check if they DO overlap even without the connect distance, then their center don't have to be aligned
allShapeGroups[indexOuter] = new NA.shapeGroup(allShapeGroups[indexOuter].shapes.concat(allShapeGroups[indexInner]));
toRemoveIndexes.push(indexInner);
}
else { // they don't overlap, they are within within (maxConnectDistance * 2.5) range
// the shapes must at least "overlap" (single dimension) this much to be combined, this exludes "overlap" of only a few pixels and diagonals
var minHorizontal = allShapeGroups[indexOuter].width * 0.25; // at least 25%
var minVertical = allShapeGroups[indexOuter].height * 0.25;
if (((base_left + minHorizontal) < comp_right && (base_right - minHorizontal) > comp_left) || // other shapeGroup should "overlap" at least minHorizontal distance (single dimension)
((base_top + minVertical) < comp_bottom && (base_bottom - minVertical) > comp_top)) // other shapeGroup should "overlap" at least minVertical distance (single dimension)
{
allShapeGroups[indexOuter] = new NA.shapeGroup(allShapeGroups[indexOuter].shapes.concat(allShapeGroups[indexInner]));
toRemoveIndexes.push(indexInner);
}
}
}
}
}
for (var i = allShapeGroups.length - 1; i >= 0; i--) {
if (toRemoveIndexes.indexOf(i) != -1) {
allShapeGroups.splice(i, 1);
}
}
//NA.benchmark('T-connectGroups T:');
return allShapeGroups;
};
NA.noteLeftclick = function (e) {
var noteBox;
if (NA.settings.clickResizeActive && e && e.target) {
if (((NA.settings.clickResizeCtrl && e.ctrlKey) || (!NA.settings.clickResizeCtrl && !e.ctrlKey)) &&
((NA.settings.clickResizeShift && e.shiftKey) || (!NA.settings.clickResizeShift && !e.shiftKey)))
{
if ($(e.target).hasClass('note-box')) {
noteBox = e.target;
}
else if ($(e.target).parent().hasClass('note-box')) {
noteBox = e.target.parentNode;
}
NA.snap('note', noteBox);
}
}
if (NA.settings.clickResizeRectangleActive && e && e.target) {
if (((NA.settings.clickResizeRectangleCtrl && e.ctrlKey) || (!NA.settings.clickResizeRectangleCtrl && !e.ctrlKey)) &&
((NA.settings.clickResizeRectangleShift && e.shiftKey) || (!NA.settings.clickResizeRectangleShift && !e.shiftKey)))
{
if ($(e.target).hasClass('note-box')) {
noteBox = e.target;
}
else if ($(e.target).parent().hasClass('note-box')) {
noteBox = e.target.parentNode;
}
NA.snap('squareTextbox', noteBox);
}
}
};
NA.ghostRightclick = function (e) {
e.preventDefault();
var noteDataId = this.getAttribute('data-id');
if (noteDataId.indexOf('x') != -1) { // unsaved notes have data id's like -> 'xxxxxxxxxxx', once saved -> '1234568', only delete unsaved notes
var noteContainer = document.getElementById('note-container');
var toBeRemoved = noteContainer.querySelectorAll('[data-id="' + noteDataId + '"]');
if (toBeRemoved[0]) { noteContainer.removeChild(toBeRemoved[0]); }
if (toBeRemoved[1]) { noteContainer.removeChild(toBeRemoved[1]); }
}
};
NA.filterOverlappingNotes = function (allShapeGroups) {
var existingNotes = document.getElementsByClassName('note-box');
for (var shapeGroupIndex = allShapeGroups.length - 1; shapeGroupIndex >= 0; shapeGroupIndex--) {
var top = allShapeGroups[shapeGroupIndex].top;
var bottom = allShapeGroups[shapeGroupIndex].bottom;
var left = allShapeGroups[shapeGroupIndex].left;
var right = allShapeGroups[shapeGroupIndex].right;
//var size = (right - left) * (bottom - top);
var size = allShapeGroups[shapeGroupIndex].size;
for (var noteIndex = 0; noteIndex < existingNotes.length; noteIndex++) {
var comp_top = parseInt(existingNotes[noteIndex].style.top, 10);
var comp_bottom = comp_top + parseInt(existingNotes[noteIndex].style.height, 10);
var comp_left = parseInt(existingNotes[noteIndex].style.left, 10);
var comp_right = comp_left + parseInt(existingNotes[noteIndex].style.width, 10);
if (top > comp_bottom ||
bottom < comp_top ||
left > comp_right ||
right < comp_left) {
continue; // no overlap, continue on to next note to compare with
}
else {
// overlap found, calculate overlap area
var overlap_left = Math.max(0, left, comp_left);
var overlap_right = Math.min(right, comp_right);
var overlap_top = Math.max(0, top, comp_top);
var overlap_bottom = Math.min(bottom, comp_bottom);
var overlap_area = (overlap_right - overlap_left) * (overlap_bottom - overlap_top);
//NA.debug.write('overlap_ratio: ' + (overlap_area / size));
if (overlap_area > size * 0.75) {
// the to-be-made note is at least 75% covered by an existing note
allShapeGroups.splice(shapeGroupIndex, 1); //remove the group
break; // stop the (inner)loop through notes, the (outer) loop through groups will continue with next
}
}
}
}
};
NA.ghostNote = function (mode) {
if (mode == 'last') {
var notes_all = document.getElementById('note-container').getElementsByClassName('note-box');
var noteBox = notes_all[notes_all.length - 1];
noteBox.className += ' ghostNote';
noteBox.addEventListener('contextmenu', NA.ghostRightclick, false); //rightclick
// noteLeftclick is bound in Note.create
}
};
NA.snap = function (mode, theNote, x, y, width, height) {
if (mode == 'squareTextbox') { return; } //debug, unfinished
// ================================================================================
// mode "note" = resize passed theNote to largest text found
// mode "last" = does the same, but finds last note first
// expands 5px before doing anything, to avoid cutting off letters that are just on the edge
// ================================================================================
// mode "full" = entire image, creates new notes instead of resizing existing
// ignores text that already has a note over it
// ================================================================================
// mode "squareTextbox" = expands 100px and snaps to the background of a square textbubble if it's found
// unfinished, doesn't work yet
// ================================================================================
// mode "area" / "virtual" = pass x/y/width/height parameters // unused for now, but should be working
// scan area inside the parameters, create 1 note on largest text found
// ================================================================================
NA.globals.startTime = Date.now(); //start point for 'NA.settings.forceEnd'
NA.benchmark('start');
// ================================================================================
// set variables, get some info from settings
// ================================================================================
var img = document.getElementById('image');
//var imgHeight = Math.floor((img.naturalHeight - 2) * NA.globals.fitToScreenRatio); // don't remember why -2
//var imgWidth = Math.floor((img.naturalWidth - 2) * NA.globals.fitToScreenRatio);
var imgHeight = img.naturalHeight;
var imgWidth = img.naturalWidth;
var canvas = document.createElement('canvas');
var context = canvas.getContext('2d');
var generateAllButton = document.getElementById('noteAssist_generateAll');
var imgStyleWidth = (parseInt(img.style.width, 10) || parseInt(img.width, 10));
// set NA.globals.fitToScreenRatio
if (NA.getMetaContents('always-resize-images') === 'true' && parseInt(img.style.height, 10) && img.naturalHeight) {
NA.globals.fitToScreenRatio = (parseInt(img.style.height, 10) / img.naturalHeight); // height used in the style (resized by JS) / real height
}
// how much we are zoomed in compared to the sample, used as multiplier with note padding space
NA.globals.sampleRatio = (imgStyleWidth / parseInt(img.getAttribute('data-large-width'), 10));
var notePadding = Math.floor(4 * NA.globals.sampleRatio); //px, extra space added around the text
var expand = Math.floor(5 * NA.globals.sampleRatio); //px, will look outside the selected area by this distance (greatly reduces accidental letter clipping)
var theNoteInner;
if (mode == 'last' || mode == 'note') {
if (mode == 'last') {
var allNotes = document.getElementById('note-container').getElementsByClassName('note-box');
theNote = allNotes[allNotes.length - 1];
}
theNoteInner = theNote.getElementsByClassName('note-box-inner-border')[0];
x = Math.ceil(parseInt(theNote.style.left, 10) / NA.globals.fitToScreenRatio);
y = Math.ceil(parseInt(theNote.style.top, 10) / NA.globals.fitToScreenRatio);
width = Math.ceil(parseInt(theNote.style.width, 10) / NA.globals.fitToScreenRatio);
height = Math.ceil(parseInt(theNote.style.height, 10) / NA.globals.fitToScreenRatio);
// ================================================================================
// expand selected area a bit, greatly reduces accidentally clipping few letters
// ================================================================================
x = x - expand;
if (x < 0) { x = 0; }
y = y - expand;
if (y < 0) { y = 0; }
width = width + (expand * 2);
if (width > imgWidth) { width = imgWidth; }
height = height + (expand * 2);
if (height > imgHeight) { height = imgHeight; }
} else if (mode == 'full') {
x = 0;
y = 0;
width = imgWidth;
height = imgHeight;
generateAllButton.value = "Working...";
generateAllButton.disabled = true;
} else if (mode == 'virtual' || mode == 'area') {
// x/y/width/height is set in the parameters
}
canvas.height = height; //set the canvas heights so we can draw on it
canvas.width = width;
NA.benchmark('T-miscStart T:');
// ================================================================================
// Convert the selected area on the image to a canvas so we can access the pixel data
// ================================================================================
//.drawImage(image, sourceX, sourceY, sourceWidth, sourceHeight, destX, destY, destWidth, destHeight);
context.drawImage(img, x, y, width, height, 0, 0, width, height);
var imageData = context.getImageData(0, 0, width, height);
NA.benchmark('T-areaToCanvas T:');
// ================================================================================
// A cheap & simple detection of the text color (which can be overriden by the menu)
// ================================================================================
var textColor = NA.detectTextColor(imageData); // guess the text color based on image brightness
NA.debug.write('textColor: ' + textColor);
NA.benchmark('T-detectTextColor T:');
// ================================================================================
// converts the selected area to black&white
// The image iself uses the red channel, values "0" and "255"
// The green & blue channels can be used for other data
// ================================================================================
NA.convertToBlackWhite(imageData, textColor);
NA.benchmark('T-to B&W T:');
// ================================================================================
// make the outer edge of the border black
// later on this helps eliminate all lines/letters that were cut off
// ================================================================================
NA.drawBorder(imageData);
NA.benchmark('T-drawBorder T:');
// ================================================================================
// First, floodfill starting from the top left corner.
// Removes all shapes connected to the border (background, cut-off, etc)
// Any pixel that is "removed" has a value 100 (again, red channel)
// ================================================================================
NA.fillborder(imageData);
NA.benchmark('T-fillBorder T:');
// ================================================================================
// Scan all pixels that still have a value of 0, store them into "shape" objects
// ================================================================================
var allShapes = NA.getShapes(imageData);
if (allShapes.length === 0) {
//*/// debug
if (NA.settings.debug) {
NA.debug.write('0 letters found(getShapes)');
if (!document.getElementById('debug-canvas-spacer')) {
document.getElementById('image-container').appendChild(NA.$c('div', { id: 'debug-canvas-spacer', style: 'height:1em;width:100%' })); //so the canvases go below the image
}
context.putImageData(imageData, 0, 0);
document.getElementById('image-container').appendChild(canvas);
}
//*/// debug
if (mode == 'full') {
generateAllButton.value = "Generate all notes";
generateAllButton.disabled = false;
}
return;
}
NA.benchmark('T-getShapes T:');
for (var i = 0; i < allShapes.length; i++) {
allShapes[i].init(); // doing init on creation or here doesn't change speed, just split for benchmarking purposes
}
//return;
NA.benchmark('T-initShapes T:');
//================================== debug - draw single letters (before connectShapes)
//*///
if (NA.settings.debug) {
context.putImageData(imageData, 0, 0);
context.strokeStyle = 'rgba(0,255,255,0.8)';
context.lineWidth = 1;
for (var i = 0; i < allShapes.length; i++) {
context.strokeRect(allShapes[i].left, allShapes[i].top, (allShapes[i].right - allShapes[i].left), (allShapes[i].bottom - allShapes[i].top));
}
imageData = context.getImageData(0, 0, width, height);
NA.benchmark('T-DrawLetters T:');
}
//*///
// ================================================================================
// Connect shapes with other nearby shapes into NA.shapeGroup objects
// "Nearby" is defined as "< the square root of the average shape's size" (in pixels, may change for a better algorithm)
// includes filtering of groups that are unlikely to be text
// ================================================================================
NA.debug.write('ConnectShapes-Start');
var allShapeGroups = NA.connectShapes(allShapes, mode); // clears the allShapes variable, if it's still needed after this then pass allShapes.slice()
NA.benchmark('T-connectShapes T:');
//================================== debug - draw single letters (after connectShapes)
/*///
if (NA.settings.debug) {
context.putImageData(imageData, 0, 0);
context.strokeStyle = 'rgba(0,255,255,0.8)';
context.lineWidth = 1;
for (var i = 0; i < allShapes.length; i++) {
context.strokeRect(allShapes[i].left, allShapes[i].top, (allShapes[i].right - allShapes[i].left), (allShapes[i].bottom - allShapes[i].top));
}
imageData = context.getImageData(0, 0, width, height);
NA.benchmark('T-DrawLetters T:');
}
//*///
if (!allShapeGroups || allShapeGroups.length === 0) {
//*/// debug
if (NA.settings.debug) {
NA.debug.write('no shapes left after(connectShapes)');
if (!document.getElementById('debug-canvas-spacer')) {
document.getElementById('image-container').appendChild(NA.$c('div', { id: 'debug-canvas-spacer', style: 'height:1em;width:100%' })); //so the canvases go below the image
}
context.putImageData(imageData, 0, 0);
document.getElementById('image-container').appendChild(canvas);
}
//*/// debug
if (mode == 'full') {
generateAllButton.value = "Generate all notes";
generateAllButton.disabled = false;
}
return;
}
// ================================================================================
// if mode is full, fire another "connectshapes" on each shapegroup
// this improves accuracy when a page uses multiple fonts or text sizes
// ================================================================================
// not sure if needed, as it does slow down the script quite a bit, need more data first
//if (mode == 'full') {
// }
//================================== debug - draw shapegroups
//*
if (NA.settings.debug) {
context.putImageData(imageData, 0, 0);
context.strokeStyle = 'rgba(255,255,255,0.8)';
context.lineWidth = 1;
context.fillStyle = 'white';
context.font = 'bold 16px Arial';
for (var i = 0; i < allShapeGroups.length; i++) {
context.strokeRect(allShapeGroups[i].left, allShapeGroups[i].top, (allShapeGroups[i].right - allShapeGroups[i].left), (allShapeGroups[i].bottom - allShapeGroups[i].top));
context.fillText(i, allShapeGroups[i].left, allShapeGroups[i].top+8); // useful in combo with the group "average:" debug text
}
imageData = context.getImageData(0, 0, width, height);
NA.benchmark('T-DrawShapeGroups T:');
}
//*/
// ================================================================================
// check if a shapeGroup is already covered by an existing note, if so, delete the group
// could use a new name for the function
// ================================================================================
if (mode == 'full') {
//document.getElementById('note-container').innerHTML = ''; // debug, remove all existing notes so it's easier to see ghost notes, debug
NA.filterOverlappingNotes(allShapeGroups);
NA.benchmark('T-filterOverlappingNotes T:');
}
// ================================================================================
// mode "full" = create ghost notes for all shapeGroups
// mode "note"/"last" = resizes the note to the largest found shapeGroup
// ================================================================================
if (mode == 'full') {
// Create a note for every shapegroup
NA.debug.write('creating ' + allShapeGroups.length + ' notes');
var noteContainer = document.getElementById('note-container');
noteContainer.style.display = 'none'; // temporally hide the note container to reduce the amount of screen redraws from new notes
for (var i = 0; i < allShapeGroups.length; i++) {
//NA.debug.write('creating note: \n' + ((allShapeGroups[i].left - notePadding) * NA.globals.fitToScreenRatio) + ' ' +
// ((allShapeGroups[i].top - notePadding) * NA.globals.fitToScreenRatio) + ' ' +
// (((allShapeGroups[i].right - allShapeGroups[i].left) + notePadding * 2) * NA.globals.fitToScreenRatio)+ ' ' +
// (((allShapeGroups[i].bottom - allShapeGroups[i].top) + notePadding * 2) * NA.globals.fitToScreenRatio));
Danbooru.Note.create((allShapeGroups[i].left - notePadding) * NA.globals.fitToScreenRatio,
(allShapeGroups[i].top - notePadding) * NA.globals.fitToScreenRatio,
((allShapeGroups[i].right - allShapeGroups[i].left) + notePadding * 2) * NA.globals.fitToScreenRatio,
((allShapeGroups[i].bottom - allShapeGroups[i].top) + notePadding * 2) * NA.globals.fitToScreenRatio);
NA.ghostNote('last');
}
noteContainer.style.display = 'block';
generateAllButton.value = "Generate all notes";
generateAllButton.disabled = false;
NA.benchmark('T-attachNotes T:');
}
else if (mode == 'note' || mode == 'last') {
// find the largest shapegroup, and fit the note to it
var largestShapeGroupIndex = 0;
for (var i = 0; i < allShapeGroups.length; i++) {
if (allShapeGroups[i].size > allShapeGroups[largestShapeGroupIndex].size) {
largestShapeGroupIndex = i;
}
}
var largestShapeGroup = allShapeGroups[largestShapeGroupIndex];
//NA.debug.write('largestShapeGroup.size I: ' + i);
//NA.debug.write('largestShapeGroup.size: ' + largestShapeGroup.size);
//NA.debug.write('width*height: ' + width * height);
//NA.debug.write('ratio: ' + (width * height) / largestShapeGroup.size);
//NA.debug.write('NA.globals.fitToScreenRatio: ' + NA.globals.fitToScreenRatio);
//NA.debug.write('resizing note: \n' + ((x + largestShapeGroup.left - notePadding) * NA.globals.fitToScreenRatio) +
// ' ' + ((y + largestShapeGroup.top - notePadding) * NA.globals.fitToScreenRatio) +
// ' ' + (((largestShapeGroup.right - largestShapeGroup.left) + notePadding * 2) * NA.globals.fitToScreenRatio) +
// ' ' + (((largestShapeGroup.bottom - largestShapeGroup.top) + notePadding * 2) * NA.globals.fitToScreenRatio));
if (((width * height) / largestShapeGroup.size) < 36) { // if the resize ration is no bigger than 32 times, prevents notes snapping to tiny sizes
var newLeft = ((x + largestShapeGroup.left - notePadding) * NA.globals.fitToScreenRatio); //x & y are the offsets of the selected area on the full image
var newTop = ((y + largestShapeGroup.top - notePadding) * NA.globals.fitToScreenRatio);
var newWidth = Math.max(10, ((largestShapeGroup.right - largestShapeGroup.left) + notePadding * 2) * NA.globals.fitToScreenRatio);
var newHeight = Math.max(10, ((largestShapeGroup.bottom - largestShapeGroup.top) + notePadding * 2) * NA.globals.fitToScreenRatio);
if (theNote.style.left != (newLeft + 'px') ||
theNote.style.top != (newTop + 'px') ||
theNote.style.width != (newWidth + 'px') ||
theNote.style.height != (newHeight + 'px'))
{
theNote.style.left = newLeft + 'px';
theNote.style.top = newTop + 'px';
theNote.style.width = newWidth + 'px'; // no smaller than 10px
theNote.style.height = newHeight + 'px';
if (theNoteInner.className.indexOf('unsaved') == -1) { // if not already marked as unsaved
theNoteInner.className += ' unsaved'; // mark as unsaved if changed, goes onto inner note
}
theNoteInner.style.width = (newWidth -2) + 'px'; // inner note is 2px smaller
theNoteInner.style.height = (newHeight - 2) + 'px';
}
}
NA.benchmark('T-resizeNote T:');
}
// ================================================================================
//
// ================================================================================
if (NA.settings.debug) {
if (!document.getElementById('debug-canvas-spacer')) {
document.getElementById('image-container').appendChild(NA.$c('div', { id: 'debug-canvas-spacer', style: 'height:1em;width:100%' })); //so the canvases go below the image
}
context.putImageData(imageData, 0, 0);
document.getElementById('image-container').appendChild(canvas);
}
NA.benchmark('T-stop, Total T:');
};
//==========================================================
// Hook into danbooru code
//==========================================================
NA.danbooruHooks = function () { //adds our own code to the Danbooru.Note functions
//=========================
// Hook into Note.create & TranslationMode.start & TranslationMode.create_note
// in case of updates, new source can be found at: raw.githubusercontent.com/r888888888/danbooru/master/app/assets/javascripts/notes.js
//=========================
// show the noteAssist window when starting translation mode
var TranslationMode_start = Danbooru.Note.TranslationMode.start;
Danbooru.Note.TranslationMode.start = function () {
$('#noteAssist_ui').show();
return TranslationMode_start.apply(this, arguments);
};
// snap & ghost when creating a new note
var TranslationMode_create_note = Danbooru.Note.TranslationMode.create_note;
Danbooru.Note.TranslationMode.create_note = function () {
TranslationMode_create_note.apply(this, arguments);
// args: function (e, x, y, w, h)
var event = arguments[0];
var w = arguments[3];
var h = arguments[4];
if (w > 9 || h > 9) { //check if a note was actually created, minimum note size: 10px
if (event && ((NA.settings.alwaysResize && !event.shiftKey) || (!NA.settings.alwaysResize && event.shiftKey))) {
NA.snap('last');
NA.ghostNote('last');
}
}
};
// add listener for ctrl-click resize
var create = Danbooru.Note.create;
Danbooru.Note.create = function () {
create.apply(this, arguments);
var notes_all = document.getElementById('note-container').getElementsByClassName('note-box');
var noteBox = notes_all[notes_all.length - 1];
noteBox.addEventListener('click', NA.noteLeftclick, false);
};
};
//==========================================================
// Single-note specific functions
//==========================================================
NA.styleNote.getActiveTextarea = function () {
var textarea = document.activeElement; //select active textarea (note edit window)
if (textarea.nodeName !== 'TEXTAREA') { //if active element is not a textarea, select the last opened edit window
textarea = null;
var editWindows = document.getElementsByClassName('note-edit-dialog');
var l = editWindows.length;
if (l > 0) {
for (var i = l - 1; i >= 0; i--) {
var w = editWindows[i];
if (w.style.display === 'block') {
textarea = w.getElementsByTagName('textarea')[0];
}
}
}
}
return textarea;
};
NA.styleNote.addCss = function (e, data) {
if (e && typeof e === "object") { // in case of notedropper, e = 'notedropper', kinda bad coding
e.preventDefault(); //don't un-focus the textarea
}
var textarea = NA.styleNote.getActiveTextarea();
if (textarea === null) { return; } //no active or visible note edit window found
var value;
if (e.target && e.target.id) { //get the targeted element
var targetID = e.target.id;
if (targetID === 'noteAssist_textBold') {
value = 'bold';
}
else if (targetID === 'noteAssist_textItalic') {
value = 'italic';
}
else if (targetID === 'noteAssist_textSizePlus') {
value = 'sizePlus';
}
else if (targetID === 'noteAssist_textSizeMinus') {
value = 'sizeMinus';
}
else if (targetID === 'noteAssist_textTn') {
value = 'tn';
}
else {
NA.error.write('NoteAssist - error in "addCss", invalid targetID: ' + targetID);
}
}
else if (typeof e === 'string') { //not an event
if (e === 'eyedropper') {
value = 'eyedropper';
}
}
else {
NA.error.write('NoteAssist - error in "addCss" - e: ' + e);
}
//get selected text
var start = textarea.selectionStart;
var end = textarea.selectionEnd;
if (end - start === 0 && value !== 'tn') {
start = 0;
end = textarea.textLength;
}
var fullText = textarea.value;
var selectedText = textarea.value.substring(start, end);
var startTag;
var endTag;
var startTagLength;
var endTagLength;
if (value === 'bold' || value === 'italic' || value === 'tn') {
if (value === 'bold') {
startTag = '';
endTag = '';
}
else if (value === 'italic') {
startTag = '';
endTag = '';
}
else { //tn
startTag = '
bold
Lorem Ipsum
#FFFFFF
Note Assist Settings
' + 'Basic Settings
' + '' + '' + '' + '' + '' + '' + 'Advanced Settings
' + '' + '' + '' + '' + '*Requires F5
' + '' + ''; var style = '#noteAssist_settingMenu { background-color:#C6C6C6; height:350px; width:350px; position:absolute; left:70px; top:70px; z-index:9999; border:10px ridge #3B3EEE; padding:9px; }' + '#noteAssist_settingMenu p { margin-bottom:0.5em; }' + '#noteAssist_settingMenu div.section { border:1px solid black; padding:4px; margin-bottom:1em; }' + '#noteAssist_settingMenu input[type="checkbox"] { width:16px; height:22px; }' + '#noteAssist_settingMenu input[type="text"] { width:5em; margin-bottom:5px; }' + '#noteAssist_settingMenu #noteAssist_settingsSave { position:absolute; bottom:5px; right:5px; }'; NA.addGlobalStyle(style); document.body.appendChild(overlay); document.body.appendChild(settingMenu); overlay.addEventListener('click', NA.settingsMenuClose, false); document.getElementById('noteAssist_settingsSave').addEventListener('click', NA.settingsMenuSave, false); }; NA.settingsMenuSave = function () { var new_settings = {}; var inputs = document.getElementById('noteAssist_settingMenu').getElementsByTagName('input'); for (var i = 0; i < inputs.length; i++) { var key = inputs[i].id.replace(/^noteAssist_/, ''); if (inputs[i].type === 'checkbox') { // save all checkboxes as boolean new_settings[key] = inputs[i].checked; } else if (inputs[i].type === 'text') { // save all text fields by their value var inputValue = inputs[i].value; if (inputValue.length > 0) { // basic validation, textfield must contain text if (key === 'forceEnd' && isNaN(parseInt(inputValue, 10))) { // basic validation, "forceEnd" field must contain text alert('NoteAssist - Error, "Force abort timer" must be a number'); return; } new_settings[key] = inputValue; } else { alert('NoteAssist - Error, input field(s) cannot be blank'); return; } } } NA.LS_setValue('settings', JSON.stringify(new_settings)); NA.settings = new_settings; //done saving, remove the setting menu & overlay now. NA.settingsMenuClose(); }; NA.settingsMenuClose = function () { $('#noteAssist_settingMenu').remove(); $('#noteAssist_settingMenuOverlay').remove(); }; //========================================================== // UI //========================================================== NA.initUi = function () { var container = NA.$c('div', { id: 'noteAssist_ui' }); container.innerHTML = '' + '' + 'Note Assist
' + 'X
' + 'Text color (override):
' + ' Black' + ' White' + ' Detect' + '' + 'Text decoration functions
' + ' ' + ' ' + ' ' + '' + '