@studious-lms/server 1.1.16 → 1.1.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"jsonConversion.d.ts","sourceRoot":"","sources":["../../src/lib/jsonConversion.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAsB,MAAM,iBAAiB,CAAA;AAEnE,wBAAsB,SAAS,CAAC,MAAM,EAAE,aAAa,EAAE,wCAohBtD"}
1
+ {"version":3,"file":"jsonConversion.d.ts","sourceRoot":"","sources":["../../src/lib/jsonConversion.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,aAAa,EAAsB,MAAM,iBAAiB,CAAA;AAEnE,wBAAsB,SAAS,CAAC,MAAM,EAAE,aAAa,EAAE,wCAouBtD"}
@@ -1,17 +1,41 @@
1
1
  import { PDFDocument, StandardFonts, rgb } from 'pdf-lib';
2
+ import fontkit from '@pdf-lib/fontkit';
3
+ import { readFileSync } from 'fs';
4
+ import { join } from 'path';
2
5
  import { FormatTypes, Fonts } from './jsonStyles.js';
3
6
  export async function createPdf(blocks) {
4
7
  console.log('createPdf: Starting PDF creation with', blocks.length, 'blocks');
5
8
  try {
6
9
  const pdfDoc = await PDFDocument.create();
7
10
  console.log('createPdf: PDFDocument created successfully');
8
- const timesRomanFont = await pdfDoc.embedFont(StandardFonts.TimesRoman);
9
- const courierFont = await pdfDoc.embedFont(StandardFonts.Courier);
10
- const helveticaFont = await pdfDoc.embedFont(StandardFonts.Helvetica);
11
- const helveticaBoldFont = await pdfDoc.embedFont(StandardFonts.HelveticaBold);
12
- const helveticaItalicFont = await pdfDoc.embedFont(StandardFonts.HelveticaOblique);
13
- const helveticaBoldItalicFont = await pdfDoc.embedFont(StandardFonts.HelveticaBoldOblique);
14
- const defaultFont = helveticaFont;
11
+ // Register fontkit to enable custom font embedding
12
+ pdfDoc.registerFontkit(fontkit);
13
+ // Load Unicode-compatible fonts (Noto Sans)
14
+ const fontDir = join(process.cwd(), 'src', 'lib');
15
+ let notoSansRegular;
16
+ let notoSansBold;
17
+ let notoSansItalic;
18
+ let courierFont;
19
+ try {
20
+ // Try to load custom Unicode fonts
21
+ const regularFontBytes = readFileSync(join(fontDir, 'NotoSans-Regular.ttf'));
22
+ const boldFontBytes = readFileSync(join(fontDir, 'NotoSans-Bold.ttf'));
23
+ const italicFontBytes = readFileSync(join(fontDir, 'NotoSans-Italic.ttf'));
24
+ notoSansRegular = await pdfDoc.embedFont(regularFontBytes);
25
+ notoSansBold = await pdfDoc.embedFont(boldFontBytes);
26
+ notoSansItalic = await pdfDoc.embedFont(italicFontBytes);
27
+ courierFont = await pdfDoc.embedFont(StandardFonts.Courier); // Keep Courier for code blocks
28
+ console.log('createPdf: Unicode fonts loaded successfully');
29
+ }
30
+ catch (fontError) {
31
+ console.warn('createPdf: Failed to load custom fonts, falling back to standard fonts:', fontError);
32
+ // Fallback to standard fonts if custom fonts fail
33
+ notoSansRegular = await pdfDoc.embedFont(StandardFonts.Helvetica);
34
+ notoSansBold = await pdfDoc.embedFont(StandardFonts.HelveticaBold);
35
+ notoSansItalic = await pdfDoc.embedFont(StandardFonts.HelveticaOblique);
36
+ courierFont = await pdfDoc.embedFont(StandardFonts.Courier);
37
+ }
38
+ const defaultFont = notoSansRegular;
15
39
  const defaultParagraphSpacing = 10;
16
40
  const defaultLineHeight = 1.3;
17
41
  const defaultFontSize = 12;
@@ -20,20 +44,20 @@ export async function createPdf(blocks) {
20
44
  const headingColor = rgb(0.1, 0.1, 0.1);
21
45
  const paragraphColor = rgb(0.15, 0.15, 0.15);
22
46
  const FONTS = {
23
- [Fonts.TIMES_ROMAN]: timesRomanFont,
47
+ [Fonts.TIMES_ROMAN]: notoSansRegular, // Use Noto Sans instead of Times
24
48
  [Fonts.COURIER]: courierFont,
25
- [Fonts.HELVETICA]: helveticaFont,
26
- [Fonts.HELVETICA_BOLD]: helveticaBoldFont,
27
- [Fonts.HELVETICA_ITALIC]: helveticaItalicFont,
28
- [Fonts.HELVETICA_BOLD_ITALIC]: helveticaBoldItalicFont,
49
+ [Fonts.HELVETICA]: notoSansRegular,
50
+ [Fonts.HELVETICA_BOLD]: notoSansBold,
51
+ [Fonts.HELVETICA_ITALIC]: notoSansItalic,
52
+ [Fonts.HELVETICA_BOLD_ITALIC]: notoSansBold, // Use bold for now, could add bold-italic later
29
53
  };
30
54
  const STYLE_PRESETS = {
31
- [FormatTypes.HEADER_1]: { fontSize: 28, lineHeight: 1.35, font: helveticaBoldFont, color: headingColor },
32
- [FormatTypes.HEADER_2]: { fontSize: 22, lineHeight: 1.35, font: helveticaBoldFont, color: headingColor },
33
- [FormatTypes.HEADER_3]: { fontSize: 18, lineHeight: 1.35, font: helveticaBoldFont, color: headingColor },
34
- [FormatTypes.HEADER_4]: { fontSize: 16, lineHeight: 1.3, font: helveticaBoldFont, color: headingColor },
35
- [FormatTypes.HEADER_5]: { fontSize: 14, lineHeight: 1.3, font: helveticaBoldFont, color: headingColor },
36
- [FormatTypes.HEADER_6]: { fontSize: 12, lineHeight: 1.3, font: helveticaBoldFont, color: headingColor },
55
+ [FormatTypes.HEADER_1]: { fontSize: 28, lineHeight: 1.35, font: notoSansBold, color: headingColor },
56
+ [FormatTypes.HEADER_2]: { fontSize: 22, lineHeight: 1.35, font: notoSansBold, color: headingColor },
57
+ [FormatTypes.HEADER_3]: { fontSize: 18, lineHeight: 1.35, font: notoSansBold, color: headingColor },
58
+ [FormatTypes.HEADER_4]: { fontSize: 16, lineHeight: 1.3, font: notoSansBold, color: headingColor },
59
+ [FormatTypes.HEADER_5]: { fontSize: 14, lineHeight: 1.3, font: notoSansBold, color: headingColor },
60
+ [FormatTypes.HEADER_6]: { fontSize: 12, lineHeight: 1.3, font: notoSansBold, color: headingColor },
37
61
  [FormatTypes.QUOTE]: { fontSize: 14, lineHeight: 1.5, color: rgb(0.35, 0.35, 0.35) },
38
62
  [FormatTypes.CODE_BLOCK]: { fontSize: 12, lineHeight: 1.6, font: courierFont, color: rgb(0.1, 0.1, 0.1), background: rgb(0.95, 0.95, 0.95) },
39
63
  [FormatTypes.PARAGRAPH]: { fontSize: 12, lineHeight: 1.3, color: paragraphColor },
@@ -68,26 +92,146 @@ export async function createPdf(blocks) {
68
92
  return color;
69
93
  }
70
94
  };
71
- // Function to replace Unicode characters that can't be encoded by WinAnsi
95
+ // Minimal sanitization - only remove truly problematic invisible characters
96
+ // With Unicode fonts, we can now keep most characters as-is
72
97
  const sanitizeText = (text) => {
73
98
  return text
74
- .replace(/→/g, '->') // Right arrow
75
- .replace(/←/g, '<-') // Left arrow
76
- .replace(/↑/g, '^') // Up arrow
77
- .replace(/↓/g, 'v') // Down arrow
78
- .replace(/•/g, '*') // Bullet point
79
- .replace(/–/g, '-') // En dash
80
- .replace(/—/g, '--') // Em dash
81
- .replace(/'/g, "'") // Left single quote
82
- .replace(/'/g, "'") // Right single quote
83
- .replace(/"/g, '"') // Left double quote
84
- .replace(/"/g, '"') // Right double quote
85
- .replace(/…/g, '...') // Ellipsis
86
- .replace(/°/g, ' degrees') // Degree symbol
87
- .replace(/±/g, '+/-') // Plus-minus
88
- .replace(/×/g, 'x') // Multiplication sign
89
- .replace(/÷/g, '/'); // Division sign
90
- // Add more replacements as needed
99
+ // Only remove invisible/control characters that break PDF generation
100
+ .replace(/\uFEFF/g, '') // Remove BOM (Byte Order Mark)
101
+ .replace(/\u200B/g, '') // Remove zero-width space
102
+ .replace(/\u200C/g, '') // Remove zero-width non-joiner
103
+ .replace(/\u200D/g, '') // Remove zero-width joiner
104
+ .replace(/\uFFFD/g, '?'); // Replace replacement character with ?
105
+ // Keep ALL visible Unicode characters - Noto Sans supports them!
106
+ };
107
+ const parseMarkdown = (text, baseFont, baseColor) => {
108
+ const segments = [];
109
+ let currentIndex = 0;
110
+ // Regex patterns for markdown
111
+ const patterns = [
112
+ { regex: /\*\*(.*?)\*\*/g, font: notoSansBold }, // **bold**
113
+ { regex: /__(.*?)__/g, font: notoSansBold }, // __bold__
114
+ { regex: /\*(.*?)\*/g, font: notoSansItalic }, // *italic*
115
+ { regex: /_(.*?)_/g, font: notoSansItalic }, // _italic_
116
+ { regex: /`(.*?)`/g, font: courierFont, color: rgb(0.2, 0.2, 0.2) }, // `code`
117
+ ];
118
+ // Find all markdown matches
119
+ const matches = [];
120
+ for (const pattern of patterns) {
121
+ let match;
122
+ pattern.regex.lastIndex = 0; // Reset regex
123
+ while ((match = pattern.regex.exec(text)) !== null) {
124
+ matches.push({
125
+ start: match.index,
126
+ end: match.index + match[0].length,
127
+ text: match[1], // Captured group (content without markdown syntax)
128
+ font: pattern.font,
129
+ color: pattern.color
130
+ });
131
+ }
132
+ }
133
+ // Sort matches by start position
134
+ matches.sort((a, b) => a.start - b.start);
135
+ // Remove overlapping matches (keep the first one)
136
+ const filteredMatches = [];
137
+ for (const match of matches) {
138
+ const hasOverlap = filteredMatches.some(existing => (match.start < existing.end && match.end > existing.start));
139
+ if (!hasOverlap) {
140
+ filteredMatches.push(match);
141
+ }
142
+ }
143
+ // Build segments
144
+ for (const match of filteredMatches) {
145
+ // Add text before this match
146
+ if (match.start > currentIndex) {
147
+ const beforeText = text.substring(currentIndex, match.start);
148
+ if (beforeText) {
149
+ segments.push({
150
+ text: sanitizeText(beforeText),
151
+ font: baseFont,
152
+ color: baseColor
153
+ });
154
+ }
155
+ }
156
+ // Add the styled match
157
+ segments.push({
158
+ text: sanitizeText(match.text),
159
+ font: match.font,
160
+ color: match.color || baseColor
161
+ });
162
+ currentIndex = match.end;
163
+ }
164
+ // Add remaining text
165
+ if (currentIndex < text.length) {
166
+ const remainingText = text.substring(currentIndex);
167
+ if (remainingText) {
168
+ segments.push({
169
+ text: sanitizeText(remainingText),
170
+ font: baseFont,
171
+ color: baseColor
172
+ });
173
+ }
174
+ }
175
+ // If no markdown was found, return the whole text as one segment
176
+ if (segments.length === 0) {
177
+ segments.push({
178
+ text: sanitizeText(text),
179
+ font: baseFont,
180
+ color: baseColor
181
+ });
182
+ }
183
+ return segments;
184
+ };
185
+ // Enhanced text wrapping that handles styled segments
186
+ const wrapStyledText = (segments, fontSize, maxWidth) => {
187
+ const lines = [];
188
+ let currentLine = [];
189
+ let currentWidth = 0;
190
+ for (let segmentIndex = 0; segmentIndex < segments.length; segmentIndex++) {
191
+ const segment = segments[segmentIndex];
192
+ const words = segment.text.split(/\s+/).filter(word => word.length > 0);
193
+ for (let i = 0; i < words.length; i++) {
194
+ const word = words[i];
195
+ const wordWidth = segment.font.widthOfTextAtSize(word, fontSize);
196
+ const spaceWidth = segment.font.widthOfTextAtSize(' ', fontSize);
197
+ // Add space before word if:
198
+ // 1. Not the first word in the line AND
199
+ // 2. (Not the first word in the segment OR not the first segment)
200
+ const needSpace = currentLine.length > 0 && (i > 0 || segmentIndex > 0);
201
+ const totalWidth = wordWidth + (needSpace ? spaceWidth : 0);
202
+ // Check if we need to wrap to new line
203
+ if (currentWidth + totalWidth > maxWidth && currentLine.length > 0) {
204
+ // Finish current line
205
+ lines.push({ segments: [...currentLine], width: currentWidth });
206
+ currentLine = [];
207
+ currentWidth = 0;
208
+ }
209
+ // Add the word to current line
210
+ if (needSpace && currentLine.length > 0) {
211
+ // Try to merge with last segment if same font and color
212
+ const lastSegment = currentLine[currentLine.length - 1];
213
+ if (lastSegment.font === segment.font && lastSegment.color === segment.color) {
214
+ lastSegment.text += ' ' + word;
215
+ currentWidth += spaceWidth + wordWidth;
216
+ }
217
+ else {
218
+ // Add space + word as new segment
219
+ currentLine.push({ text: ' ' + word, font: segment.font, color: segment.color });
220
+ currentWidth += spaceWidth + wordWidth;
221
+ }
222
+ }
223
+ else {
224
+ // Add word without space (first word in line or first word overall)
225
+ currentLine.push({ text: word, font: segment.font, color: segment.color });
226
+ currentWidth += wordWidth;
227
+ }
228
+ }
229
+ }
230
+ // Add final line if it has content
231
+ if (currentLine.length > 0) {
232
+ lines.push({ segments: currentLine, width: currentWidth });
233
+ }
234
+ return lines.length > 0 ? lines : [{ segments: [{ text: '', font: defaultFont, color: rgb(0, 0, 0) }], width: 0 }];
91
235
  };
92
236
  let page = pdfDoc.addPage();
93
237
  let { width, height } = page.getSize();
@@ -196,41 +340,51 @@ export async function createPdf(blocks) {
196
340
  }
197
341
  };
198
342
  const drawParagraph = (text) => {
199
- const sanitizedText = sanitizeText(text);
200
- const lines = wrapText(sanitizedText, font, fontSize, maxTextWidth());
343
+ const segments = parseMarkdown(text, font, color);
344
+ const lines = wrapStyledText(segments, fontSize, maxTextWidth());
201
345
  for (const line of lines) {
202
346
  ensureSpace(lineHeight);
203
- page.drawText(line, {
204
- x: marginLeft,
205
- y: y,
206
- size: fontSize,
207
- font: font,
208
- color: color,
209
- });
347
+ let currentX = marginLeft;
348
+ for (const segment of line.segments) {
349
+ if (segment.text.trim()) {
350
+ page.drawText(segment.text, {
351
+ x: currentX,
352
+ y: y,
353
+ size: fontSize,
354
+ font: segment.font,
355
+ color: segment.color,
356
+ });
357
+ currentX += segment.font.widthOfTextAtSize(segment.text, fontSize);
358
+ }
359
+ }
210
360
  y -= lineHeight;
211
361
  }
212
362
  };
213
363
  const drawHeading = (text, align) => {
214
- const sanitizedText = sanitizeText(text);
215
- const lines = wrapText(sanitizedText, font, fontSize, maxTextWidth());
364
+ const segments = parseMarkdown(text, font, color);
365
+ const lines = wrapStyledText(segments, fontSize, maxTextWidth());
216
366
  for (const line of lines) {
217
367
  ensureSpace(lineHeight);
218
- let x = marginLeft;
368
+ let startX = marginLeft;
219
369
  if (align === 'center') {
220
- const tw = font.widthOfTextAtSize(line, fontSize);
221
- x = marginLeft + (maxTextWidth() - tw) / 2;
370
+ startX = marginLeft + (maxTextWidth() - line.width) / 2;
222
371
  }
223
372
  else if (align === 'right') {
224
- const tw = font.widthOfTextAtSize(line, fontSize);
225
- x = marginLeft + maxTextWidth() - tw;
373
+ startX = marginLeft + maxTextWidth() - line.width;
374
+ }
375
+ let currentX = startX;
376
+ for (const segment of line.segments) {
377
+ if (segment.text.trim()) {
378
+ page.drawText(segment.text, {
379
+ x: currentX,
380
+ y: y,
381
+ size: fontSize,
382
+ font: segment.font,
383
+ color: segment.color,
384
+ });
385
+ currentX += segment.font.widthOfTextAtSize(segment.text, fontSize);
386
+ }
226
387
  }
227
- page.drawText(line, {
228
- x,
229
- y: y,
230
- size: fontSize,
231
- font: font,
232
- color: color,
233
- });
234
388
  y -= lineHeight;
235
389
  }
236
390
  };
@@ -239,36 +393,37 @@ export async function createPdf(blocks) {
239
393
  const gap = 8;
240
394
  const contentWidth = maxTextWidth() - (bulletIndent + gap);
241
395
  for (const item of items) {
242
- const sanitizedItem = sanitizeText(item);
243
- const lines = wrapText(sanitizedItem, font, fontSize, contentWidth);
244
- ensureSpace(lineHeight);
245
- // Bullet glyph (use ASCII bullet instead of Unicode)
246
- page.drawText('*', {
247
- x: marginLeft + gap,
248
- y: y,
249
- size: fontSize,
250
- font: font,
251
- color: color,
252
- });
253
- // First line
254
- page.drawText(lines[0], {
255
- x: marginLeft + bulletIndent + gap,
256
- y: y,
257
- size: fontSize,
258
- font: font,
259
- color: color,
260
- });
261
- y -= lineHeight;
262
- // Continuation lines with hanging indent
263
- for (let i = 1; i < lines.length; i++) {
396
+ // Clean up any bullet symbols that the AI might have added
397
+ const cleanItem = item.replace(/^\s*[•*-]\s*/, '').trim();
398
+ const segments = parseMarkdown(cleanItem, font, color);
399
+ const lines = wrapStyledText(segments, fontSize, contentWidth);
400
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
401
+ const line = lines[lineIndex];
264
402
  ensureSpace(lineHeight);
265
- page.drawText(lines[i], {
266
- x: marginLeft + bulletIndent + gap,
267
- y: y,
268
- size: fontSize,
269
- font: font,
270
- color: color,
271
- });
403
+ // Draw bullet only on first line
404
+ if (lineIndex === 0) {
405
+ page.drawText('•', {
406
+ x: marginLeft + gap,
407
+ y: y,
408
+ size: fontSize,
409
+ font: font,
410
+ color: color,
411
+ });
412
+ }
413
+ // Draw styled text
414
+ let currentX = marginLeft + bulletIndent + gap;
415
+ for (const segment of line.segments) {
416
+ if (segment.text.trim()) {
417
+ page.drawText(segment.text, {
418
+ x: currentX,
419
+ y: y,
420
+ size: fontSize,
421
+ font: segment.font,
422
+ color: segment.color,
423
+ });
424
+ currentX += segment.font.widthOfTextAtSize(segment.text, fontSize);
425
+ }
426
+ }
272
427
  y -= lineHeight;
273
428
  }
274
429
  }
@@ -279,35 +434,39 @@ export async function createPdf(blocks) {
279
434
  const contentWidth = maxTextWidth() - (numberIndent + gap);
280
435
  let index = 1;
281
436
  for (const item of items) {
437
+ // Clean up any numbers that the AI might have added
438
+ const cleanItem = item.replace(/^\s*\d+\.\s*/, '').trim();
282
439
  const numLabel = `${index}.`;
283
440
  const numWidth = font.widthOfTextAtSize(numLabel, fontSize);
284
- const sanitizedItem = sanitizeText(item);
285
- const lines = wrapText(sanitizedItem, font, fontSize, contentWidth);
286
- ensureSpace(lineHeight);
287
- page.drawText(numLabel, {
288
- x: marginLeft + gap,
289
- y: y,
290
- size: fontSize,
291
- font: font,
292
- color: color,
293
- });
294
- page.drawText(lines[0], {
295
- x: marginLeft + Math.max(numberIndent, numWidth + 6) + gap,
296
- y: y,
297
- size: fontSize,
298
- font: font,
299
- color: color,
300
- });
301
- y -= lineHeight;
302
- for (let i = 1; i < lines.length; i++) {
441
+ const segments = parseMarkdown(cleanItem, font, color);
442
+ const lines = wrapStyledText(segments, fontSize, contentWidth);
443
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
444
+ const line = lines[lineIndex];
303
445
  ensureSpace(lineHeight);
304
- page.drawText(lines[i], {
305
- x: marginLeft + Math.max(numberIndent, numWidth + 6) + gap,
306
- y: y,
307
- size: fontSize,
308
- font: font,
309
- color: color,
310
- });
446
+ // Draw number only on first line
447
+ if (lineIndex === 0) {
448
+ page.drawText(numLabel, {
449
+ x: marginLeft + gap,
450
+ y: y,
451
+ size: fontSize,
452
+ font: font,
453
+ color: color,
454
+ });
455
+ }
456
+ // Draw styled text
457
+ let currentX = marginLeft + Math.max(numberIndent, numWidth + 6) + gap;
458
+ for (const segment of line.segments) {
459
+ if (segment.text.trim()) {
460
+ page.drawText(segment.text, {
461
+ x: currentX,
462
+ y: y,
463
+ size: fontSize,
464
+ font: segment.font,
465
+ color: segment.color,
466
+ });
467
+ currentX += segment.font.widthOfTextAtSize(segment.text, fontSize);
468
+ }
469
+ }
311
470
  y -= lineHeight;
312
471
  }
313
472
  index++;
@@ -318,14 +477,14 @@ export async function createPdf(blocks) {
318
477
  const ruleGap = 8;
319
478
  const contentX = marginLeft + ruleWidth + ruleGap;
320
479
  const contentWidth = maxTextWidth() - (ruleWidth + ruleGap);
321
- const sanitizedText = sanitizeText(text);
322
- const lines = wrapText(sanitizedText, font, fontSize, contentWidth);
480
+ const segments = parseMarkdown(text, font, color);
481
+ const lines = wrapStyledText(segments, fontSize, contentWidth);
323
482
  const totalHeight = lines.length * lineHeight + fontSize;
324
483
  var remainingHeight = totalHeight;
325
484
  for (const line of lines) {
326
485
  let pageAdded = ensureSpace(lineHeight);
327
486
  if (pageAdded || remainingHeight == totalHeight) {
328
- let blockHeight = Math.floor(Math.min(remainingHeight, y - marginBottom) / lineHeight) * lineHeight; // Get remaining height on page
487
+ let blockHeight = Math.floor(Math.min(remainingHeight, y - marginBottom) / lineHeight) * lineHeight;
329
488
  page.drawRectangle({
330
489
  x: marginLeft,
331
490
  y: y + lineHeight,
@@ -335,13 +494,20 @@ export async function createPdf(blocks) {
335
494
  });
336
495
  remainingHeight -= blockHeight + lineHeight - fontSize;
337
496
  }
338
- page.drawText(line, {
339
- x: contentX,
340
- y: y,
341
- size: fontSize,
342
- font: font,
343
- color: color,
344
- });
497
+ // Draw styled text
498
+ let currentX = contentX;
499
+ for (const segment of line.segments) {
500
+ if (segment.text.trim()) {
501
+ page.drawText(segment.text, {
502
+ x: currentX,
503
+ y: y,
504
+ size: fontSize,
505
+ font: segment.font,
506
+ color: segment.color,
507
+ });
508
+ currentX += segment.font.widthOfTextAtSize(segment.text, fontSize);
509
+ }
510
+ }
345
511
  y -= lineHeight;
346
512
  }
347
513
  y -= lineHeight - fontSize;