Skip to content
This repository has been archived by the owner on Oct 29, 2020. It is now read-only.

refactor: replace canvas with sharp #79

Merged
merged 1 commit into from
Sep 29, 2020
Merged
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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,12 @@
},
"dependencies": {
"@votingworks/ballot-encoder": "^4.0.0",
"canvas": "^2.6.1",
"chalk": "^4.0.0",
"debug": "^4.1.1",
"jsfeat": "^0.0.8",
"jsqr": "^1.3.1",
"node-quirc": "^2.2.1",
"sharp": "^0.26.1",
"table": "^5.4.6",
"uuid": "^7.0.3"
},
Expand All @@ -58,6 +58,7 @@
"@types/jsfeat": "./types/jsfeat",
"@types/memorystream": "^0.3.0",
"@types/node": "^13.11.1",
"@types/sharp": "^0.26.0",
"@types/table": "^5.0.0",
"@types/uuid": "^7.0.2",
"@typescript-eslint/eslint-plugin": "^4.1.0",
Expand Down
48 changes: 24 additions & 24 deletions src/Interpreter.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { BallotType } from '@votingworks/ballot-encoder'
import * as choctaw2020Special from '../test/fixtures/choctaw-2020-09-22-f30480cc99'
import * as choctawMock2020 from '../test/fixtures/choctaw-county-mock-general-election-choctaw-2020-e87f23ca2c'
import {
blankPage1,
blankPage2,
Expand All @@ -7,13 +10,10 @@ import {
partialBorderPage2,
} from '../test/fixtures/election-4e31cb17d8-ballot-style-77-precinct-oaklawn-branch-library'
import * as hamilton from '../test/fixtures/election-5c6e578acf-state-of-hamilton-2020'
import * as choctaw2019 from '../test/fixtures/election-98f5203139-choctaw-general-2019'
import * as choctaw2020 from '../test/fixtures/election-7c61368c3b-choctaw-general-2020'
import * as choctawMock2020 from '../test/fixtures/choctaw-county-mock-general-election-choctaw-2020-e87f23ca2c'
import * as choctaw2020Special from '../test/fixtures/choctaw-2020-09-22-f30480cc99'
import * as choctaw2019 from '../test/fixtures/election-98f5203139-choctaw-general-2019'
import Interpreter from './Interpreter'
import { DetectQRCodeResult, BallotTargetMark } from './types'
import { BallotType } from '@votingworks/ballot-encoder'
import { BallotTargetMark, DetectQRCodeResult } from './types'

test('interpret three-column template with instructions', async () => {
const interpreter = new Interpreter(election)
Expand Down Expand Up @@ -1614,7 +1614,7 @@ test('interpret votes', async () => {
},
Object {
"option": "Jane Bland",
"score": 0.5717884130982368,
"score": 0.5714285714285714,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The decoding of images is slightly different between canvas and sharp, so we get different scores.

"type": "candidate",
},
Object {
Expand All @@ -1629,12 +1629,12 @@ test('interpret votes', async () => {
},
Object {
"option": "Write-In",
"score": 0.66,
"score": 0.6567164179104478,
"type": "candidate",
},
Object {
"option": "John Ames",
"score": 0.7860824742268041,
"score": 0.7866323907455013,
"type": "candidate",
},
Object {
Expand All @@ -1649,7 +1649,7 @@ test('interpret votes', async () => {
},
Object {
"option": "Chad Prda",
"score": 0.601010101010101,
"score": 0.5989974937343359,
"type": "candidate",
},
Object {
Expand Down Expand Up @@ -1985,7 +1985,7 @@ test('invalid marks', async () => {
"type": "yesno",
},
"option": "yes",
"score": 0.14910025706940874,
"score": 0.1483375959079284,
"target": Object {
"bounds": Object {
"height": 21,
Expand Down Expand Up @@ -2037,10 +2037,10 @@ test('invalid marks', async () => {
},
Object {
"bounds": Object {
"height": 22,
"height": 21,
"width": 32,
"x": 470,
"y": 315,
"y": 316,
},
"contest": Object {
"description": "Shall the Dallas County extend the Recycling Program countywide?",
Expand All @@ -2054,10 +2054,10 @@ test('invalid marks', async () => {
"score": 0,
"target": Object {
"bounds": Object {
"height": 22,
"height": 21,
"width": 32,
"x": 470,
"y": 315,
"y": 316,
},
"inner": Object {
"height": 18,
Expand All @@ -2070,10 +2070,10 @@ test('invalid marks', async () => {
},
Object {
"bounds": Object {
"height": 22,
"height": 21,
"width": 32,
"x": 470,
"y": 365,
"y": 366,
},
"contest": Object {
"description": "Shall the Dallas County extend the Recycling Program countywide?",
Expand All @@ -2087,10 +2087,10 @@ test('invalid marks', async () => {
"score": 0.7455470737913485,
"target": Object {
"bounds": Object {
"height": 22,
"height": 21,
"width": 32,
"x": 470,
"y": 365,
"y": 366,
},
"inner": Object {
"height": 18,
Expand Down Expand Up @@ -2523,7 +2523,7 @@ test('invalid marks', async () => {
},
Object {
"bounds": Object {
"height": 22,
"height": 21,
"width": 32,
"x": 470,
"y": 1037,
Expand Down Expand Up @@ -2577,7 +2577,7 @@ test('invalid marks', async () => {
"score": 0,
"target": Object {
"bounds": Object {
"height": 22,
"height": 21,
"width": 32,
"x": 470,
"y": 1037,
Expand Down Expand Up @@ -2663,7 +2663,7 @@ test('invalid marks', async () => {
},
Object {
"bounds": Object {
"height": 22,
"height": 21,
"width": 32,
"x": 470,
"y": 1137,
Expand Down Expand Up @@ -2717,7 +2717,7 @@ test('invalid marks', async () => {
"score": 0,
"target": Object {
"bounds": Object {
"height": 22,
"height": 21,
"width": 32,
"x": 470,
"y": 1137,
Expand Down Expand Up @@ -2873,10 +2873,10 @@ test('invalid marks', async () => {
"y": 333,
},
"inner": Object {
"height": 17,
"height": 18,
"width": 28,
"x": 874,
"y": 335,
"y": 334,
},
},
"type": "candidate",
Expand Down
2 changes: 1 addition & 1 deletion src/utils/binarize.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createImageData } from 'canvas'
import { croppedQRCode } from '../../test/fixtures'
import { binarize, RGBA_BLACK, RGBA_WHITE } from './binarize'
import { createImageData } from './canvas'

test('binarize grayscale', async () => {
const imageData = createImageData(2, 2)
Expand Down
33 changes: 33 additions & 0 deletions src/utils/canvas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
const RGBA_CHANNEL_COUNT = 4

export function createImageData(
data: Uint8ClampedArray,
width: number,
height: number
): ImageData
export function createImageData(width: number, height: number): ImageData
export function createImageData(...args: unknown[]): ImageData {
let data: Uint8ClampedArray
let width: number
let height: number

if (
args.length === 2 &&
typeof args[0] === 'number' &&
typeof args[1] === 'number'
) {
;[width, height] = args
data = new Uint8ClampedArray(width * height * RGBA_CHANNEL_COUNT)
} else if (
args.length === 3 &&
args[0] instanceof Uint8ClampedArray &&
typeof args[1] === 'number' &&
typeof args[2] === 'number'
) {
;[data, width, height] = args
} else {
throw new TypeError('unexpected arguments given to createImageData')
}

return { data, width, height }
}
2 changes: 1 addition & 1 deletion src/utils/crop.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createImageData } from 'canvas'
import { randomImage, randomInset } from '../../test/utils'
import { Rect } from '../types'
import { createImageData } from './canvas'
import crop from './crop'

/**
Expand Down
2 changes: 1 addition & 1 deletion src/utils/crop.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createImageData } from 'canvas'
import { Rect } from '../types'
import { createImageData } from './canvas'

/**
* Returns a new image cropped to the specified bounds.
Expand Down
2 changes: 1 addition & 1 deletion src/utils/flip.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createImageData } from 'canvas'
import { createImageData } from './canvas'
import { vh } from './flip'

test('vh does nothing to 1x1 image (rgba)', () => {
Expand Down
6 changes: 2 additions & 4 deletions src/utils/jsfeat/matToImageData.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { createCanvas } from 'canvas'
import * as jsfeat from 'jsfeat'
import { createImageData } from '../canvas'

export default function matToImageData(mat: jsfeat.matrix_t): ImageData {
const canvas = createCanvas(mat.cols, mat.rows)
const ctx = canvas.getContext('2d')
const imageData = ctx.getImageData(0, 0, mat.cols, mat.rows)
const imageData = createImageData(mat.cols, mat.rows)
const data_u32 = new Uint32Array(imageData.data.buffer)
const alpha = 0xff << 24
let i = mat.cols * mat.rows
Expand Down
2 changes: 1 addition & 1 deletion src/utils/outline.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createImageData } from 'canvas'
import { PIXEL_BLACK } from './binarize'
import { createImageData } from './canvas'
import { getImageChannelCount } from './imageFormatUtils'

/**
Expand Down
31 changes: 14 additions & 17 deletions src/utils/qrcode/quirc.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,25 @@
import { createCanvas } from 'canvas'
import makeDebug from 'debug'
import sharp, { Channels } from 'sharp'
import { DetectQRCodeResult } from '../../types'
import { withCropping } from './withCropping'

const debug = makeDebug('hmpb-interpreter:quirc')

const PNG_DATA_URL_PREFIX = 'data:image/png;base64,'

/**
* Encodes an image as a PNG.
*/
function toPNGData(imageData: ImageData): Buffer {
const canvas = createCanvas(imageData.width, imageData.height)
const context = canvas.getContext('2d')

context.putImageData(imageData, 0, 0)

const dataURL = canvas.toDataURL('image/png')

if (!dataURL.startsWith(PNG_DATA_URL_PREFIX)) {
throw new Error(`PNG data URL has unexpected format: ${dataURL}`)
}

return Buffer.from(dataURL.slice(PNG_DATA_URL_PREFIX.length), 'base64')
async function toPNGData(imageData: ImageData): Promise<Buffer> {
return await sharp(Buffer.from(imageData.data.buffer), {
raw: {
width: imageData.width,
height: imageData.height,
channels: (imageData.data.length /
imageData.width /
imageData.height) as Channels,
},
})
.png()
.toBuffer()
}

/**
Expand All @@ -36,7 +33,7 @@ export async function detect(
// Unfortunately, quirc requires either JPEG or PNG encoded images and can't
// handle raw bitmaps.
const quirc = await import('node-quirc')
const result = await quirc.decode(toPNGData(imageData))
const result = await quirc.decode(await toPNGData(imageData))

for (const symbol of result) {
if (!('err' in symbol)) {
Expand Down
21 changes: 10 additions & 11 deletions src/utils/readImageData.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
import { createCanvas, loadImage } from 'canvas'
import { promises as fs } from 'fs'
import sharp from 'sharp'

export async function readImageData(fileData: Buffer): Promise<ImageData>
export async function readImageData(filePath: string): Promise<ImageData>
export async function readImageData(
filePathOrData: string | Buffer
): Promise<ImageData> {
const fileData =
typeof filePathOrData === 'string'
? await fs.readFile(filePathOrData)
: filePathOrData
const image = await loadImage(fileData)
const canvas = createCanvas(image.width, image.height)
const context = canvas.getContext('2d')
context.drawImage(image, 0, 0)
return context.getImageData(0, 0, image.width, image.height)
const img = await sharp(filePathOrData)
.raw()
.ensureAlpha()
.toBuffer({ resolveWithObject: true })
return {
data: Uint8ClampedArray.from(img.data),
width: img.info.width,
height: img.info.height,
}
}
Loading