Browse Source

Initial commit; 1.0.0

master
Sven Slootweg 6 years ago
commit
3950dce32f
  1. 2
      .gitignore
  2. 1
      .npmignore
  3. 98
      README.md
  4. 18
      gulpfile.js
  5. 3
      index.js
  6. 34
      package.json
  7. 8
      src/create-canvas.js
  8. 10
      src/draw-character.js
  9. 42
      src/find-edge.js
  10. 6
      src/get-image-data.js
  11. 55
      src/index.js
  12. 7
      src/reset-canvas.js
  13. 15
      src/scan-row.js

2
.gitignore

@ -0,0 +1,2 @@
/node_modules/
/lib/

1
.npmignore

@ -0,0 +1 @@
/node_modules/

98
README.md

@ -0,0 +1,98 @@
# measure-font
Measures the dimensions and baseline offsets of a font in the browser. Written as a workaround for lacking `measureText`/`TextMetrics` support in browsers.
## Caveats
* This only works in a browser environment, or some other kind of environment that implements the Canvas API. Support for the following is required:
* `document.createElement`
* `context.getImageData`
* I'm not an expert at either fonts or text rendering. This implementation may very well be wrong in some way, even if it worked for my purposes. Patches welcome!
* If you are using Web Fonts, make sure that all the fonts have loaded __before__ trying to measure them. Otherwise, your measurements will be wrong.
## License
[WTFPL](http://www.wtfpl.net/txt/copying/) or [CC0](https://creativecommons.org/publicdomain/zero/1.0/), whichever you prefer. A donation and/or attribution are appreciated, but not required.
## Donate
Maintaining open-source projects takes a lot of time, and the more donations I receive, the more time I can dedicate to open-source. If this module is useful to you, consider [making a donation](http://cryto.net/~joepie91/donate.html)!
You can donate using Bitcoin, PayPal, Flattr, cash-in-mail, SEPA transfers, and pretty much anything else. Thank you!
## Contributing
Pull requests welcome. Please make sure your modifications are in line with the overall code style, and ensure that you're editing the files in `src/`, not those in `lib/`.
Build tool of choice is `gulp`; simply run `gulp` while developing, and it will watch for changes.
Be aware that by making a pull request, you agree to release your modifications under the licenses stated above.
## Measuring precision
This library accepts two options that control the precision of the measurements: `fontSize` and `tolerance`. These are __not__ required to be set, and you do __not__ need to re-measure the font for every font size you wish to use - all measurements are expressed in multipliers, that you can apply to any font size to get the right offsets for that font size. See the "Using the measurements" section for more details.
Fonts are measured by rendering a number of reference characters on an invisible canvas. The `fontSize` option is used to determine what size the characters should be rendered at - the larger the `fontSize`, the more accurate the edge detection will be (leading to a more accurate measurement). In practice, the default font size of `20` is usually sufficient.
The invisible canvas needs to have a certain width and height, large enough to fit the measured
characters. You can control the size of this canvas using the `tolerance` parameter - both the width and the height of the canvas will be `fontSize * tolerance` pixels large. The default tolerance of `6` (3 units in each direction) should be more than sufficient for most cases, but if you're dealing with very strangely sized fonts, you may want to increase this value.
It's __not recommended__ to reduce the `tolerance` below the default value of `6`, unless you have very stringent performance requirements and have thoroughly tested and verified that your `tolerance` setting works correctly. Setting the `tolerance` too low will result in measurements silently failing, yielding incorrect results. Often, the better solution to performance issues is to simply [memoize](https://www.npmjs.com/package/memoizee) the measurement results.
## Using the measurements
All measurements are *multipliers*, and are *relative to the alphabetic baseline*. That means that if you have an `ascender` measurement of `-0.75`, you would calculate the tallest possible ascender in a string at a `40px` font size as follows:
```js
tallestPossibleAscender = measurements.ascender * 40;
```
This would yield a `tallestPossibleAscender` of `-30` - in other words, the tallest possible ascender reaches up to `baselineHeight - 30`.
Typically, only the `descender` is a positive number (since it ends up below the baseline), and all the other measurements are negative numbers (since they end up above the baseline).
## Usage
A simple example:
```javascript
const measureFont = require("measure-font");
let sansSerifMeasurements = measureFont("sans-serif");
console.log(sansSerifMeasurements); // {descender: 0.15, ascender: -0.75, capHeight: -0.7, median: -0.55, topBounding: -0.75}
```
With a custom precision option:
```javascript
const measureFont = require("measure-font");
let sansSerifMeasurements = measureFont("sans-serif", {
fontSize: 40
});
console.log(sansSerifMeasurements); // {descender: 0.15, ascender: -0.75, capHeight: -0.7, median: -0.55, topBounding: -0.75}
```
## API
### measureFont(fontName, [options])
Measures the specified font.
* __fontName__: The (CSS) name of the font family you wish to measure.
* __options__: *Optional.* See the "Measuring precision" section above for more details.
* __fontSize__: The font size (in `px`) to measure at.
* __tolerance__: The size multiplier for the measuring canvas.
Returns an object containing measurements. All measurements are *multipliers*, relative to the alphabetic baseline.
* __descender__: The offset of the lowest descender.
* __ascender__: The offset of the highest ascender.
* __capHeight__: The offset of the cap height.
* __median__: The offset of the mean line (usually equals the x-height).
* __topBounding__: The offset of the top bounding line, ie. the tallest a regular character could be (this can extend beyond the highest ascender).
Reference image (from [Wikipedia](https://en.wikipedia.org/wiki/File:Typography_Line_Terms.svg)):
![Font dimension visualization](https://upload.wikimedia.org/wikipedia/commons/3/39/Typography_Line_Terms.svg)

18
gulpfile.js

@ -0,0 +1,18 @@
var gulp = require("gulp");
var presetES2015 = require("@joepie91/gulp-preset-es2015");
var source = ["src/**/*.js"]
gulp.task('babel', function() {
return gulp.src(source)
.pipe(presetES2015({
basePath: __dirname
}))
.pipe(gulp.dest("lib/"));
});
gulp.task("watch", function () {
gulp.watch(source, ["babel"]);
});
gulp.task("default", ["babel", "watch"]);

3
index.js

@ -0,0 +1,3 @@
'use strict';
module.exports = require("./lib");

34
package.json

@ -0,0 +1,34 @@
{
"name": "measure-font",
"version": "1.0.0",
"description": "Measures the dimensions and baseline offsets of a font in the browser",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"gulp": "gulp"
},
"repository": {
"type": "git",
"url": "http://git.cryto.net/joepie91/measure-font.git"
},
"keywords": [
"fonts",
"canvas",
"measuring",
"measurements",
"TextMetrics",
"measureText",
"workaround",
"text"
],
"author": "Sven Slootweg",
"license": "WTFPL",
"dependencies": {
"default-value": "^1.0.0"
},
"devDependencies": {
"@joepie91/gulp-preset-es2015": "^1.0.1",
"babel-preset-es2015": "^6.6.0",
"gulp": "^3.9.1"
}
}

8
src/create-canvas.js

@ -0,0 +1,8 @@
'use strict';
module.exports = function createCanvas(size) {
let canvas = document.createElement("canvas");
canvas.width = size;
canvas.height = size;
return canvas;
};

10
src/draw-character.js

@ -0,0 +1,10 @@
'use strict';
module.exports = function drawCharacter(canvas, character, fontFamily, fontSize) {
let context = canvas.getContext("2d");
context.textAlign = "center";
context.textBaseline = "alphabetic";
context.font = `${fontSize}px '${fontFamily}'`;
context.fillStyle = "white";
context.fillText(character, canvas.width / 2, canvas.height / 2);
};

42
src/find-edge.js

@ -0,0 +1,42 @@
'use strict';
const getImageData = require("./get-image-data");
const scanRow = require("./scan-row");
function findEdge(canvas, firstRow, lastRow, step) {
let imageData = getImageData(canvas).data;
let valuesPerRow = canvas.width * 4;
let hitEnd = false;
if (step === 0) {
throw new Error("Step cannot be 0");
}
let row = firstRow;
while(!hitEnd) {
let highestValue = scanRow(imageData, (row * valuesPerRow), canvas.width);
/* 240 is a somewhat randomly picked value to deal with anti-aliasing. */
if (highestValue > 240) {
return row;
}
row += step;
if (step > 0) {
hitEnd = (row > lastRow);
} else if (step < 0) {
hitEnd = (row < lastRow);
}
}
}
module.exports = {
lowest: function findLowestEdge(canvas) {
return findEdge(canvas, canvas.height - 1, 0, -1);
},
highest: function findHighestEdge(canvas) {
return findEdge(canvas, 0, canvas.height - 1, 1);
}
};

6
src/get-image-data.js

@ -0,0 +1,6 @@
'use strict';
module.exports = function getImageData(canvas) {
let context = canvas.getContext("2d");
return context.getImageData(0, 0, canvas.width, canvas.height);
};

55
src/index.js

@ -0,0 +1,55 @@
'use strict';
const defaultValue = require("default-value");
const findEdge = require("./find-edge");
const drawCharacter = require("./draw-character");
const resetCanvas = require("./reset-canvas");
const createCanvas = require("./create-canvas");
module.exports = function measureFont(fontFamily, options = {}) {
let fontSize = defaultValue(options.fontSize, 20);
let canvasSize = defaultValue(options.tolerance, 6) * fontSize;
let descenderCharacters = ["g", "j", "p", "q", "y"];
let ascenderCharacters = ["h", "d", "t", "l"];
let capHeightCharacters = ["H", "I", "T"];
let medianCharacters = ["x", "v", "w", "z"];
let topBoundingCharacters = ["O", "A", "8", "#", "%", "^", "!", "/", "|", "]"];
let testingCanvas = createCanvas(canvasSize);
function getLowest(characters) {
resetCanvas(testingCanvas);
characters.forEach((character) => {
drawCharacter(testingCanvas, character, fontFamily, fontSize);
});
return findEdge.lowest(testingCanvas);
}
function getHighest(characters) {
resetCanvas(testingCanvas);
characters.forEach((character) => {
drawCharacter(testingCanvas, character, fontFamily, fontSize);
});
return findEdge.highest(testingCanvas);
}
let lowestDescenderPoint = getLowest(descenderCharacters) - (testingCanvas.height / 2);
let highestAscenderPoint = (testingCanvas.height / 2) - getHighest(ascenderCharacters);
let highestCapHeightPoint = (testingCanvas.height / 2) - getHighest(capHeightCharacters);
let highestMedianPoint = (testingCanvas.height / 2) - getHighest(medianCharacters);
let highestTopBoundingPoint = (testingCanvas.height / 2) - getHighest(topBoundingCharacters);
return {
descender: lowestDescenderPoint / fontSize,
ascender: -highestAscenderPoint / fontSize,
capHeight: -highestCapHeightPoint / fontSize,
median: -highestMedianPoint / fontSize,
topBounding: -highestTopBoundingPoint / fontSize
}
}

7
src/reset-canvas.js

@ -0,0 +1,7 @@
'use strict';
module.exports = function resetCanvas(canvas) {
let context = canvas.getContext("2d");
context.fillStyle = "black";
context.fillRect(0, 0, canvas.width, canvas.height);
};

15
src/scan-row.js

@ -0,0 +1,15 @@
'use strict';
module.exports = function scanRow(imageData, offset, length) {
let highestValue = 0;
for (let column = 0; column < length; column += 1) {
let pixelValue = imageData[offset + (column * 4)];
if (pixelValue > highestValue) {
highestValue = pixelValue;
}
}
return highestValue;
};
Loading…
Cancel
Save