diff --git a/out.pdf b/out.pdf index b8a9254..abb1fda 100644 Binary files a/out.pdf and b/out.pdf differ diff --git a/src/generators/GlyphGenerator.js b/src/generators/GlyphGenerator.js index 6bc33d0..96011d2 100644 --- a/src/generators/GlyphGenerator.js +++ b/src/generators/GlyphGenerator.js @@ -53,7 +53,7 @@ export default class GlyphGenerator { return res; }); - return new GlyphString(attributedString.string, glyphRuns, 0, attributedString.string.length); + return new GlyphString(attributedString.string, glyphRuns); } resolveRuns(attributedString) { diff --git a/src/layout/LayoutEngine.js b/src/layout/LayoutEngine.js index d18d426..95d238d 100644 --- a/src/layout/LayoutEngine.js +++ b/src/layout/LayoutEngine.js @@ -97,9 +97,10 @@ export default class LayoutEngine { const fragments = []; while (lineRect.y < rect.maxY && pos < glyphString.length && lines < maxLines) { + const lineString = glyphString.slice(pos, glyphString.length); const lineFragments = this.typesetter.layoutLineFragments( lineRect, - glyphString.slice(pos, glyphString.length), + lineString, container, paragraphStyle ); @@ -108,7 +109,7 @@ export default class LayoutEngine { if (lineFragments.length > 0) { fragments.push(...lineFragments); - pos += lineFragments[lineFragments.length - 1].end; + pos = lineFragments[lineFragments.length - 1].end; lines++; if (firstLine) { diff --git a/src/layout/LineBreaker.js b/src/layout/LineBreaker.js index dea83b2..6ba5f5f 100644 --- a/src/layout/LineBreaker.js +++ b/src/layout/LineBreaker.js @@ -21,39 +21,38 @@ export default class LineBreaker { } let stringIndex = glyphString.stringIndexForGlyphIndex(glyphIndex); - const bk = this.findBreakPreceeding(glyphString.string, stringIndex); - // if (bk) { - // let breakIndex = glyphString.glyphIndexForStringIndex(bk.position); - // - // if ( - // bk.next != null && - // this.shouldHyphenate(glyphString, breakIndex, width, hyphenationFactor) - // ) { - // const lineWidth = glyphString.offsetAtGlyphIndex(glyphIndex); - // const shrunk = lineWidth + lineWidth * SHRINK_FACTOR; - // - // const shrunkIndex = glyphString.glyphIndexAtOffset(shrunk); - // stringIndex = Math.min(bk.next, glyphString.stringIndexForGlyphIndex(shrunkIndex)); - // - // const point = this.findHyphenationPoint( - // glyphString.string.slice(bk.position, bk.next), - // stringIndex - bk.position - // ); - // - // if (point > 0) { - // bk.position += point; - // breakIndex = glyphString.glyphIndexForStringIndex(bk.position); - // - // if (bk.position < bk.next) { - // glyphString.insertGlyph(breakIndex++, HYPHEN); - // } - // } - // } - // - // bk.position = breakIndex; - // } + if (bk) { + let breakIndex = glyphString.glyphIndexForStringIndex(bk.position); + + if ( + bk.next != null && + this.shouldHyphenate(glyphString, breakIndex, width, hyphenationFactor) + ) { + const lineWidth = glyphString.offsetAtGlyphIndex(glyphIndex); + const shrunk = lineWidth + lineWidth * SHRINK_FACTOR; + + const shrunkIndex = glyphString.glyphIndexAtOffset(shrunk); + stringIndex = Math.min(bk.next, glyphString.stringIndexForGlyphIndex(shrunkIndex)); + + const point = this.findHyphenationPoint( + glyphString.string.slice(bk.position, bk.next), + stringIndex - bk.position + ); + + if (point > 0) { + bk.position += point; + breakIndex = glyphString.glyphIndexForStringIndex(bk.position); + + if (bk.position < bk.next) { + glyphString.insertGlyph(breakIndex++, HYPHEN); + } + } + } + + bk.position = breakIndex; + } return bk; } diff --git a/src/models/GlyphRun.js b/src/models/GlyphRun.js index e5585df..d897ee2 100644 --- a/src/models/GlyphRun.js +++ b/src/models/GlyphRun.js @@ -7,13 +7,7 @@ class GlyphRun extends Run { this.positions = positions; this.stringIndices = stringIndices; this.scale = attributes.fontSize / attributes.font.unitsPerEm; - this.stringStart = Math.min(...stringIndices); - this.stringEnd = Math.max(...stringIndices); - this.glyphIndices = []; - - for (let i = 0; i < stringIndices.length; i++) { - this.glyphIndices[stringIndices[i]] = i; - } + this._glyphIndices = null; if (!preScaled) { for (const pos of this.positions) { @@ -26,7 +20,39 @@ class GlyphRun extends Run { } get length() { - return this.glyphs.length; + return this.end - this.start; + } + + get glyphIndices() { + if (this._glyphIndices) { + return this._glyphIndices; + } + + const glyphIndices = []; + + for (let i = 0; i < this.stringIndices.length; i++) { + glyphIndices[this.stringIndices[i]] = i; + } + + let lastValue = 0; + for (let i = glyphIndices.length - 1; i >= 0; i--) { + if (glyphIndices[i] === undefined) { + glyphIndices[i] = lastValue; + } else { + lastValue = glyphIndices[i]; + } + } + + this._glyphIndices = glyphIndices; + return glyphIndices; + } + + get stringStart() { + return Math.min(...this.stringIndices); + } + + get stringEnd() { + return Math.max(...this.stringIndices); } get advanceWidth() { @@ -61,15 +87,15 @@ class GlyphRun extends Run { } slice(start, end) { + const glyphs = this.glyphs.slice(start, end); + const positions = this.positions.slice(start, end); + let stringIndices = this.stringIndices.slice(start, end); + + stringIndices = stringIndices.map(index => index - this.stringIndices[start]); + start += this.start; end += this.start; - end = Math.min(end, this.start + this.glyphs.length); - - const glyphs = this.glyphs.slice(start - this.start, end - this.start); - const positions = this.positions.slice(start - this.start, end - this.start); - const stringIndices = this.stringIndices - .slice(start - this.start, end - this.start) - .map(index => index - this.stringIndices[start - this.start]); + end = Math.min(end, this.end); return new GlyphRun(start, end, this.attributes, glyphs, positions, stringIndices, true); } diff --git a/src/models/GlyphString.js b/src/models/GlyphString.js index 5f7a6ad..83c4e76 100644 --- a/src/models/GlyphString.js +++ b/src/models/GlyphString.js @@ -22,7 +22,7 @@ const HANGING_PUNCTUATION_END_CODEPOINTS = new Set([ class GlyphString { constructor(string, glyphRuns, start, end) { - this._string = string; + this.string = string; this._glyphRuns = glyphRuns; this.start = start || 0; this._end = end; @@ -30,16 +30,14 @@ class GlyphString { this._glyphRunsCacheEnd = null; } - get string() { - return this._string.slice(this.start, this.end); - } - get end() { - if (this._end != null) { - return this._end; + const glyphEnd = this._glyphRuns[this._glyphRuns.length - 1].end; + + if (this._end) { + return Math.min(this._end, glyphEnd); } - return this._glyphRuns.length > 0 ? this._glyphRuns[this._glyphRuns.length - 1].end : 0; + return this._glyphRuns.length > 0 ? glyphEnd : 0; } get length() { @@ -89,39 +87,56 @@ class GlyphString { } slice(start, end) { - return new GlyphString(this._string, this._glyphRuns, start + this.start, end + this.start); + const stringStart = this.stringIndexForGlyphIndex(start); + const stringEnd = this.stringIndexForGlyphIndex(end - 1); + + return new GlyphString( + this.string.slice(stringStart, stringEnd + 1), + this._glyphRuns, + start + this.start, + end + this.start + ); } runIndexAtGlyphIndex(index) { - const idx = index + this.start; + index += this.start; + let count = 0; for (let i = 0; i < this._glyphRuns.length; i++) { - if (this._glyphRuns[i].start <= idx && idx < this._glyphRuns[i].end) { + const run = this._glyphRuns[i]; + + if (count <= index && index < count + run.glyphs.length) { return i; } + + count += run.glyphs.length; } return this._glyphRuns.length - 1; } runAtGlyphIndex(index) { - return this._glyphRuns[this.runIndexAtGlyphIndex(index)]; + return this.glyphRuns[this.runIndexAtGlyphIndex(index)]; } runIndexAtStringIndex(index) { - const idx = index + this._glyphRuns[0].stringStart + this.start; + let offset = 0; - for (let i = 0; i < this._glyphRuns.length; i++) { - if (this._glyphRuns[i].stringStart <= idx && idx < this._glyphRuns[i].stringEnd + 1) { + for (let i = 0; i < this.glyphRuns.length; i++) { + const run = this.glyphRuns[i]; + + if (offset + run.stringStart <= index && offset + run.stringEnd >= index) { return i; } + + offset += run.stringEnd; } return this._glyphRuns.length - 1; } runAtStringIndex(index) { - return this._glyphRuns[this.runIndexAtStringIndex(index)]; + return this.glyphRuns[this.runIndexAtStringIndex(index)]; } glyphAtIndex(index) { @@ -129,9 +144,25 @@ class GlyphString { return run.glyphs[this.start + index - run.start]; } + positionAtIndex(index) { + let run; + let count = 0; + + for (let i = 0; i < this.glyphRuns.length; i++) { + run = this.glyphRuns[i]; + + if (count <= index && index < count + run.positions.length) { + return run.positions[index - count]; + } + + count += run.positions.length; + } + + return run.positions[run.positions.length - 1]; + } + getGlyphWidth(index) { - const run = this.runAtGlyphIndex(index); - return run.positions[this.start + index - run.start].xAdvance; + return this.positionAtIndex(index).xAdvance; } glyphIndexAtOffset(width) { @@ -158,19 +189,42 @@ class GlyphString { return index; } - stringIndexForGlyphIndex(glyphIndex) { - const run = this.runAtGlyphIndex(glyphIndex); + stringIndexForGlyphIndex(index) { + let run; + let count = 0; + let offset = 0; - if (glyphIndex >= run.end) { - return -1; + for (let i = 0; i < this.glyphRuns.length; i++) { + run = this.glyphRuns[i]; + + if (offset <= index && offset + run.length > index) { + return count + run.stringIndices[index + this.start - run.start]; + } + + offset += run.length; + count += run.stringEnd + 1; } - return run.start - this.start + run.stringIndices[this.start + glyphIndex - run.start]; + return count + run.stringIndices[run.stringIndices.length - 1]; } - glyphIndexForStringIndex(stringIndex) { - const run = this.runAtStringIndex(stringIndex); - return run.start - this.start + run.glyphIndices[this.start + stringIndex - run.start]; + glyphIndexForStringIndex(index) { + let run; + let count = 0; + let offset = 0; + + for (let i = 0; i < this.glyphRuns.length; i++) { + run = this.glyphRuns[i]; + + if (offset <= index && index < offset + run.stringEnd + 1) { + return count + run.glyphIndices[index - offset]; + } + + count += run.glyphs.length; + offset += run.stringEnd + 1; + } + + return offset; } codePointAtGlyphIndex(glyphIndex) { @@ -183,15 +237,16 @@ class GlyphString { offsetAtGlyphIndex(glyphIndex) { let offset = 0; + let count = glyphIndex; for (const run of this.glyphRuns) { for (let i = 0; i < run.glyphs.length; i++) { - if (glyphIndex === 0) { + if (count === 0) { return offset; } offset += run.positions[i].xAdvance; - glyphIndex--; + count -= 1; } } @@ -234,25 +289,26 @@ class GlyphString { const runIndex = this.runIndexAtGlyphIndex(index); const run = this._glyphRuns[runIndex]; const glyph = run.attributes.font.glyphForCodePoint(codePoint); + const idx = this.start + index - run.start; + + if (this._end) { + this._end += 1; + } - glyph.inserted = true; // TODO: don't do this - run.glyphs.splice(this.start + index - run.start, 0, glyph); - run.positions.splice(this.start + index - run.start, 0, { + run.glyphs.splice(idx, 0, glyph); + run.stringIndices.splice(idx, 0, run.stringIndices[idx]); + run.positions.splice(idx, 0, { xAdvance: glyph.advanceWidth, yAdvance: 0, xOffset: 0, yOffset: 0 }); - run.end++; + run.end += 1; for (let i = runIndex + 1; i < this._glyphRuns.length; i++) { - this._glyphRuns[i].start++; - this._glyphRuns[i].end++; - } - - if (this._end != null) { - this._end++; + this._glyphRuns[i].start += 1; + this._glyphRuns[i].end += 1; } this._glyphRunsCache = null; @@ -279,10 +335,6 @@ class GlyphString { this._glyphRuns[i].end--; } - if (this._end != null) { - this._end--; - } - this._glyphRunsCache = null; } } diff --git a/src/models/LineFragment.js b/src/models/LineFragment.js index 31c5ffa..dd7520c 100644 --- a/src/models/LineFragment.js +++ b/src/models/LineFragment.js @@ -2,12 +2,7 @@ import GlyphString from './GlyphString'; export default class LineFragment extends GlyphString { constructor(rect, glyphString) { - super( - glyphString.string, - glyphString.glyphRuns, - glyphString.start, - glyphString.end - ); + super(glyphString.string, glyphString._glyphRuns, glyphString.start, glyphString.end); this.rect = rect; this.decorationLines = []; this.overflowLeft = 0; diff --git a/temp.js b/temp.js index 7d34fab..b389956 100644 --- a/temp.js +++ b/temp.js @@ -2,6 +2,7 @@ import fs from 'fs'; import PDFDocument from 'pdfkit'; import Path from './src/geom/Path'; import LayoutEngine from './src/layout/LayoutEngine'; +import Run from './src/models/Run'; import AttributedString from './src/models/AttributedString'; import Container from './src/models/Container'; import TextRenderer from './src/renderers/TextRenderer'; @@ -11,7 +12,7 @@ const path = new Path(); path.rect(0, 0, 300, 400); const exclusion = new Path(); -exclusion.circle(200, 200, 50); +exclusion.circle(135, 30, 20); const doc = new PDFDocument(); doc.pipe(fs.createWriteStream('out.pdf')); @@ -30,8 +31,8 @@ const string = AttributedString.fromFragments([ fontSize: 14, bold: true, // align: 'justify', - // hyphenationFactor: 0.9, - // hangingPunctuation: true, + hyphenationFactor: 0.9, + hangingPunctuation: true, lineSpacing: 5, underline: true, underlineStyle: 'wavy', @@ -44,19 +45,25 @@ const string = AttributedString.fromFragments([ attributes: { font: 'Arial', fontSize: 14, color: 'red' } }, { - string: 'sed do eiusmod tempor incididunt ut labore', + string: + 'sed 🎉 do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea volupt\u0301ate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?”', attributes: { font: 'Arial', fontSize: 14, // align: 'justify', - // hyphenationFactor: 0.9, - // hangingPunctuation: true, + hyphenationFactor: 0.9, + hangingPunctuation: true, lineSpacing: 5, truncate: true } } ]); +// const string = new AttributedString('ខ្ញុំអាចញ៉ាំកញ្ចក់បាន',[ +// new Run(0, 8, { font: 'Khmer', color: 'red' }), +// new Run(8, 21, { font: 'Khmer', color: 'red' }), +// ]) + const l = new LayoutEngine(); const container = new Container(path, { exclusionPaths: [exclusion] diff --git a/test/data/Khmer.ttf b/test/data/Khmer.ttf new file mode 100644 index 0000000..e127df4 Binary files /dev/null and b/test/data/Khmer.ttf differ diff --git a/test/models/GlyphRun.test.js b/test/models/GlyphRun.test.js index cc6e912..42eabb2 100644 --- a/test/models/GlyphRun.test.js +++ b/test/models/GlyphRun.test.js @@ -3,100 +3,274 @@ import fontkit from 'fontkit'; import Attachment from '../../src/models/Attachment'; import { glyphRunFactory } from '../utils/glyphRuns'; -const font = fontkit.openSync(path.resolve(__dirname, '../data/OpenSans-Regular.ttf')); +const khmer = fontkit.openSync(path.resolve(__dirname, '../data/Khmer.ttf')); +const openSans = fontkit.openSync(path.resolve(__dirname, '../data/OpenSans-Regular.ttf')); -const createRun = glyphRunFactory(font); +const createKhmerRun = glyphRunFactory(khmer); +const createLatinRun = glyphRunFactory(openSans); + +const round = num => Math.round(num * 100) / 100; describe('GlyphRun', () => { test('should get correct length', () => { - const { glyphRun, glyphs } = createRun(); + const { glyphRun, glyphs } = createLatinRun(); expect(glyphRun.length).toBe(glyphs.length); }); + test('should get correct start', () => { + const { glyphRun } = createLatinRun(); + + expect(glyphRun.start).toBe(0); + }); + + test('should get correct start (non latin)', () => { + const { glyphRun } = createKhmerRun({ value: 'ខ្ញុំអាចញ៉ាំកញ្ចក់បាន', start: 0 }); + + expect(glyphRun.start).toBe(0); + }); + + test('should get correct end', () => { + const { glyphRun } = createLatinRun(); + + expect(glyphRun.end).toBe(11); + }); + + test('should get correct end', () => { + const { glyphRun } = createKhmerRun({ value: 'ខ្ញុំអាចញ៉ាំកញ្ចក់បាន', start: 0 }); + + expect(glyphRun.end).toBe(16); + }); + test('should get ascent correctly when no attachments', () => { - const { glyphRun, attrs } = createRun(); - const scale = attrs.fontSize / font.unitsPerEm; + const { glyphRun, attrs } = createLatinRun(); + const scale = attrs.fontSize / openSans.unitsPerEm; - expect(glyphRun.ascent).toBe(font.ascent * scale); + expect(glyphRun.ascent).toBe(openSans.ascent * scale); }); test('should get ascent correctly when higher attachments', () => { const attachment = new Attachment(20, 50); - const { glyphRun } = createRun({ attributes: { attachment } }); + const { glyphRun } = createLatinRun({ attributes: { attachment } }); expect(glyphRun.ascent).toBe(50); }); test('should get ascent correctly when lower attachments', () => { const attachment = new Attachment(20, 10); - const { glyphRun, attrs } = createRun({ attributes: { attachment } }); - const scale = attrs.fontSize / font.unitsPerEm; + const { glyphRun, attrs } = createLatinRun({ attributes: { attachment } }); + const scale = attrs.fontSize / openSans.unitsPerEm; - expect(glyphRun.ascent).toBe(font.ascent * scale); + expect(glyphRun.ascent).toBe(openSans.ascent * scale); }); test('should get descent correctly when no attachments', () => { const fontSize = 20; - const { glyphRun } = createRun({ attributes: { fontSize } }); - const scale = fontSize / font.unitsPerEm; + const { glyphRun } = createLatinRun({ attributes: { fontSize } }); + const scale = fontSize / openSans.unitsPerEm; - expect(glyphRun.descent).toBe(font.descent * scale); + expect(glyphRun.descent).toBe(openSans.descent * scale); }); test('should get descent correctly when no attachments', () => { const fontSize = 20; - const { glyphRun } = createRun({ attributes: { fontSize } }); - const scale = fontSize / font.unitsPerEm; + const { glyphRun } = createLatinRun({ attributes: { fontSize } }); + const scale = fontSize / openSans.unitsPerEm; - expect(glyphRun.descent).toBe(font.descent * scale); + expect(glyphRun.descent).toBe(openSans.descent * scale); }); test('should get lineGap correctly when no attachments', () => { const fontSize = 20; - const { glyphRun } = createRun({ attributes: { fontSize } }); - const scale = fontSize / font.unitsPerEm; + const { glyphRun } = createLatinRun({ attributes: { fontSize } }); + const scale = fontSize / openSans.unitsPerEm; - expect(glyphRun.lineGap).toBe(font.lineGap * scale); + expect(glyphRun.lineGap).toBe(openSans.lineGap * scale); }); test('should get height correctly when no attachments', () => { const fontSize = 20; - const { glyphRun } = createRun({ attributes: { fontSize } }); - const scale = fontSize / font.unitsPerEm; + const { glyphRun } = createLatinRun({ attributes: { fontSize } }); + const scale = fontSize / openSans.unitsPerEm; - const expectedHeight = (font.ascent - font.descent + font.lineGap) * scale; + const expectedHeight = (openSans.ascent - openSans.descent + openSans.lineGap) * scale; expect(glyphRun.height).toBe(expectedHeight); }); + test('should exact slice range return same run', () => { + const { glyphRun } = createLatinRun({ start: 0 }); + const sliced = glyphRun.slice(0, 11); + + expect(sliced.start).toBe(0); + expect(sliced.end).toBe(11); + }); + test('should slice containing range', () => { - const { glyphRun } = createRun(); + const { glyphRun } = createLatinRun({ start: 0 }); const sliced = glyphRun.slice(2, 5); - const getId = g => g.id; - const expectedGlyphs = glyphRun.glyphs.slice(2, 5); - const expectedPositions = glyphRun.positions.slice(2, 5); - - expect(sliced.end).toBe(5); expect(sliced.start).toBe(2); - expect(sliced.stringIndices[0]).toEqual(0); - expect(sliced.positions).toEqual(expectedPositions); - expect(sliced.glyphs.map(getId)).toEqual(expectedGlyphs.map(getId)); + expect(sliced.end).toBe(5); }); test('should slice exceeding range', () => { - const { glyphRun } = createRun(); + const { glyphRun } = createLatinRun({ start: 0 }); const sliced = glyphRun.slice(2, 20); - const getId = g => g.id; - const expectedGlyphs = glyphRun.glyphs.slice(2); - const expectedPositions = glyphRun.positions.slice(2); - expect(sliced.start).toBe(2); expect(sliced.end).toBe(11); - expect(sliced.end).toBe(glyphRun.end); - expect(sliced.stringIndices[0]).toEqual(0); - expect(sliced.glyphs.map(getId)).toEqual(expectedGlyphs.map(getId)); - expect(sliced.positions).toEqual(expectedPositions); + }); + + test('should slice containing range when start not zero', () => { + const { glyphRun } = createLatinRun({ start: 5 }); + const sliced = glyphRun.slice(2, 5); + + expect(sliced.start).toBe(7); + expect(sliced.end).toBe(10); + }); + + test('should slice exceeding range when start not zero', () => { + const { glyphRun } = createLatinRun({ start: 5 }); + const sliced = glyphRun.slice(2, 20); + + expect(sliced.start).toBe(7); + expect(sliced.end).toBe(11); + }); + + test('should correctly slice glyphs', () => { + // 47 82 85 72 80 3 918 83 86 88 80 + // l o r e m i p s u m + const { glyphRun } = createLatinRun({ start: 0 }); + const { glyphs } = glyphRun.slice(2, 8); + + expect(glyphs.map(g => g.id)).toEqual([85, 72, 80, 3, 918, 83]); + }); + + test('should correctly slice glyphs (non latin)', () => { + const { glyphRun } = createKhmerRun({ value: 'ខ្ញុំអាចញ៉ាំកញ្ចក់បាន', start: 0 }); + const { glyphs } = glyphRun.slice(1, 8); + + expect(glyphs.map(g => g.id)).toEqual([153, 177, 112, 248, 188, 49, 296]); + }); + + test('should exact slice return same glyphs', () => { + // 47 82 85 72 80 3 918 83 86 88 80 + // l o r e m i p s u m + const { glyphRun } = createLatinRun({ start: 0 }); + const { glyphs } = glyphRun.slice(0, 11); + + expect(glyphs.map(g => g.id)).toEqual([47, 82, 85, 72, 80, 3, 918, 83, 86, 88, 80]); + }); + + test('should exact slice return same glyphs (non latin)', () => { + const { glyphRun } = createKhmerRun({ value: 'ខ្ញុំអាចញ៉ាំកញ្ចក់បាន', start: 0 }); + const { glyphs } = glyphRun.slice(0, 21); + + expect(glyphs.map(g => g.id)).toEqual([ + 45, + 153, + 177, + 112, + 248, + 188, + 49, + 296, + 44, + 187, + 149, + 44, + 117, + 236, + 188, + 63 + ]); + }); + + test('should correctly slice positions', () => { + // 6.23 7.25 4.66 6.73 11.16 3.12 3.35 7.35 5.72 7.37 11.16 + // l o r e m i p s u m + const { glyphRun } = createLatinRun({ start: 0 }); + const sliced = glyphRun.slice(2, 8); + const positions = sliced.positions.map(p => round(p.xAdvance)); + + expect(positions).toEqual([4.66, 6.73, 11.16, 3.12, 3.35, 7.35]); + }); + + test('should correctly slice positions (non latin)', () => { + const { glyphRun } = createKhmerRun({ value: 'ខ្ញុំអាចញ៉ាំកញ្ចក់បាន', start: 0 }); + const sliced = glyphRun.slice(1, 8); + const positions = sliced.positions.map(p => round(p.xAdvance)); + + expect(positions).toEqual([0, 0, 0, 9.08, 4.54, 9.08, 18.16]); + }); + + test('should exact slice return same positions', () => { + // 6.23 7.25 4.66 6.73 11.16 3.12 3.35 7.35 5.72 7.37 11.16 + // l o r e m i p s u m + const { glyphRun } = createLatinRun({ start: 0 }); + const sliced = glyphRun.slice(0, 11); + const positions = sliced.positions.map(p => round(p.xAdvance)); + + expect(positions).toEqual([6.23, 7.25, 4.66, 6.73, 11.16, 3.12, 3.35, 7.35, 5.72, 7.37, 11.16]); + }); + + test('should exact slice return same positions (non latin)', () => { + const { glyphRun } = createKhmerRun({ value: 'ខ្ញុំអាចញ៉ាំកញ្ចក់បាន', start: 0 }); + const sliced = glyphRun.slice(0, 21); + const positions = sliced.positions.map(p => round(p.xAdvance)); + + expect(positions).toEqual([ + 9.08, + 0, + 0, + 0, + 9.08, + 4.54, + 9.08, + 18.16, + 9.08, + 13.62, + 0, + 9.08, + 0, + 9.08, + 4.54, + 9.08 + ]); + }); + + test('should correctly slice string indices', () => { + const { glyphRun } = createLatinRun({ start: 0 }); + const { stringIndices } = glyphRun.slice(2, 8); + + expect(stringIndices).toEqual([0, 1, 2, 3, 4, 5]); + }); + + test('should correctly slice string indices (non latin)', () => { + const { glyphRun } = createKhmerRun({ value: 'ខ្ញុំអាចញ៉ាំកញ្ចក់បាន', start: 0 }); + const { stringIndices } = glyphRun.slice(1, 8); + + expect(stringIndices).toEqual([0, 2, 3, 4, 5, 6, 7]); + }); + + test('should exact slice return same string indices', () => { + const { glyphRun } = createLatinRun({ start: 0 }); + const { stringIndices } = glyphRun.slice(0, 11); + + expect(stringIndices).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + }); + + test('should exact slice return same string indices (non latin)', () => { + const { glyphRun } = createKhmerRun({ value: 'ខ្ញុំអាចញ៉ាំកញ្ចក់បាន', start: 0 }); + const { stringIndices } = glyphRun.slice(0, 21); + + expect(stringIndices).toEqual([0, 1, 3, 4, 5, 6, 7, 8, 12, 13, 14, 16, 17, 18, 19, 20]); + }); + + test('should correctly slice glyph indices', () => { + const { glyphRun } = createLatinRun({ start: 0 }); + const { glyphIndices } = glyphRun.slice(2, 8); + + expect(glyphIndices).toEqual([0, 1, 2, 3, 4, 5]); }); }); diff --git a/test/models/GlyphString.test.js b/test/models/GlyphString.test.js index 2a53717..71bbac8 100644 --- a/test/models/GlyphString.test.js +++ b/test/models/GlyphString.test.js @@ -2,87 +2,157 @@ import path from 'path'; import fontkit from 'fontkit'; import { glyphStringFactory } from '../utils/glyphStrings'; -const font = fontkit.openSync(path.resolve(__dirname, '../data/OpenSans-Regular.ttf')); +const openSans = fontkit.openSync(path.resolve(__dirname, '../data/OpenSans-Regular.ttf')); +const khmer = fontkit.openSync(path.resolve(__dirname, '../data/Khmer.ttf')); -const createString = glyphStringFactory(font); +const createLatinString = glyphStringFactory(openSans); +const createKhmerString = glyphStringFactory(khmer); describe('GlyphString', () => { test('should get string value', () => { - const string = createString({ value: 'Lorem ipsum' }); + const string = createLatinString({ value: 'Lorem ipsum' }); expect(string.string).toBe('Lorem ipsum'); }); test('should get string end', () => { - const string = createString({ value: 'Lorem ipsum' }); + const string = createLatinString({ value: 'Lorem ipsum' }); expect(string.end).toBe(11); }); + test('should get string end (non latin)', () => { + const string = createKhmerString({ value: 'ខ្ញុំអាចញ៉ាំកញ្ចក់បាន' }); + + expect(string.end).toBe(16); + }); + test('should get string length', () => { - const string = createString({ value: 'Lorem ipsum' }); + const string = createLatinString({ value: 'Lorem ipsum' }); expect(string.length).toBe(11); }); + test('should get string length (non latin)', () => { + const string = createKhmerString({ value: 'ខ្ញុំអាចញ៉ាំកញ្ចក់បាន' }); + + expect(string.length).toBe(16); + }); + test('should get string advance width', () => { - const string = createString({ value: 'Lorem ipsum' }); + const string = createLatinString({ value: 'Lorem ipsum' }); expect(string.advanceWidth).toBeCloseTo(74, 0); }); test('should get string height', () => { - const string = createString({ value: 'Lorem ipsum' }); + const string = createLatinString({ value: 'Lorem ipsum' }); expect(string.height).toBeCloseTo(16, 0); }); test('should get string ascent', () => { - const string = createString({ value: 'Lorem ipsum' }); + const string = createLatinString({ value: 'Lorem ipsum' }); expect(string.ascent).toBeCloseTo(13, 0); }); test('should get string descent', () => { - const string = createString({ value: 'Lorem ipsum' }); + const string = createLatinString({ value: 'Lorem ipsum' }); expect(string.descent).toBeCloseTo(-3.5, 1); }); + test('should get glyph runs', () => { + const string = createLatinString({ + value: 'Lorem ipsum', + runs: [[0, 6], [6, 11]] + }); + + expect(string.glyphRuns).toHaveLength(2); + expect(string.glyphRuns[0].start).toBe(0); + expect(string.glyphRuns[0].end).toBe(6); + expect(string.glyphRuns[1].start).toBe(6); + expect(string.glyphRuns[1].end).toBe(11); + }); + + test('should get glyphs run (non latin)', () => { + const string = createKhmerString({ + value: 'ខ្ញុំអាចញ៉ាំកញ្ចក់បាន', + runs: [[0, 8], [8, 21]] + }); + + expect(string.glyphRuns).toHaveLength(2); + expect(string.glyphRuns[0].start).toBe(0); + expect(string.glyphRuns[0].end).toBe(7); + expect(string.glyphRuns[1].start).toBe(7); + expect(string.glyphRuns[1].end).toBe(16); + }); + + test('should get glyph runs for sliced string', () => { + const string = createLatinString({ + value: 'Lorem ipsum', + runs: [[0, 6], [6, 11]] + }); + + const sliced = string.slice(2, 8); + + expect(sliced.glyphRuns).toHaveLength(2); + expect(sliced.glyphRuns[0].start).toBe(2); + expect(sliced.glyphRuns[0].end).toBe(6); + expect(sliced.glyphRuns[1].start).toBe(6); + expect(sliced.glyphRuns[1].end).toBe(8); + }); + + test('should get glyphs run for sliced string (non latin)', () => { + const string = createKhmerString({ + value: 'ខ្ញុំអាចញ៉ាំកញ្ចក់បាន', + runs: [[0, 8], [8, 21]] + }); + + const sliced = string.slice(1, 15); + + expect(sliced.glyphRuns).toHaveLength(2); + expect(sliced.glyphRuns[0].start).toBe(1); + expect(sliced.glyphRuns[0].end).toBe(7); + expect(sliced.glyphRuns[1].start).toBe(7); + expect(sliced.glyphRuns[1].end).toBe(15); + }); + test('should isWhiteSpace return true if white space', () => { - const string = createString({ value: 'Lorem ipsum' }); + const string = createLatinString({ value: 'Lorem ipsum' }); expect(string.isWhiteSpace(5)).toBeTruthy(); }); test('should isWhiteSpace return false if non white space', () => { - const string = createString({ value: 'Lorem ipsum' }); + const string = createLatinString({ value: 'Lorem ipsum' }); expect(string.isWhiteSpace(3)).toBeFalsy(); }); test('should slice containing range', () => { - const string = createString({ value: 'Lorem ipsum' }); + const string = createLatinString({ value: 'Lorem ipsum' }); const sliced = string.slice(2, 6); expect(sliced.string).toBe('rem '); expect(sliced.glyphRuns[0].start).toBe(2); expect(sliced.glyphRuns[0].end).toBe(6); - expect(sliced.glyphRuns[0].glyphs).toHaveLength(4); + expect(sliced.glyphRuns[0].glyphs.length).toBe(4); }); test('should slice exceeding range', () => { - const string = createString({ value: 'Lorem ipsum' }); + const string = createLatinString({ value: 'Lorem ipsum' }); const sliced = string.slice(2, 14); expect(sliced.string).toBe('rem ipsum'); expect(sliced.glyphRuns[0].start).toBe(2); expect(sliced.glyphRuns[0].end).toBe(11); - expect(sliced.glyphRuns[0].glyphs).toHaveLength(9); + expect(sliced.glyphRuns[0].glyphs.length).toBe(9); }); test('should ignore unnecesary trailing runs when slice', () => { - const string = createString({ + const string = createLatinString({ value: 'Lorem ipsum', runs: [[0, 6], [6, 11]] }); @@ -95,7 +165,7 @@ describe('GlyphString', () => { }); test('should ignore unnecesary leading runs when slice', () => { - const string = createString({ + const string = createLatinString({ value: 'Lorem ipsum', runs: [[0, 6], [6, 11]] }); @@ -108,7 +178,7 @@ describe('GlyphString', () => { }); test('should return correct run index at glyph index', () => { - const string = createString({ + const string = createLatinString({ value: 'Lorem ipsum', runs: [[0, 6], [6, 11]] }); @@ -117,8 +187,22 @@ describe('GlyphString', () => { expect(string.runIndexAtGlyphIndex(9)).toBe(1); }); + test('should return correct run index at glyph index (non latin)', () => { + const string = createKhmerString({ + value: 'ខ្ញុំអាចញ៉ាំកញ្ចក់បាន', + runs: [[0, 8], [8, 21]] + }); + + expect(string.runIndexAtGlyphIndex(0)).toBe(0); + expect(string.runIndexAtGlyphIndex(4)).toBe(0); + expect(string.runIndexAtGlyphIndex(6)).toBe(0); + expect(string.runIndexAtGlyphIndex(7)).toBe(1); + expect(string.runIndexAtGlyphIndex(11)).toBe(1); + expect(string.runIndexAtGlyphIndex(15)).toBe(1); + }); + test('should return correct run index at glyph index for sliced strings', () => { - const string = createString({ + const string = createLatinString({ value: 'Lorem ipsum', runs: [[0, 6], [6, 11]] }); @@ -131,8 +215,22 @@ describe('GlyphString', () => { expect(sliced.runIndexAtGlyphIndex(9)).toBe(1); }); + test('should return correct run index at glyph index for sliced strings (non latin)', () => { + const string = createKhmerString({ + value: 'ខ្ញុំអាចញ៉ាំកញ្ចក់បាន', + runs: [[0, 8], [8, 21]] + }); + + const sliced = string.slice(4, 11); + + expect(sliced.runIndexAtGlyphIndex(0)).toBe(0); + expect(sliced.runIndexAtGlyphIndex(2)).toBe(0); + expect(sliced.runIndexAtGlyphIndex(3)).toBe(1); + expect(sliced.runIndexAtGlyphIndex(6)).toBe(1); + }); + test('should return correct run at glyph index', () => { - const string = createString({ + const string = createLatinString({ value: 'Lorem ipsum', runs: [[0, 6], [6, 11]] }); @@ -142,31 +240,47 @@ describe('GlyphString', () => { }); test('should return correct run at glyph index for sliced strings', () => { - const string = createString({ + const string = createLatinString({ value: 'Lorem ipsum', runs: [[0, 6], [6, 11]] }); const sliced = string.slice(4, 11); - expect(sliced.runAtGlyphIndex(0).start).toBe(0); - expect(sliced.runAtGlyphIndex(1).start).toBe(0); + expect(sliced.runAtGlyphIndex(0).start).toBe(4); + expect(sliced.runAtGlyphIndex(1).start).toBe(4); expect(sliced.runAtGlyphIndex(2).start).toBe(6); expect(sliced.runAtGlyphIndex(5).start).toBe(6); }); test('should return correct run index at string index', () => { - const string = createString({ + const string = createLatinString({ value: 'Lorem ipsum', runs: [[0, 6], [6, 11]] }); expect(string.runIndexAtStringIndex(2)).toBe(0); + expect(string.runIndexAtStringIndex(5)).toBe(0); + expect(string.runIndexAtStringIndex(6)).toBe(1); expect(string.runIndexAtStringIndex(9)).toBe(1); }); + test('should return correct run index at string index (non latin)', () => { + const string = createKhmerString({ + value: 'ខ្ញុំអាចញ៉ាំកញ្ចក់បាន', + runs: [[0, 8], [8, 21]] + }); + + expect(string.runIndexAtStringIndex(0)).toBe(0); + expect(string.runIndexAtStringIndex(4)).toBe(0); + expect(string.runIndexAtStringIndex(7)).toBe(0); + expect(string.runIndexAtStringIndex(8)).toBe(1); + expect(string.runIndexAtStringIndex(14)).toBe(1); + expect(string.runIndexAtStringIndex(20)).toBe(1); + }); + test('should return correct run index at string index for sliced strings', () => { - const string = createString({ + const string = createLatinString({ value: 'Lorem ipsum', runs: [[0, 6], [6, 11]] }); @@ -179,8 +293,23 @@ describe('GlyphString', () => { expect(sliced.runIndexAtStringIndex(5)).toBe(1); }); + test('should return correct run index at string index for sliced strings (non latin)', () => { + const string = createKhmerString({ + value: 'ខ្ញុំអាចញ៉ាំកញ្ចក់បាន', + runs: [[0, 8], [8, 21]] + }); + + const sliced = string.slice(4, 11); + + expect(sliced.runIndexAtStringIndex(0)).toBe(0); + expect(sliced.runIndexAtStringIndex(2)).toBe(0); + expect(sliced.runIndexAtStringIndex(3)).toBe(1); + expect(sliced.runIndexAtStringIndex(5)).toBe(1); + expect(sliced.runIndexAtStringIndex(6)).toBe(1); + }); + test('should return correct run at string index', () => { - const string = createString({ + const string = createLatinString({ value: 'Lorem ipsum', runs: [[0, 6], [6, 11]] }); @@ -189,22 +318,49 @@ describe('GlyphString', () => { expect(string.runAtStringIndex(9).start).toBe(6); }); + test('should return correct run at string index (non latin)', () => { + const string = createKhmerString({ + value: 'ខ្ញុំអាចញ៉ាំកញ្ចក់បាន', + runs: [[0, 8], [8, 21]] + }); + + expect(string.runAtStringIndex(2).start).toBe(0); + expect(string.runAtStringIndex(7).start).toBe(0); + expect(string.runAtStringIndex(8).start).toBe(7); + expect(string.runAtStringIndex(20).start).toBe(7); + }); + test('should return correct run at string index for sliced strings', () => { - const string = createString({ + const string = createLatinString({ value: 'Lorem ipsum', runs: [[0, 6], [6, 11]] }); const sliced = string.slice(4, 11); - expect(sliced.runAtStringIndex(0).start).toBe(0); - expect(sliced.runAtStringIndex(1).start).toBe(0); + expect(sliced.runAtStringIndex(0).start).toBe(4); + expect(sliced.runAtStringIndex(1).start).toBe(4); expect(sliced.runAtStringIndex(2).start).toBe(6); expect(sliced.runAtStringIndex(5).start).toBe(6); }); + test('should return correct run at string index for sliced strings (non latin)', () => { + const string = createKhmerString({ + value: 'ខ្ញុំអាចញ៉ាំកញ្ចក់បាន', + runs: [[0, 8], [8, 21]] + }); + + const sliced = string.slice(4, 11); + + expect(sliced.runAtStringIndex(0).start).toBe(4); + expect(sliced.runAtStringIndex(1).start).toBe(4); + expect(sliced.runAtStringIndex(2).start).toBe(4); + expect(sliced.runAtStringIndex(3).start).toBe(7); + expect(sliced.runAtStringIndex(6).start).toBe(7); + }); + test('should return correct glyph at index', () => { - const string = createString({ + const string = createLatinString({ value: 'Lorem ipsum', runs: [[0, 6], [6, 11]] }); @@ -216,8 +372,23 @@ describe('GlyphString', () => { expect(string.glyphAtIndex(9).id).toBe(secondRunGlyphs[3].id); }); + test('should return correct glyph at index (non latin)', () => { + const string = createKhmerString({ + value: 'ខ្ញុំអាចញ៉ាំកញ្ចក់បាន', + runs: [[0, 8], [8, 21]] + }); + + const firstRunGlyphs = string._glyphRuns[0].glyphs; + const secondRunGlyphs = string._glyphRuns[1].glyphs; + + expect(string.glyphAtIndex(2).id).toBe(firstRunGlyphs[2].id); + expect(string.glyphAtIndex(6).id).toBe(firstRunGlyphs[6].id); + expect(string.glyphAtIndex(7).id).toBe(secondRunGlyphs[0].id); + expect(string.glyphAtIndex(15).id).toBe(secondRunGlyphs[8].id); + }); + test('should return correct glyph at index for sliced strings', () => { - const string = createString({ + const string = createLatinString({ value: 'Lorem ipsum', runs: [[0, 6], [6, 11]] }); @@ -232,8 +403,23 @@ describe('GlyphString', () => { expect(sliced.glyphAtIndex(5).id).toBe(secondRunGlyphs[3].id); }); + test('should return correct glyph at index for sliced strings (non latin)', () => { + const string = createKhmerString({ + value: 'ខ្ញុំអាចញ៉ាំកញ្ចក់បាន', + runs: [[0, 8], [8, 21]] + }); + + const sliced = string.slice(4, 11); + const firstRunGlyphs = sliced._glyphRuns[0].glyphs; + const secondRunGlyphs = sliced._glyphRuns[1].glyphs; + + expect(sliced.glyphAtIndex(0).id).toBe(firstRunGlyphs[4].id); + expect(sliced.glyphAtIndex(2).id).toBe(firstRunGlyphs[6].id); + expect(sliced.glyphAtIndex(3).id).toBe(secondRunGlyphs[0].id); + }); + test('should return correct glyph width at index', () => { - const string = createString({ + const string = createLatinString({ value: 'Lorem ipsum', runs: [[0, 6], [6, 11]] }); @@ -245,8 +431,23 @@ describe('GlyphString', () => { expect(string.getGlyphWidth(9)).toBe(secondRunPositions[3].xAdvance); }); + test('should return correct glyph width at index (non latin)', () => { + const string = createKhmerString({ + value: 'ខ្ញុំអាចញ៉ាំកញ្ចក់បាន', + runs: [[0, 8], [8, 21]] + }); + + const firstRunPositions = string._glyphRuns[0].positions; + const secondRunPositions = string._glyphRuns[1].positions; + + expect(string.getGlyphWidth(0)).toBe(firstRunPositions[0].xAdvance); + expect(string.getGlyphWidth(6)).toBe(firstRunPositions[6].xAdvance); + expect(string.getGlyphWidth(7)).toBe(secondRunPositions[0].xAdvance); + expect(string.getGlyphWidth(15)).toBe(secondRunPositions[8].xAdvance); + }); + test('should return correct glyph width at index for sliced strings', () => { - const string = createString({ + const string = createLatinString({ value: 'Lorem ipsum', runs: [[0, 6], [6, 11]] }); @@ -261,10 +462,26 @@ describe('GlyphString', () => { expect(sliced.getGlyphWidth(5)).toBe(secondRunPositions[3].xAdvance); }); + test('should return correct glyph width at index for sliced strings (non latin)', () => { + const string = createKhmerString({ + value: 'ខ្ញុំអាចញ៉ាំកញ្ចក់បាន', + runs: [[0, 8], [8, 21]] + }); + + const sliced = string.slice(4, 11); + const firstRunPositions = sliced._glyphRuns[0].positions; + const secondRunPositions = sliced._glyphRuns[1].positions; + + expect(sliced.getGlyphWidth(0)).toBe(firstRunPositions[4].xAdvance); + expect(sliced.getGlyphWidth(1)).toBe(firstRunPositions[5].xAdvance); + expect(sliced.getGlyphWidth(3)).toBe(secondRunPositions[0].xAdvance); + expect(sliced.getGlyphWidth(4)).toBe(secondRunPositions[1].xAdvance); + }); + test('should return correct glyph index at offset', () => { // l o r e m i p s u m // 6.22 7.24 4.65 6.73 11.16 3.11 3.03 7.35 5.72 7.36 11.16 - const string = createString({ + const string = createLatinString({ value: 'Lorem ipsum', runs: [[0, 6], [6, 11]] }); @@ -275,8 +492,22 @@ describe('GlyphString', () => { expect(string.glyphIndexAtOffset(50)).toBe(8); }); + test('should return correct glyph index at offset (non latin)', () => { + const string = createKhmerString({ + value: 'ខ្ញុំអាចញ៉ាំកញ្ចក់បាន', + runs: [[0, 8], [8, 21]] + }); + + expect(string.glyphIndexAtOffset(8)).toBe(0); + expect(string.glyphIndexAtOffset(10)).toBe(4); + expect(string.glyphIndexAtOffset(24)).toBe(6); + expect(string.glyphIndexAtOffset(32)).toBe(7); + expect(string.glyphIndexAtOffset(51)).toBe(8); + expect(string.glyphIndexAtOffset(92)).toBe(14); + }); + test('should return correct glyph index at offset for sliced strings', () => { - const string = createString({ + const string = createLatinString({ value: 'Lorem ipsum', runs: [[0, 6], [6, 11]] }); @@ -293,8 +524,24 @@ describe('GlyphString', () => { expect(sliced.glyphIndexAtOffset(39)).toBe(6); }); + test('should return correct glyph index at offset for sliced strings (non latin)', () => { + const string = createKhmerString({ + value: 'ខ្ញុំអាចញ៉ាំកញ្ចក់បាន', + runs: [[0, 8], [8, 21]] + }); + + const sliced = string.slice(4, 11); + + expect(sliced.glyphIndexAtOffset(0)).toBe(0); + expect(sliced.glyphIndexAtOffset(10)).toBe(1); + expect(sliced.glyphIndexAtOffset(14)).toBe(2); + expect(sliced.glyphIndexAtOffset(24)).toBe(3); + expect(sliced.glyphIndexAtOffset(43)).toBe(4); + expect(sliced.glyphIndexAtOffset(52)).toBe(5); + }); + test('should return correct string index for glyph index', () => { - const string = createString({ + const string = createLatinString({ value: 'Lorem ipsum', runs: [[0, 6], [6, 11]] }); @@ -305,8 +552,23 @@ describe('GlyphString', () => { expect(string.stringIndexForGlyphIndex(10)).toBe(10); }); + test('should return correct string index for glyph index (non latin)', () => { + const string = createKhmerString({ + value: 'ខ្ញុំអាចញ៉ាំកញ្ចក់បាន', + runs: [[0, 8], [8, 21]] + }); + + expect(string.stringIndexForGlyphIndex(0)).toBe(0); + expect(string.stringIndexForGlyphIndex(2)).toBe(3); + expect(string.stringIndexForGlyphIndex(6)).toBe(7); + expect(string.stringIndexForGlyphIndex(7)).toBe(8); + expect(string.stringIndexForGlyphIndex(8)).toBe(12); + expect(string.stringIndexForGlyphIndex(11)).toBe(16); + expect(string.stringIndexForGlyphIndex(15)).toBe(20); + }); + test('should return correct string index for glyph index for sliced strings', () => { - const string = createString({ + const string = createLatinString({ value: 'Lorem ipsum', runs: [[0, 6], [6, 11]] }); @@ -318,8 +580,24 @@ describe('GlyphString', () => { expect(sliced.stringIndexForGlyphIndex(6)).toBe(6); }); + test('should return correct string index for glyph index for sliced strings (not latin)', () => { + const string = createKhmerString({ + value: 'ខ្ញុំអាចញ៉ាំកញ្ចក់បាន', + runs: [[0, 8], [8, 21]] + }); + + const sliced = string.slice(4, 11); + + expect(sliced.stringIndexForGlyphIndex(0)).toBe(0); + expect(sliced.stringIndexForGlyphIndex(1)).toBe(1); + expect(sliced.stringIndexForGlyphIndex(3)).toBe(3); + expect(sliced.stringIndexForGlyphIndex(4)).toBe(7); + expect(sliced.stringIndexForGlyphIndex(5)).toBe(8); + expect(sliced.stringIndexForGlyphIndex(6)).toBe(9); + }); + test('should return correct glyph index for string index', () => { - const string = createString({ + const string = createLatinString({ value: 'Lorem ipsum', runs: [[0, 6], [6, 11]] }); @@ -330,21 +608,48 @@ describe('GlyphString', () => { expect(string.glyphIndexForStringIndex(10)).toBe(10); }); + test('should return correct glyph index for string index (not latin)', () => { + const string = createKhmerString({ + value: 'ខ្ញុំអាចញ៉ាំកញ្ចក់បាន', + runs: [[0, 8], [8, 21]] + }); + + expect(string.glyphIndexForStringIndex(0)).toBe(0); + expect(string.glyphIndexForStringIndex(4)).toBe(3); + expect(string.glyphIndexForStringIndex(7)).toBe(6); + expect(string.glyphIndexForStringIndex(8)).toBe(7); + expect(string.glyphIndexForStringIndex(12)).toBe(8); + }); + test('should return correct glyph index for string index for sliced strings', () => { - const string = createString({ + const string = createLatinString({ value: 'Lorem ipsum', runs: [[0, 6], [6, 11]] }); - const sliced = string.slice(4, 11); + const sliced = string.slice(2, 6); expect(sliced.glyphIndexForStringIndex(0)).toBe(0); + expect(sliced.glyphIndexForStringIndex(1)).toBe(1); expect(sliced.glyphIndexForStringIndex(2)).toBe(2); - expect(sliced.glyphIndexForStringIndex(6)).toBe(6); + expect(sliced.glyphIndexForStringIndex(3)).toBe(3); + }); + + test('should return correct glyph index for string index for sliced strings (not latin)', () => { + const string = createKhmerString({ + value: 'ខ្ញុំអាចញ៉ាំកញ្ចក់បាន', + runs: [[0, 8], [8, 21]] + }); + + const sliced = string.slice(4, 15); + + expect(sliced.glyphIndexForStringIndex(0)).toBe(0); + expect(sliced.glyphIndexForStringIndex(3)).toBe(3); + expect(sliced.glyphIndexForStringIndex(4)).toBe(4); }); test('should return correct glyph code at glyph index', () => { - const string = createString({ + const string = createLatinString({ value: 'Lorem ipsum', runs: [[0, 6], [6, 11]] }); @@ -354,7 +659,7 @@ describe('GlyphString', () => { }); test('should return correct glyph code at glyph index for sliced strings', () => { - const string = createString({ + const string = createLatinString({ value: 'Lorem ipsum', runs: [[0, 6], [6, 11]] }); @@ -368,7 +673,7 @@ describe('GlyphString', () => { }); test('should return correct char at glyph index', () => { - const string = createString({ + const string = createLatinString({ value: 'Lorem ipsum', runs: [[0, 6], [6, 11]] }); @@ -378,7 +683,7 @@ describe('GlyphString', () => { }); test('should return correct char at glyph index for sliced strings', () => { - const string = createString({ + const string = createLatinString({ value: 'Lorem ipsum', runs: [[0, 6], [6, 11]] }); @@ -394,7 +699,7 @@ describe('GlyphString', () => { test('should return correct offset at glyph index', () => { // l o r e m i p s u m // 6.22 7.24 4.65 6.73 11.16 3.11 3.03 7.35 5.72 7.36 11.16 - const string = createString({ + const string = createLatinString({ value: 'Lorem ipsum', runs: [[0, 6], [6, 11]] }); @@ -406,7 +711,7 @@ describe('GlyphString', () => { }); test('should return correct offset at glyph index for sliced strings', () => { - const string = createString({ + const string = createLatinString({ value: 'Lorem ipsum', runs: [[0, 6], [6, 11]] }); @@ -423,7 +728,7 @@ describe('GlyphString', () => { }); test('should return correct index of', () => { - const string = createString({ + const string = createLatinString({ value: 'Lorem ipsum', runs: [[0, 6], [6, 11]] }); @@ -437,7 +742,7 @@ describe('GlyphString', () => { }); test('should return correct index of for sliced strings', () => { - const string = createString({ + const string = createLatinString({ value: 'Lorem ipsum', runs: [[0, 6], [6, 11]] }); @@ -452,7 +757,7 @@ describe('GlyphString', () => { }); test('should return correct unicode category', () => { - const string = createString({ + const string = createLatinString({ value: 'Lo1.+ ' }); @@ -465,22 +770,59 @@ describe('GlyphString', () => { }); test('should return if is white space for index', () => { - const string = createString({ - value: 'L ' - }); + const string = createLatinString({ value: 'L ' }); expect(string.isWhiteSpace(0)).toBeFalsy(); expect(string.isWhiteSpace(1)).toBeTruthy(); }); test('should return if is white space for index for sliced string', () => { - const string = createString({ - value: 'someL ' - }); + const string = createLatinString({ value: 'someL ' }); const sliced = string.slice(4, string.length); expect(sliced.isWhiteSpace(0)).toBeFalsy(); expect(sliced.isWhiteSpace(1)).toBeTruthy(); }); + + test('should successfully insert glyph', () => { + const char = 0x002d; // "-" + const string = createLatinString({ value: 'Lorem ipsum', runs: [[0, 6], [6, 11]] }); + + string.insertGlyph(2, char); + + expect(string.start).toBe(0); + expect(string.end).toBe(12); + expect(string.glyphRuns[0].start).toBe(0); + expect(string.glyphRuns[0].end).toBe(7); + expect(string.glyphRuns[1].start).toBe(7); + expect(string.glyphRuns[1].end).toBe(12); + expect(string.glyphRuns[0].glyphs[2].id).toBe(16); + + // Test string indices. + // The new glyph shouldn't interfer with current indices + expect(string.glyphRuns[0].stringIndices[2]).toBe(2); + expect(string.glyphRuns[0].stringIndices[3]).toBe(2); + }); + + test('should successfully insert glyph for sliced strings', () => { + const char = 0x002d; // "-" + const string = createLatinString({ value: 'Lorem ipsum', runs: [[0, 6], [6, 11]] }); + const sliced = string.slice(4, string.length); + + sliced.insertGlyph(2, char); + + expect(sliced.start).toBe(4); + expect(sliced.end).toBe(12); + expect(sliced._glyphRuns[0].start).toBe(0); + expect(sliced._glyphRuns[0].end).toBe(6); + expect(sliced._glyphRuns[1].start).toBe(6); + expect(sliced._glyphRuns[1].end).toBe(12); + expect(sliced._glyphRuns[1].glyphs[0].id).toBe(16); + + // Test string indices. + // The new glyph shouldn't interfer with current indices + expect(string._glyphRuns[1].stringIndices[0]).toBe(0); + expect(string._glyphRuns[1].stringIndices[1]).toBe(0); + }); }); diff --git a/test/utils/glyphRuns.js b/test/utils/glyphRuns.js index 989e08d..2095b39 100644 --- a/test/utils/glyphRuns.js +++ b/test/utils/glyphRuns.js @@ -5,21 +5,12 @@ import GlyphRun from '../../src/models/GlyphRun'; export const glyphRunFactory = font => ({ attributes = {}, value = 'Lorem Ipsum', - start = 0, - end = value.length + start = 0 } = {}) => { - const string = value.slice(start, end); - const run = font.layout(string); + const run = font.layout(value); const attrs = new RunStyle(Object.assign({}, { font }, attributes)); const { glyphs, positions, stringIndices } = run; - const glyphRun = new GlyphRun( - start, - end || glyphs.length, - attrs, - glyphs, - positions, - stringIndices - ); + const glyphRun = new GlyphRun(start, glyphs.length, attrs, glyphs, positions, stringIndices); return { glyphRun, glyphs, positions, stringIndices, attrs }; }; diff --git a/test/utils/glyphStrings.js b/test/utils/glyphStrings.js index fbf4bfb..3e2d0a9 100644 --- a/test/utils/glyphStrings.js +++ b/test/utils/glyphStrings.js @@ -1,21 +1,31 @@ +import RunStyle from '../../src/models/RunStyle'; +import GlyphRun from '../../src/models/GlyphRun'; import GlyphString from '../../src/models/GlyphString'; -import { glyphRunFactory } from './glyphRuns'; /* eslint-disable-next-line */ export const glyphStringFactory = font => ({ value = 'Lorem ipsum', runs = [[0, value.length]] } = {}) => { - const createRun = glyphRunFactory(font); + let glyphIndex = 0; - const glyphRuns = runs.map( - run => - createRun({ - value, - start: run[0], - end: run[1] - }).glyphRun - ); + const glyphRuns = runs.map(run => { + const string = value.slice(run[0], run[1]); + const attrs = new RunStyle(Object.assign({}, { font })); + const { glyphs, positions, stringIndices } = font.layout(string); + const glyphRun = new GlyphRun( + glyphIndex, + glyphIndex + glyphs.length, + attrs, + glyphs, + positions, + stringIndices + ); - return new GlyphString(value, glyphRuns); + glyphIndex += glyphs.length; + + return glyphRun; + }); + + return new GlyphString(value, glyphRuns, 0, value.length); };