Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Add frontend for text based needles #5165

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 71 additions & 11 deletions assets/javascripts/needlediff.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,64 @@ function NeedleDiff(id, width, height) {
});
}

/**
* Function ensuring that the text fits into the area rectangle.
* @param {Object} ctx - The Canvas context.
* @param {Object} area - The OCR area to be drawn into.
* @param {Object} txtStyle - The target text style for calibration.
* @returns {number} The calculated font size.
*/
function calcFontSize(ctx, area, txtStyle) {
const ocrLines = area.ocr_str.split('\n');

// 1px margin in height
let ret = Math.ceil(((area.height - ocrLines.length - 1) / ocrLines.length) - 1);

ctx.font = txtStyle.weight + ' ' + ret + 'px ' + txtStyle.typeface;
for (line of ocrLines) {
let ocrLinesWidth = ctx.measureText(line).width;
while (ocrLinesWidth > area.width - 2) {
ret--;
ctx.font = txtStyle.weight + ' ' + ret + 'px ' + txtStyle.typeface;
ocrLinesWidth = ctx.measureText(line).width;
}
}
return ret;
}

/**
* Method drawing the OCR text into the OCR area.
* @param {Object} needlediff - The needlediff containing the canvas context.
* @param {Object} area - The OCR area to be drawn into.
*/
function drawOcrTxt(needlediff, area) {
const txtStyle = {
alignment: 'center',
typeface: 'Arial',
color: 'rgb(64,224,208)',
weight: 'bold'
}
const fontSize = calcFontSize(needlediff.ctx, area, txtStyle);
const ocrLines = area.ocr_str.split('\n');
const ocrTxtXPos = Math.ceil(area.xpos + area.width / 2);

// Body line appears to be at about 1/3 of whole font height
let ocrTxtYPos = Math.floor(area.ypos + area.height / 2 + fontSize / 3 -
((ocrLines.length - 1) * fontSize) / 2);

needlediff.ctx.textAlign = txtStyle.alignment;
needlediff.ctx.fillStyle = txtStyle.color;
needlediff.ctx.font = txtStyle.weight + ' ' + fontSize + 'px ' +
txtStyle.typeface;

for (line of ocrLines) {
needlediff.ctx.fillText(line, ocrTxtXPos, ocrTxtYPos);

// 1px space between lines
ocrTxtYPos += fontSize + 1;
}
}

NeedleDiff.prototype.draw = function () {
// First of all, draw the screenshot as gray background (if ready)
if (!this.screenshotImg) {
Expand All @@ -89,11 +147,6 @@ NeedleDiff.prototype.draw = function () {
this.ctx.drawImage(this.screenshotImg, 0, 0);
}

// Then, check if there is a needle to compare with
if (!this.needleImg) {
return;
}

// Calculate the pixel in which the division will be done
var split = this.divide * this.width;
if (split < 1) {
Expand Down Expand Up @@ -138,7 +191,12 @@ NeedleDiff.prototype.draw = function () {
if (!this.fullNeedleImg) {
// draw matching part of needle image
this.ctx.strokeStyle = NeedleDiff.strokecolor(a.type);
this.ctx.drawImage(this.needleImg, orig.xpos, orig.ypos, usedWith, a.height, x, a.ypos, usedWith, a.height);
if (this.needleImg) {
this.ctx.drawImage(this.needleImg, orig.xpos, orig.ypos, usedWith, a.height, x, a.ypos, usedWith, a.height);
} else if (a.ocr_str) {
drawOcrTxt(this, a);
} else return;

// draw frame of match area
this.ctx.lineWidth = lineWidth;
this.ctx.beginPath();
Expand Down Expand Up @@ -221,6 +279,7 @@ NeedleDiff.prototype.draw = function () {
this.ctx.strokeStyle = 'rgb(0, 0, 0)';
this.ctx.lineWidth = 3;
this.ctx.font = 'bold 14px Arial';
this.ctx.textAlign = 'left';
var text = a['similarity'] + '%';
var textSize = this.ctx.measureText(text);
var tx;
Expand Down Expand Up @@ -326,16 +385,17 @@ function setDiffScreenshot(screenshotSrc) {
$('<img src="' + screenshotSrc + '">').on('load', function () {
var image = $(this).get(0);

// set screenshot resolution
window.differ = new NeedleDiff('needle_diff', image.width, image.height);
window.differ.screenshotImg = image;
setNeedle();

// create gray version of it in off screen canvas
var gray_canvas = document.createElement('canvas');
gray_canvas.width = image.width;
gray_canvas.height = image.height;

// set screenshot resolution
window.differ = new NeedleDiff('needle_diff', image.width, image.height);
window.differ.screenshotImg = image;
window.differ.gray_canvas = gray_canvas;
setNeedle();

var gray_context = gray_canvas.getContext('2d');

gray_context.drawImage(image, 0, 0);
Expand Down
77 changes: 73 additions & 4 deletions assets/javascripts/needleeditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,13 @@ NeedleEditor.prototype.init = function () {
return;
}
a.type = NeedleEditor.nexttype(a.type);

updateAreaCtrls(a);

// Attrs only apply to some area types and are not retained elsewhere.
delete a.refstr;
delete a.margin;

shape.fill = NeedleEditor.areacolor(a.type);
editor.UpdateTextArea();
cv.redraw();
Expand Down Expand Up @@ -110,15 +117,14 @@ NeedleEditor.prototype.init = function () {
cv.addShape(shape);
editor.needle.area.push(a);
editor.UpdateTextArea();
updateAreaCtrls(a);
return shape;
};
var areaSpecificButtons = $('#change-match, #change-margin, #toggle-click-coordinates');
$(cv).on('shape.selected', function () {
areaSpecificButtons.removeClass('disabled').removeAttr('disabled');
updateToggleClickCoordinatesButton(editor.currentClickCoordinates());
updateAreaCtrls(editor.selection().area);
});
$(cv).on('shape.unselected', function () {
areaSpecificButtons.addClass('disabled').attr('disabled', 1);
updateAreaCtrls(null);
});

document.getElementById('needleeditor_name').onchange = function () {
Expand Down Expand Up @@ -160,6 +166,15 @@ NeedleEditor.prototype.AddTag = function (tag, checked) {
return input;
};

/**
* Method updating the reference string and it's in it's text box and the
* corresponding attribute of it's area object.
*/
NeedleEditor.prototype.updateRefstr = function () {
this.selection().area.refstr = document.getElementById('txtarea-refstr').value;
this.UpdateTextArea();
};

NeedleEditor.nexttype = function (type) {
if (type == 'match') {
return 'exclude';
Expand Down Expand Up @@ -377,6 +392,7 @@ NeedleEditor.prototype.toggleClickCoordinates = function () {

function loadBackground() {
var needle = window.needles[$('#image_select option:selected').val()];
needle = needle.imageurl ? needle : window.needles['screenshot'];
nEditor.LoadBackground(needle.imageurl);
$('#needleeditor_image').val(needle.imagename);
$('#needleeditor_imagedistri').val(needle.imagedistri);
Expand Down Expand Up @@ -440,6 +456,55 @@ function loadAreas() {
}
}

/**
* Method disabling a HTML element.
* @param {string} elemId - The ID of the element in the HTML document.
*/
function disableElem(elemId) {
const elem = document.getElementById(elemId);
elem.classList.add('disabled');
elem.disabled = 'disabled';
}

/**
* Method enabling a HTML element.
* @param {string} elemId - The ID of the element in the HTML document.
*/
function enableElem(elemId) {
const elem = document.getElementById(elemId);
elem.classList.remove('disabled');
elem.removeAttribute('disabled');
}

/**
* Method enabling/disabling controls for a specific area.
* @param {Object} area - The area which the controls should be prepared for.
*/
function updateAreaCtrls(area) {

const ctrlElemIds = {
all: ['change-match', 'change-margin', 'toggle-click-coordinates', 'txtarea-refstr'],
match: ['change-match', 'change-margin', 'toggle-click-coordinates'],
ocr: ['change-match', 'toggle-click-coordinates', 'txtarea-refstr'],
exclude: ['toggle-click-coordinates']
}

if (!area || !(area.type in ctrlElemIds)) {
ctrlElemIds.all.forEach(elem => disableElem(elem));
document.getElementById('txtarea-refstr').value = '';
return;
}
ctrlElemIds[area.type].forEach(elem => enableElem(elem));
const otherElemIds = ctrlElemIds.all.filter(elem => !ctrlElemIds[area.type].includes(elem));
otherElemIds.forEach(elem => disableElem(elem));

if (area.type === 'ocr') {
document.getElementById('txtarea-refstr').value = area.refstr;
} else {
document.getElementById('txtarea-refstr').value = '';
}
}

function addTag() {
var input = $('#newtag');
var checkbox = nEditor.AddTag(input.val(), false);
Expand All @@ -457,6 +522,10 @@ function setMatch() {
nEditor.setMatch($('#match').val());
}

/** Method triggering update in refstr text box in needle editor instance. */
function updateRefstr() {
nEditor.updateRefstr();
}
function toggleClickCoordinates() {
updateToggleClickCoordinatesButton(nEditor.toggleClickCoordinates());
}
Expand Down
2 changes: 1 addition & 1 deletion docs/Contributing.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ In the case of os-autoinst, only a few http://www.cpan.org/[CPAN] modules are
required. Basically `Carp::Always`, `Data::Dump`. `JSON` and `YAML`. On the other
hand, several external tools are needed including
http://wiki.qemu.org/Main_Page[QEMU],
https://code.google.com/p/tesseract-ocr/[Tesseract] and
https://www-e.uni-magdeburg.de/jschulen/ocr/[GOCR] and
http://optipng.sourceforge.net/[OptiPNG]. Last but not least, the
http://opencv.org/[OpenCV] library is the core of the openQA image matching
mechanism, so it must be available on the system.
Expand Down
23 changes: 12 additions & 11 deletions docs/GettingStarted.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -193,14 +193,14 @@ information and results (if any) are kept for future reference.

One of the main mechanisms for openQA to know the state of the virtual machine
is checking the presence of some elements in the machine's 'screen'.
This is performed using fuzzy image matching between the screen and the so
called 'needles'. A needle specifies both the elements to search for and a
This is performed matching a reference (so called 'needle') with the screen.
A needle specifies both the elements to search for and a
list of tags used to decide which needles should be used at any moment.

A needle consists of a full screenshot in PNG format and a json file with
the same name (e.g. foo.png and foo.json) containing the associated data, like
which areas inside the full screenshot are relevant or the mentioned list of
tags.
A needle consists of at least a JSON file and, optionally, a full screenshot
in PNG format with the same name (e.g. foo.png and foo.json). The JSON file
contains the associated data, like which areas inside the full screenshot are
relevant and the mentioned list of tags.

[source,json]
-------------------------------------------------------------------
Expand All @@ -212,7 +212,8 @@ tags.
"width" : INTEGER,
"height" : INTEGER,
"type" : ( "match" | "ocr" | "exclude" ),
"match" : INTEGER, // 0-100. similarity percentage
"match" : INTEGER, // 0-100. similarity percentage,
"refstr": STRING
},
...
],
Expand All @@ -229,11 +230,11 @@ There are three kinds of areas:
with at least the specified similarity percentage. Regular areas are
displayed as green boxes in the needle editor and as green or red frames
in the needle view (green for matching areas, red for non-matching ones).
* *OCR areas* also define relevant parts of the screenshot. However, an OCR
algorithm is used for matching. In the needle editor OCR areas are
* *OCR areas* also define relevant parts of the screenshot. They are
converted to text in order to be matched on an OCR reference text. The
reference text is stored in the needle. In the needle editor OCR areas are
displayed as orange boxes. To turn a regular area into an OCR area within
the needle editor, double click the concerning area twice. Note that such
needles are only rarely used.
the needle editor, double click the concerning area twice.
* *Exclude areas* can be used to ignore parts of the reference picture.
In the needle editor exclude areas are displayed as red boxes. To turn a
regular area into an exclude area within the needle editor, double click
Expand Down
2 changes: 1 addition & 1 deletion lib/OpenQA/Schema/Result/Jobs.pm
Original file line number Diff line number Diff line change
Expand Up @@ -1029,7 +1029,7 @@ sub append_log ($self, $log, $file_name) {
my $path = $self->worker->get_property('WORKER_TMPDIR');
return unless -d $path; # we can't help
$path .= "/$file_name";
if (open(my $fd, '>>', $path)) {
if (open(my $fd, '>>:utf8', $path)) {
print $fd $log->{data};
close($fd);
}
Expand Down
Loading