-
Notifications
You must be signed in to change notification settings - Fork 315
/
Copy pathdrawing-builtin.js
442 lines (398 loc) · 12.5 KB
/
drawing-builtin.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
// drawing-builtin.js
//
// The aliased (except the fonts) graphics used by drawing-canvas.js and
// drawing-png.js
//
// All x,y and lengths are integer values.
//
// For the methods that take a color `rgb` parameter, the value is always a
// string with format RRGGBB.
//
// opts is the same options object passed into the bwipjs methods.
function DrawingBuiltin(opts) {
var floor = Math.floor;
// Unrolled x,y rotate/translate matrix
var tx0 = 0, tx1 = 0, tx2 = 0, tx3 = 0;
var ty0 = 0, ty1 = 0, ty2 = 0, ty3 = 0;
var gs_image, gs_rowbyte; // rowbyte will be 1 for png's, 0 for canvas
var gs_width, gs_height; // image size, in pixels
var gs_dx, gs_dy; // x,y translate (padding)
var gs_r, gs_g, gs_b; // rgb
var gs_xymap; // even-odd edge map
return {
// Ensure compliant bar codes by always using integer scaling factors.
scale : function(sx, sy) {
return [ (sx|0)||1, (sy|0)||1 ];
},
// Measure text. This and scale() are the only drawing primitives that
// are called before init().
//
// `font` is the font name typically OCR-A or OCR-B.
// `fwidth` and `fheight` are the requested font cell size. They will
// usually be the same, except when the scaling is not symetric.
measure : function(str, font, fwidth, fheight) {
fwidth = fwidth|0;
fheight = fheight|0;
var fontid = FontLib.lookup(font);
var width = 0;
var ascent = 0;
var descent = 0;
for (var i = 0, l = str.length; i < l; i++) {
var ch = str.charCodeAt(i);
var glyph = FontLib.getglyph(fontid, ch, fwidth, fheight);
ascent = Math.max(ascent, glyph.top);
descent = Math.max(descent, glyph.height - glyph.top);
if (i == l-1) {
width += glyph.left + glyph.width;
} else {
width += glyph.advance;
}
}
return { width:width, ascent:ascent, descent:descent };
},
// width and height represent the maximum bounding box the graphics will occupy.
// The dimensions are for an unrotated rendering. Adjust as necessary.
init : function(width, height) {
// Add in the effects of padding. These are always set before the
// drawing constructor is called.
var padl = opts.paddingleft;
var padr = opts.paddingright;
var padt = opts.paddingtop;
var padb = opts.paddingbottom;
var rot = opts.rotate || 'N';
width += padl + padr;
height += padt + padb;
if (+opts.sizelimit && +opts.sizelimit < width * height) {
throw new Error('Image size over limit');
}
// Transform indexes are: x, y, w, h
switch (rot) {
// tx = w-y, ty = x
case 'R': tx1 = -1; tx2 = 1; ty0 = 1; break;
// tx = w-x, ty = h-y
case 'I': tx0 = -1; tx2 = 1; ty1 = -1; ty3 = 1; break;
// tx = y, ty = h-x
case 'L': tx1 = 1; ty0 = -1; ty3 = 1; break;
// tx = x, ty = y
default: tx0 = ty1 = 1; break;
}
// Setup the graphics state
var swap = rot == 'L' || rot == 'R';
gs_width = swap ? height : width;
gs_height = swap ? width : height;
gs_dx = padl;
gs_dy = padt;
gs_xymap = [];
gs_xymap.min = Infinity;
gs_r = gs_g = gs_b = 0;
// Get the rgba image from the constructor
var res = this.image(gs_width, gs_height);
gs_image = res.buffer;
gs_rowbyte = res.ispng ? 1 : 0;
},
// Unconnected stroked lines are used to draw the bars in linear barcodes;
// and the border around a linear barcode (e.g. ITF-14)
// No line cap should be applied. These lines are always orthogonal.
line : function(x0, y0, x1, y1, lw, rgb) {
x0 = x0|0;
y0 = y0|0;
x1 = x1|0;
y1 = y1|0;
// Most linear barcodes, the line width will be integral. The exceptions
// are variable width barcodes (e.g. code39) and the postal 4-state codes.
lw = Math.round(lw) || 1;
if (y1 < y0) { var t = y0; y0 = y1; y1 = t; }
if (x1 < x0) { var t = x0; x0 = x1; x1 = t; }
gs_r = parseInt(rgb.substr(0,2), 16);
gs_g = parseInt(rgb.substr(2,2), 16);
gs_b = parseInt(rgb.substr(4,2), 16);
// Horizontal or vertical line?
var w2 = (lw/2)|0;
if (x0 == x1) {
// Vertical line
x0 = x0 - w2;
x1 = x1 + lw - w2 - 1;
} else {
// Horizontal line
y0 = y0 - w2;
y1 = y1 + lw - w2 - 1;
}
for (var y = y0; y <= y1; y++) {
for (var x = x0; x <= x1; x++) {
set(x, y, 255);
}
}
},
// Polygons are used to draw the connected regions in a 2d barcode.
// These will always be unstroked, filled, non-intersecting,
// orthogonal shapes.
// You will see a series of polygon() calls, followed by a fill().
polygon : function(pts) {
var npts = pts.length;
for (var j = npts-1, i = 0; i < npts; j = i++) {
if (pts[j][0] == pts[i][0]) {
// Vertical lines do not get their end points. End points
// are added by the horizontal line logic.
var xj = pts[j][0]|0; // i or j, doesn't matter
var yj = pts[j][1]|0;
var yi = pts[i][1]|0;
if (yj > yi) {
for (var y = yi+1; y < yj; y++) {
addPoint(xj, y);
}
} else {
for (var y = yj+1; y < yi; y++) {
addPoint(xj, y);
}
}
} else {
var xj = pts[j][0]|0;
var xi = pts[i][0]|0;
var yj = pts[j][1]|0; // i or j, doesn't matter
// Horizontal lines are tricky. As a rule, top lines get filled,
// bottom lines do not (similar to how left edges get filled and
// right edges do not).
//
// Where it gets complex is deciding whether the line actually
// adds edges. There are cases where a horizontal line does
// not add anything to the scanline plotting. And it doesn't
// actually matter whether the line is a top or bottom edge,
// the logic is the same.
//
// A left edge is added if the edge to its left is below.
// A right edge is added if the edge to its right is below.
if (xj < xi) {
var yl = pts[j == 0 ? npts-1 : j-1][1]; // left edge
var yr = pts[i == npts-1 ? 0 : i+1][1]; // right edge
if (yl > yj) {
addPoint(xj, yj);
}
if (yr > yj) {
addPoint(xi, yj);
}
} else {
var yl = pts[i == npts-1 ? 0 : i+1][1]; // left edge
var yr = pts[j == 0 ? npts-1 : j-1][1]; // right edge
if (yl > yj) {
addPoint(xi, yj);
}
if (yr > yj) {
addPoint(xj, yj);
}
}
}
}
},
// An unstroked, filled hexagon used by maxicode. You can choose to fill
// each individually, or wait for the final fill().
//
// The hexagon is drawn from the top, counter-clockwise.
//
// The X-coordinate for the top and bottom points on the hexagon is always
// .5 pixels. We draw our hexagons with a 2 pixel flat top.
//
// All other points of the polygon/hexagon are guaranteed to be integer values.
hexagon : function(pts, rgb) {
var x = pts[0][0]|0;
var y = pts[0][1]|0;
var qh = pts[1][1] - pts[0][1]; // height of triangle (quarter height)
var vh = pts[2][1] - pts[1][1] - 1; // height of vertical side
var xl = pts[2][0]; // left side
var xr = pts[4][0]; // right side
gs_r = parseInt(rgb.substr(0,2), 16);
gs_g = parseInt(rgb.substr(2,2), 16);
gs_b = parseInt(rgb.substr(4,2), 16);
fillSegment(x, x+1, y++);
for (var k = 1; k < qh; k++) {
fillSegment(x-2*k, x+1+2*k, y++);
}
for (var k = 0; k <= vh; k++) {
fillSegment(xl, xr, y++);
}
for (var k = qh-1; k >= 1; k--) {
fillSegment(x-2*k, x+1+2*k, y++);
}
fillSegment(x, x+1, y);
},
// An unstroked, filled ellipse. Used by dotcode and maxicode at present.
// maxicode issues pairs of ellipse calls (one cw, one ccw) followed by a fill()
// to create the bullseye rings. dotcode issues all of its ellipses then a
// fill().
ellipse : function(x, y, rx, ry, ccw) {
drawEllipse(x-rx, y-ry, x+rx, y+ry, ccw);
},
// PostScript's default fill rule is even-odd.
fill : function(rgb) {
gs_r = parseInt(rgb.substr(0,2), 16);
gs_g = parseInt(rgb.substr(2,2), 16);
gs_b = parseInt(rgb.substr(4,2), 16);
evenodd();
gs_xymap = [];
gs_xymap.min = Infinity;
},
// Draw text with optional inter-character spacing. `y` is the baseline.
// font is an object with properties { name, width, height, dx }
// width and height are the font cell size.
// dx is extra space requested between characters (usually zero).
text : function(x, y, str, rgb, font) {
x = x|0;
y = y|0;
gs_r = parseInt(rgb.substr(0,2), 16);
gs_g = parseInt(rgb.substr(2,2), 16);
gs_b = parseInt(rgb.substr(4,2), 16);
var fontid = FontLib.lookup(font.name);
var fwidth = font.width|0;
var fheight = font.height|0;
var dx = font.dx|0;
for (var k = 0; k < str.length; k++) {
var ch = str.charCodeAt(k);
var glyph = FontLib.getglyph(fontid, ch, fwidth, fheight);
var gt = y - glyph.top;
var gl = glyph.left;
var gw = glyph.width;
var gh = glyph.height;
var gb = glyph.bytes;
var go = glyph.offset; // offset into bytes
for (var i = 0; i < gw; i++) {
for (var j = 0; j < gh; j++) {
var a = gb[go + j * gw + i];
if (a) {
set(x+gl+i, gt+j, a);
}
}
}
x += glyph.advance + dx;
}
},
// Called after all drawing is complete.
end : function() {
},
};
// This code is specialized to deal with two types of RGBA buffers:
// - canvas style, which is true RGBA
// - PNG style, which has a one-byte "filter code" prefixing each row.
function set(x, y, a) {
// translate/rotate
x += gs_dx;
y += gs_dy;
var tx = tx0 * x + tx1 * y + tx2 * (gs_width-1) + tx3 * (gs_height-1);
var ty = ty0 * x + ty1 * y + ty2 * (gs_width-1) + ty3 * (gs_height-1);
// https://en.wikipedia.org/wiki/Alpha_compositing
var offs = (ty * gs_width + tx) * 4 + (ty+1) * gs_rowbyte;
var dsta = gs_image[offs+3] / 255;
var srca = a / 255;
var inva = (1 - srca) * dsta;
var outa = srca + inva;
gs_image[offs+0] = ((gs_r * srca + gs_image[offs+0] * inva) / outa)|0;
gs_image[offs+1] = ((gs_g * srca + gs_image[offs+1] * inva) / outa)|0;
gs_image[offs+2] = ((gs_b * srca + gs_image[offs+2] * inva) / outa)|0;
gs_image[offs+3] = (255 * outa)|0;
}
// Add a point on an edge to the scanline map.
function addPoint(x, y) {
if (gs_xymap.min > y) gs_xymap.min = y;
if (!gs_xymap[y]) {
gs_xymap[y] = [ x ];
} else {
gs_xymap[y].push(x);
}
}
function fillSegment(x0, x1, y) {
while (x0 <= x1) {
set(x0++, y, 255);
}
}
// even-odd fill
//
// This implementation is optimized for BWIPP's simple usage.
// It is not a general purpose scanline fill. It relies heavily on
// polygon() creating the correct intersections.
function evenodd() {
var ymin = gs_xymap.min;
var ymax = gs_xymap.length-1;
for (var y = ymin; y <= ymax; y++) {
var pts = gs_xymap[y];
if (!pts) {
continue
}
pts.sort(function(a, b) { return a - b; });
var wn = false;
var xl = 0;
for (var n = 0, npts = pts.length; n < npts; n++) {
var x = pts[n];
if (wn) {
fillSegment(xl, x-1, y);
} else {
xl = x;
}
wn = !wn;
}
}
}
function drawEllipse(x0, y0, x1, y1, dir) {
x0 = x0|0;
y0 = y0|0;
x1 = x1|0;
y1 = y1|0;
var a = Math.abs(x1-x0);
var b = Math.abs(y1-y0);
var b1 = b & 1;
var dx = 4*(1-a)*b*b;
var dy = 4*(b1+1)*a*a;
var err = dx + dy + b1*a*a;
var e2;
// Left and right edges
var left = [], right = [];
left.min = right.min = Infinity;
if (x0 > x1) { x0 = x1; x1 += a; }
if (y0 > y1) y0 = y1;
y0 += ((b+1)/2)|0;
y1 = y0 - b1;
a *= 8*a; b1 = 8*b*b;
do {
maxedge(right, x1, y0); // 1st quadrant
minedge(left, x0, y0); // 2nd quadrant
minedge(left, x0, y1); // 3rd quadrant
maxedge(right, x1, y1); // 4th quadrant
e2 = 2*err;
if (e2 >= dx) { x0++; x1--; dx += b1; err += dx; }
if (e2 <= dy) { y0++; y1--; dy += a; err += dy; }
} while (x0 <= x1);
while (y0-y1 < b) { // too early stop of flat ellipse
maxedge(right, x1+1, y0);
minedge(left, x0-1, y0++);
minedge(left, x0-1, y1);
maxedge(right, x1+1, y1--);
}
for (var y = left.min, max = left.length-1; y <= max; y++) {
addPoint(left[y], y);
}
// The points we calculated are "inside". The fill algorithm excludes
// right edges, so +1 on each x.
for (var y = right.min, max = right.length-1; y <= max; y++) {
addPoint(right[y]+1, y);
}
function minedge(e, x, y) {
if (e.min > y) e.min = y;
var ey = e[y];
if (ey == null || ey > x) {
e[y] = x;
}
}
function maxedge(e, x, y) {
if (e.min > y) e.min = y;
var ey = e[y];
if (ey == null || ey < x) {
e[y] = x;
}
}
}
// Returns 1 if clockwise, -1 if ccw.
function polydir(pts) {
var xp = 0;
for (var i = 0, l = pts.length, j = l-1; i < l; j = i++) {
xp += pts[j][0] * pts[i][1] - pts[i][0] * pts[j][1];
}
return xp > 0 ? 1 : -1;
}
}