From 2462590b540ca38f92047254577b17882b23e59d Mon Sep 17 00:00:00 2001
From: Diego Muracciole <diegomuracciole@gmail.com>
Date: Sun, 25 Mar 2018 20:07:37 -0300
Subject: [PATCH 1/5] Fix insert glyph

---
 out.pdf                         | Bin 6254 -> 18638 bytes
 src/layout/LayoutEngine.js      |   5 +--
 src/layout/LineBreaker.js       |  61 ++++++++++++++++----------------
 src/models/GlyphString.js       |   9 +++--
 temp.js                         |  13 +++----
 test/models/GlyphString.test.js |  38 ++++++++++++++++++++
 test/utils/glyphStrings.js      |   2 +-
 7 files changed, 86 insertions(+), 42 deletions(-)

diff --git a/out.pdf b/out.pdf
index b8a9254f5b4f4683242e19a54b763fbbc1774f7b..924614b918d2b7d4b4bfe7574e4ab2fab28beda6 100644
GIT binary patch
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/GlyphString.js b/src/models/GlyphString.js
index 5f7a6ad..d819b70 100644
--- a/src/models/GlyphString.js
+++ b/src/models/GlyphString.js
@@ -112,7 +112,7 @@ class GlyphString {
     const idx = index + this._glyphRuns[0].stringStart + this.start;
 
     for (let i = 0; i < this._glyphRuns.length; i++) {
-      if (this._glyphRuns[i].stringStart <= idx && idx < this._glyphRuns[i].stringEnd + 1) {
+      if (this._glyphRuns[i].start <= idx && idx < this._glyphRuns[i].end) {
         return i;
       }
     }
@@ -233,9 +233,14 @@ class GlyphString {
   insertGlyph(index, codePoint) {
     const runIndex = this.runIndexAtGlyphIndex(index);
     const run = this._glyphRuns[runIndex];
+    const char = String.fromCharCode(codePoint);
     const glyph = run.attributes.font.glyphForCodePoint(codePoint);
 
-    glyph.inserted = true; // TODO: don't do this
+    // Should we edit the string value?
+    // Otherwise, the glyph runs and string would be inconsistent
+    this._string =
+      this._string.slice(0, this.start + index) + char + this._string.slice(this.start + index);
+
     run.glyphs.splice(this.start + index - run.start, 0, glyph);
     run.positions.splice(this.start + index - run.start, 0, {
       xAdvance: glyph.advanceWidth,
diff --git a/temp.js b/temp.js
index 7d34fab..2eba6da 100644
--- a/temp.js
+++ b/temp.js
@@ -11,7 +11,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 +30,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,13 +44,14 @@ 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
     }
diff --git a/test/models/GlyphString.test.js b/test/models/GlyphString.test.js
index 2a53717..358b6fa 100644
--- a/test/models/GlyphString.test.js
+++ b/test/models/GlyphString.test.js
@@ -483,4 +483,42 @@ describe('GlyphString', () => {
     expect(sliced.isWhiteSpace(0)).toBeFalsy();
     expect(sliced.isWhiteSpace(1)).toBeTruthy();
   });
+
+  test('should successfully insert glyph', () => {
+    const string = createString({
+      value: 'Lorem ipsum',
+      runs: [[0, 6], [6, 11]]
+    });
+
+    string.insertGlyph(2, 0x002d);
+
+    expect(string.start).toBe(0);
+    expect(string.end).toBe(12);
+    expect(string.string).toBe('Lo-rem ipsum');
+    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('should successfully insert glyph for sliced strings', () => {
+    const string = createString({
+      value: 'Lorem ipsum',
+      runs: [[0, 6], [6, 11]]
+    });
+
+    const sliced = string.slice(4, string.length);
+
+    sliced.insertGlyph(2, 0x002d);
+
+    expect(sliced.start).toBe(4);
+    expect(sliced.end).toBe(12);
+    expect(sliced.string).toBe('m -ipsum');
+    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);
+  });
 });
diff --git a/test/utils/glyphStrings.js b/test/utils/glyphStrings.js
index fbf4bfb..f24be4b 100644
--- a/test/utils/glyphStrings.js
+++ b/test/utils/glyphStrings.js
@@ -17,5 +17,5 @@ export const glyphStringFactory = font => ({
       }).glyphRun
   );
 
-  return new GlyphString(value, glyphRuns);
+  return new GlyphString(value, glyphRuns, 0, value.length);
 };

From 0a8862b091ce317f9280535015eb0ad6307111b2 Mon Sep 17 00:00:00 2001
From: Diego Muracciole <diegomuracciole@gmail.com>
Date: Mon, 26 Mar 2018 00:47:39 -0300
Subject: [PATCH 2/5] Fix GlyphRun and add tests

---
 out.pdf                         | Bin 18638 -> 27683 bytes
 src/models/GlyphRun.js          |  15 +-
 src/models/GlyphString.js       |  26 +--
 test/data/Khmer.ttf             | Bin 0 -> 77684 bytes
 test/models/GlyphRun.test.js    | 230 ++++++++++++++++++++-----
 test/models/GlyphString.test.js | 296 +++++++++++++++++++++++++-------
 6 files changed, 447 insertions(+), 120 deletions(-)
 create mode 100644 test/data/Khmer.ttf

diff --git a/out.pdf b/out.pdf
index 924614b918d2b7d4b4bfe7574e4ab2fab28beda6..b248413312e88935eccb43fb34a95d42adf38c55 100644
GIT binary patch
diff --git a/src/models/GlyphRun.js b/src/models/GlyphRun.js
index e5585df..c21e3c4 100644
--- a/src/models/GlyphRun.js
+++ b/src/models/GlyphRun.js
@@ -7,8 +7,6 @@ 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++) {
@@ -61,15 +59,18 @@ class GlyphRun extends Run {
   }
 
   slice(start, end) {
+    const glyphStart = this.glyphIndices[start];
+    const glyphEnd = this.glyphIndices[end] || this.end;
+
     start += this.start;
     end += this.start;
-    end = Math.min(end, this.start + this.glyphs.length);
+    end = Math.min(end, this.end);
 
-    const glyphs = this.glyphs.slice(start - this.start, end - this.start);
-    const positions = this.positions.slice(start - this.start, end - this.start);
+    const glyphs = this.glyphs.slice(glyphStart - this.start, glyphEnd - this.start);
+    const positions = this.positions.slice(glyphStart - this.start, glyphEnd - this.start);
     const stringIndices = this.stringIndices
-      .slice(start - this.start, end - this.start)
-      .map(index => index - this.stringIndices[start - this.start]);
+      .slice(glyphStart - this.start, glyphEnd - this.start)
+      .map(index => index - this.stringIndices[glyphStart - this.start]);
 
     return new GlyphRun(start, end, this.attributes, glyphs, positions, stringIndices, true);
   }
diff --git a/src/models/GlyphString.js b/src/models/GlyphString.js
index d819b70..a535cdf 100644
--- a/src/models/GlyphString.js
+++ b/src/models/GlyphString.js
@@ -67,8 +67,8 @@ class GlyphString {
       return this._glyphRunsCache;
     }
 
-    const startRunIndex = this.runIndexAtGlyphIndex(0);
-    const endRunIndex = this.runIndexAtGlyphIndex(this.length);
+    const startRunIndex = this.runIndexAtStringIndex(0);
+    const endRunIndex = this.runIndexAtStringIndex(this.length);
     const startRun = this._glyphRuns[startRunIndex];
     const endRun = this._glyphRuns[endRunIndex];
     const runs = [];
@@ -94,11 +94,16 @@ class GlyphString {
 
   runIndexAtGlyphIndex(index) {
     const idx = 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 <= idx && idx < count + run.glyphs.length) {
         return i;
       }
+
+      count += run.glyphs.length;
     }
 
     return this._glyphRuns.length - 1;
@@ -109,7 +114,7 @@ class GlyphString {
   }
 
   runIndexAtStringIndex(index) {
-    const idx = index + this._glyphRuns[0].stringStart + this.start;
+    const idx = index + this._glyphRuns[0].start + this.start;
 
     for (let i = 0; i < this._glyphRuns.length; i++) {
       if (this._glyphRuns[i].start <= idx && idx < this._glyphRuns[i].end) {
@@ -233,16 +238,13 @@ class GlyphString {
   insertGlyph(index, codePoint) {
     const runIndex = this.runIndexAtGlyphIndex(index);
     const run = this._glyphRuns[runIndex];
-    const char = String.fromCharCode(codePoint);
     const glyph = run.attributes.font.glyphForCodePoint(codePoint);
+    const idx = this.start + index - run.start;
 
-    // Should we edit the string value?
-    // Otherwise, the glyph runs and string would be inconsistent
-    this._string =
-      this._string.slice(0, this.start + index) + char + this._string.slice(this.start + index);
-
-    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.glyphIndices.splice(idx, 0, run.glyphIndices[idx]);
+    run.stringIndices.splice(idx, 0, run.stringIndices[idx]);
+    run.positions.splice(idx, 0, {
       xAdvance: glyph.advanceWidth,
       yAdvance: 0,
       xOffset: 0,
diff --git a/test/data/Khmer.ttf b/test/data/Khmer.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..e127df46607865834d20f916fd27d9c92437c4ad
GIT binary patch
diff --git a/test/models/GlyphRun.test.js b/test/models/GlyphRun.test.js
index cc6e912..3d52965 100644
--- a/test/models/GlyphRun.test.js
+++ b/test/models/GlyphRun.test.js
@@ -3,100 +3,250 @@ 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 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, end: 11 });
+    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, end: 11 });
     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, end: 11 });
     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, end: 11 });
+    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, end: 11 });
+    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, end: 11 });
+    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, end: 21 });
+    const { glyphs } = glyphRun.slice(1, 8);
+
+    expect(glyphs.map(g => g.id)).toEqual([153, 177, 112, 248, 188, 49]);
+  });
+
+  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, end: 11 });
+    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, end: 21 });
+    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, end: 11 });
+    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, end: 21 });
+    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]);
+  });
+
+  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, end: 11 });
+    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, end: 21 });
+    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, end: 11 });
+    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, end: 21 });
+    const { stringIndices } = glyphRun.slice(1, 8);
+
+    expect(stringIndices).toEqual([0, 2, 3, 4, 5, 6]);
+  });
+
+  test('should exact slice return same string indices', () => {
+    const { glyphRun } = createLatinRun({ start: 0, end: 11 });
+    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, end: 21 });
+    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, end: 11 });
+    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 358b6fa..d63c21c 100644
--- a/test/models/GlyphString.test.js
+++ b/test/models/GlyphString.test.js
@@ -2,67 +2,125 @@ 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 length', () => {
-    const string = createString({ value: 'Lorem ipsum' });
+    const string = createLatinString({ value: 'Lorem ipsum' });
 
     expect(string.length).toBe(11);
   });
 
   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 glyphs run', () => {
+    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 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 (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(8);
+    expect(string.glyphRuns[1].start).toBe(8);
+    expect(string.glyphRuns[1].end).toBe(21);
+  });
+
+  test('should get glyphs run for sliced string (non latin)', () => {
+    const string = createKhmerString({
+      value: 'ខ្ញុំអាចញ៉ាំកញ្ចក់បាន',
+      runs: [[0, 8], [8, 21]]
+    });
+
+    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 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 ');
@@ -72,7 +130,7 @@ describe('GlyphString', () => {
   });
 
   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');
@@ -82,7 +140,7 @@ describe('GlyphString', () => {
   });
 
   test('should ignore unnecesary trailing runs when slice', () => {
-    const string = createString({
+    const string = createLatinString({
       value: 'Lorem ipsum',
       runs: [[0, 6], [6, 11]]
     });
@@ -95,7 +153,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 +166,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]]
     });
@@ -118,7 +176,7 @@ describe('GlyphString', () => {
   });
 
   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 +189,40 @@ describe('GlyphString', () => {
     expect(sliced.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.skip('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);
+
+    console.log(sliced);
+
+    expect(sliced.runIndexAtGlyphIndex(0)).toBe(0);
+    expect(sliced.runIndexAtGlyphIndex(4)).toBe(0);
+    expect(sliced.runIndexAtGlyphIndex(6)).toBe(0);
+    expect(sliced.runIndexAtGlyphIndex(7)).toBe(1);
+    expect(sliced.runIndexAtGlyphIndex(11)).toBe(1);
+    expect(sliced.runIndexAtGlyphIndex(15)).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,7 +232,7 @@ 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]]
     });
@@ -156,7 +246,7 @@ describe('GlyphString', () => {
   });
 
   test('should return correct run index at string index', () => {
-    const string = createString({
+    const string = createLatinString({
       value: 'Lorem ipsum',
       runs: [[0, 6], [6, 11]]
     });
@@ -166,7 +256,7 @@ describe('GlyphString', () => {
   });
 
   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 +269,38 @@ describe('GlyphString', () => {
     expect(sliced.runIndexAtStringIndex(5)).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 (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(0);
+    expect(sliced.runIndexAtStringIndex(4)).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]]
     });
@@ -190,7 +310,7 @@ describe('GlyphString', () => {
   });
 
   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]]
     });
@@ -203,8 +323,32 @@ describe('GlyphString', () => {
     expect(sliced.runAtStringIndex(5).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(9).start).toBe(8);
+  });
+
+  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(0);
+    expect(sliced.runAtStringIndex(1).start).toBe(0);
+    expect(sliced.runAtStringIndex(4).start).toBe(8);
+    expect(sliced.runAtStringIndex(6).start).toBe(8);
+  });
+
   test('should return correct glyph at index', () => {
-    const string = createString({
+    const string = createLatinString({
       value: 'Lorem ipsum',
       runs: [[0, 6], [6, 11]]
     });
@@ -217,7 +361,7 @@ describe('GlyphString', () => {
   });
 
   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 +376,39 @@ describe('GlyphString', () => {
     expect(sliced.glyphAtIndex(5).id).toBe(secondRunGlyphs[3].id);
   });
 
+  test.skip('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.skip('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;
+
+    const runs = sliced.glyphRuns;
+    // expect(sliced.glyphAtIndex(0).id).toBe(firstRunGlyphs[3].id);
+    // expect(sliced.glyphAtIndex(3).id).toBe(firstRunGlyphs[6].id);
+    // expect(sliced.glyphAtIndex(4).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]]
     });
@@ -246,7 +421,7 @@ describe('GlyphString', () => {
   });
 
   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]]
     });
@@ -264,7 +439,7 @@ describe('GlyphString', () => {
   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]]
     });
@@ -276,7 +451,7 @@ describe('GlyphString', () => {
   });
 
   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]]
     });
@@ -294,7 +469,7 @@ describe('GlyphString', () => {
   });
 
   test('should return correct string index for glyph index', () => {
-    const string = createString({
+    const string = createLatinString({
       value: 'Lorem ipsum',
       runs: [[0, 6], [6, 11]]
     });
@@ -306,7 +481,7 @@ describe('GlyphString', () => {
   });
 
   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]]
     });
@@ -319,7 +494,7 @@ describe('GlyphString', () => {
   });
 
   test('should return correct glyph index for string index', () => {
-    const string = createString({
+    const string = createLatinString({
       value: 'Lorem ipsum',
       runs: [[0, 6], [6, 11]]
     });
@@ -331,7 +506,7 @@ describe('GlyphString', () => {
   });
 
   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]]
     });
@@ -344,7 +519,7 @@ describe('GlyphString', () => {
   });
 
   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 +529,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 +543,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 +553,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 +569,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 +581,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 +598,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 +612,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 +627,7 @@ describe('GlyphString', () => {
   });
 
   test('should return correct unicode category', () => {
-    const string = createString({
+    const string = createLatinString({
       value: 'Lo1.+ '
     });
 
@@ -465,18 +640,14 @@ 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);
 
@@ -485,40 +656,43 @@ describe('GlyphString', () => {
   });
 
   test('should successfully insert glyph', () => {
-    const string = createString({
-      value: 'Lorem ipsum',
-      runs: [[0, 6], [6, 11]]
-    });
+    const char = 0x002d; // "-"
+    const string = createLatinString({ value: 'Lorem ipsum', runs: [[0, 6], [6, 11]] });
 
-    string.insertGlyph(2, 0x002d);
+    string.insertGlyph(2, char);
 
     expect(string.start).toBe(0);
     expect(string.end).toBe(12);
-    expect(string.string).toBe('Lo-rem ipsum');
     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 string = createString({
-      value: 'Lorem ipsum',
-      runs: [[0, 6], [6, 11]]
-    });
-
+    const char = 0x002d; // "-"
+    const string = createLatinString({ value: 'Lorem ipsum', runs: [[0, 6], [6, 11]] });
     const sliced = string.slice(4, string.length);
 
-    sliced.insertGlyph(2, 0x002d);
+    sliced.insertGlyph(2, char);
 
     expect(sliced.start).toBe(4);
     expect(sliced.end).toBe(12);
-    expect(sliced.string).toBe('m -ipsum');
     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);
   });
 });

From 8ba8cc9adb54adffa6401527cfbb9dbfcdb0a2a9 Mon Sep 17 00:00:00 2001
From: Diego Muracciole <diegomuracciole@gmail.com>
Date: Mon, 26 Mar 2018 05:07:25 -0300
Subject: [PATCH 3/5] Add some more GlyphString tests

---
 out.pdf                         | Bin 27683 -> 10520 bytes
 src/models/GlyphRun.js          |  68 ++++++--
 src/models/GlyphString.js       |  84 +++++++--
 test/models/GlyphString.test.js | 292 ++++++++++++++++++++++----------
 4 files changed, 324 insertions(+), 120 deletions(-)

diff --git a/out.pdf b/out.pdf
index b248413312e88935eccb43fb34a95d42adf38c55..2c4104c8963dbd68852149d78122ec363f9d3179 100644
GIT binary patch
diff --git a/src/models/GlyphRun.js b/src/models/GlyphRun.js
index c21e3c4..75f7313 100644
--- a/src/models/GlyphRun.js
+++ b/src/models/GlyphRun.js
@@ -7,11 +7,7 @@ class GlyphRun extends Run {
     this.positions = positions;
     this.stringIndices = stringIndices;
     this.scale = attributes.fontSize / attributes.font.unitsPerEm;
-    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) {
@@ -24,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() {
@@ -60,18 +88,32 @@ class GlyphRun extends Run {
 
   slice(start, end) {
     const glyphStart = this.glyphIndices[start];
-    const glyphEnd = this.glyphIndices[end] || this.end;
+    let glyphEnd = this.glyphIndices[end];
+
+    if (glyphEnd === undefined) {
+      glyphEnd = this.glyphIndices[this.glyphIndices.length - 1] + 1;
+    }
+
+    let glyphs;
+    let positions;
+    let stringIndices;
+
+    if (glyphEnd === 0) {
+      glyphs = [this.glyphs[glyphStart]];
+      positions = [this.positions[glyphStart]];
+      stringIndices = [this.stringIndices[glyphStart]];
+    } else {
+      glyphs = this.glyphs.slice(glyphStart, glyphEnd);
+      positions = this.positions.slice(glyphStart, glyphEnd);
+      stringIndices = this.stringIndices.slice(glyphStart, glyphEnd);
+    }
+
+    stringIndices = stringIndices.map(index => index - this.stringIndices[glyphStart]);
 
     start += this.start;
     end += this.start;
     end = Math.min(end, this.end);
 
-    const glyphs = this.glyphs.slice(glyphStart - this.start, glyphEnd - this.start);
-    const positions = this.positions.slice(glyphStart - this.start, glyphEnd - this.start);
-    const stringIndices = this.stringIndices
-      .slice(glyphStart - this.start, glyphEnd - this.start)
-      .map(index => index - this.stringIndices[glyphStart - this.start]);
-
     return new GlyphRun(start, end, this.attributes, glyphs, positions, stringIndices, true);
   }
 
diff --git a/src/models/GlyphString.js b/src/models/GlyphString.js
index a535cdf..da7d4a9 100644
--- a/src/models/GlyphString.js
+++ b/src/models/GlyphString.js
@@ -93,20 +93,19 @@ class GlyphString {
   }
 
   runIndexAtGlyphIndex(index) {
-    const idx = index + this.start;
     let count = 0;
 
-    for (let i = 0; i < this._glyphRuns.length; i++) {
-      const run = this._glyphRuns[i];
+    for (let i = 0; i < this.glyphRuns.length; i++) {
+      const run = this.glyphRuns[i];
 
-      if (count <= idx && idx < count + run.glyphs.length) {
+      if (count <= index && index < count + run.glyphs.length) {
         return i;
       }
 
       count += run.glyphs.length;
     }
 
-    return this._glyphRuns.length - 1;
+    return this.glyphRuns.length - 1;
   }
 
   runAtGlyphIndex(index) {
@@ -130,13 +129,41 @@ class GlyphString {
   }
 
   glyphAtIndex(index) {
-    const run = this.runAtGlyphIndex(index);
-    return run.glyphs[this.start + index - run.start];
+    let run;
+    let count = 0;
+
+    for (let i = 0; i < this.glyphRuns.length; i++) {
+      run = this.glyphRuns[i];
+
+      if (count <= index && index < count + run.glyphs.length) {
+        return run.glyphs[index - count];
+      }
+
+      count += run.glyphs.length;
+    }
+
+    return run.glyphs[run.glyphs.length - 1];
+  }
+
+  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) {
@@ -163,19 +190,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 (count <= index && index < count + run.glyphs.length) {
+        return offset + run.stringIndices[index - count];
+      }
+
+      offset += run.stringEnd + 1;
+      count += run.glyphs.length;
     }
 
-    return run.start - this.start + run.stringIndices[this.start + glyphIndex - run.start];
+    return 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 (count <= index && index < count + run.length) {
+        return offset + run.glyphIndices[index - count];
+      }
+
+      count += run.length;
+      offset += run.glyphs.length;
+    }
+
+    return run.glyphIndices[run.glyphIndices.length - 1];
   }
 
   codePointAtGlyphIndex(glyphIndex) {
diff --git a/test/models/GlyphString.test.js b/test/models/GlyphString.test.js
index d63c21c..0d76c90 100644
--- a/test/models/GlyphString.test.js
+++ b/test/models/GlyphString.test.js
@@ -51,7 +51,7 @@ describe('GlyphString', () => {
     expect(string.descent).toBeCloseTo(-3.5, 1);
   });
 
-  test('should get glyphs run', () => {
+  test('should get glyph runs', () => {
     const string = createLatinString({
       value: 'Lorem ipsum',
       runs: [[0, 6], [6, 11]]
@@ -64,21 +64,6 @@ describe('GlyphString', () => {
     expect(string.glyphRuns[1].end).toBe(11);
   });
 
-  test('should get glyphs run 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 (non latin)', () => {
     const string = createKhmerString({
       value: 'ខ្ញុំអាចញ៉ាំកញ្ចក់បាន',
@@ -92,10 +77,10 @@ describe('GlyphString', () => {
     expect(string.glyphRuns[1].end).toBe(21);
   });
 
-  test('should get glyphs run for sliced string (non latin)', () => {
-    const string = createKhmerString({
-      value: 'ខ្ញុំអាចញ៉ាំកញ្ចក់បាន',
-      runs: [[0, 8], [8, 21]]
+  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);
@@ -107,6 +92,21 @@ describe('GlyphString', () => {
     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(8);
+    expect(sliced.glyphRuns[1].start).toBe(8);
+    expect(sliced.glyphRuns[1].end).toBe(15);
+  });
+
   test('should isWhiteSpace return true if white space', () => {
     const string = createLatinString({ value: 'Lorem ipsum' });
 
@@ -126,7 +126,7 @@ describe('GlyphString', () => {
     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', () => {
@@ -136,7 +136,7 @@ describe('GlyphString', () => {
     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', () => {
@@ -175,20 +175,6 @@ describe('GlyphString', () => {
     expect(string.runIndexAtGlyphIndex(9)).toBe(1);
   });
 
-  test('should return correct run index at glyph index for sliced strings', () => {
-    const string = createLatinString({
-      value: 'Lorem ipsum',
-      runs: [[0, 6], [6, 11]]
-    });
-
-    const sliced = string.slice(4, 11);
-
-    expect(sliced.runIndexAtGlyphIndex(0)).toBe(0);
-    expect(sliced.runIndexAtGlyphIndex(1)).toBe(0);
-    expect(sliced.runIndexAtGlyphIndex(2)).toBe(1);
-    expect(sliced.runIndexAtGlyphIndex(9)).toBe(1);
-  });
-
   test('should return correct run index at glyph index (non latin)', () => {
     const string = createKhmerString({
       value: 'ខ្ញុំអាចញ៉ាំកញ្ចក់បាន',
@@ -203,7 +189,21 @@ describe('GlyphString', () => {
     expect(string.runIndexAtGlyphIndex(15)).toBe(1);
   });
 
-  test.skip('should return correct run index at glyph index for sliced strings (non latin)', () => {
+  test('should return correct run index at glyph index for sliced strings', () => {
+    const string = createLatinString({
+      value: 'Lorem ipsum',
+      runs: [[0, 6], [6, 11]]
+    });
+
+    const sliced = string.slice(4, 11);
+
+    expect(sliced.runIndexAtGlyphIndex(0)).toBe(0);
+    expect(sliced.runIndexAtGlyphIndex(1)).toBe(0);
+    expect(sliced.runIndexAtGlyphIndex(2)).toBe(1);
+    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]]
@@ -211,14 +211,10 @@ describe('GlyphString', () => {
 
     const sliced = string.slice(4, 11);
 
-    console.log(sliced);
-
     expect(sliced.runIndexAtGlyphIndex(0)).toBe(0);
-    expect(sliced.runIndexAtGlyphIndex(4)).toBe(0);
-    expect(sliced.runIndexAtGlyphIndex(6)).toBe(0);
-    expect(sliced.runIndexAtGlyphIndex(7)).toBe(1);
-    expect(sliced.runIndexAtGlyphIndex(11)).toBe(1);
-    expect(sliced.runIndexAtGlyphIndex(15)).toBe(1);
+    expect(sliced.runIndexAtGlyphIndex(3)).toBe(0);
+    expect(sliced.runIndexAtGlyphIndex(4)).toBe(1);
+    expect(sliced.runIndexAtGlyphIndex(6)).toBe(1);
   });
 
   test('should return correct run at glyph index', () => {
@@ -255,20 +251,6 @@ describe('GlyphString', () => {
     expect(string.runIndexAtStringIndex(9)).toBe(1);
   });
 
-  test('should return correct run index at string index for sliced strings', () => {
-    const string = createLatinString({
-      value: 'Lorem ipsum',
-      runs: [[0, 6], [6, 11]]
-    });
-
-    const sliced = string.slice(4, 11);
-
-    expect(sliced.runIndexAtStringIndex(0)).toBe(0);
-    expect(sliced.runIndexAtStringIndex(1)).toBe(0);
-    expect(sliced.runIndexAtStringIndex(2)).toBe(1);
-    expect(sliced.runIndexAtStringIndex(5)).toBe(1);
-  });
-
   test('should return correct run index at string index (non latin)', () => {
     const string = createKhmerString({
       value: 'ខ្ញុំអាចញ៉ាំកញ្ចក់បាន',
@@ -283,6 +265,20 @@ describe('GlyphString', () => {
     expect(string.runIndexAtStringIndex(20)).toBe(1);
   });
 
+  test('should return correct run index at string index for sliced strings', () => {
+    const string = createLatinString({
+      value: 'Lorem ipsum',
+      runs: [[0, 6], [6, 11]]
+    });
+
+    const sliced = string.slice(4, 11);
+
+    expect(sliced.runIndexAtStringIndex(0)).toBe(0);
+    expect(sliced.runIndexAtStringIndex(1)).toBe(0);
+    expect(sliced.runIndexAtStringIndex(2)).toBe(1);
+    expect(sliced.runIndexAtStringIndex(5)).toBe(1);
+  });
+
   test('should return correct run index at string index for sliced strings (non latin)', () => {
     const string = createKhmerString({
       value: 'ខ្ញុំអាចញ៉ាំកញ្ចក់បាន',
@@ -309,6 +305,18 @@ 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(8);
+    expect(string.runAtStringIndex(20).start).toBe(8);
+  });
+
   test('should return correct run at string index for sliced strings', () => {
     const string = createLatinString({
       value: 'Lorem ipsum',
@@ -323,16 +331,6 @@ describe('GlyphString', () => {
     expect(sliced.runAtStringIndex(5).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(9).start).toBe(8);
-  });
-
   test('should return correct run at string index for sliced strings (non latin)', () => {
     const string = createKhmerString({
       value: 'ខ្ញុំអាចញ៉ាំកញ្ចក់បាន',
@@ -343,6 +341,7 @@ describe('GlyphString', () => {
 
     expect(sliced.runAtStringIndex(0).start).toBe(0);
     expect(sliced.runAtStringIndex(1).start).toBe(0);
+    expect(sliced.runAtStringIndex(3).start).toBe(0);
     expect(sliced.runAtStringIndex(4).start).toBe(8);
     expect(sliced.runAtStringIndex(6).start).toBe(8);
   });
@@ -360,6 +359,21 @@ 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 = createLatinString({
       value: 'Lorem ipsum',
@@ -376,22 +390,7 @@ describe('GlyphString', () => {
     expect(sliced.glyphAtIndex(5).id).toBe(secondRunGlyphs[3].id);
   });
 
-  test.skip('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.skip('should return correct glyph at index for sliced strings (non latin)', () => {
+  test('should return correct glyph at index for sliced strings (non latin)', () => {
     const string = createKhmerString({
       value: 'ខ្ញុំអាចញ៉ាំកញ្ចក់បាន',
       runs: [[0, 8], [8, 21]]
@@ -401,10 +400,9 @@ describe('GlyphString', () => {
     const firstRunGlyphs = sliced._glyphRuns[0].glyphs;
     const secondRunGlyphs = sliced._glyphRuns[1].glyphs;
 
-    const runs = sliced.glyphRuns;
-    // expect(sliced.glyphAtIndex(0).id).toBe(firstRunGlyphs[3].id);
-    // expect(sliced.glyphAtIndex(3).id).toBe(firstRunGlyphs[6].id);
-    // expect(sliced.glyphAtIndex(4).id).toBe(secondRunGlyphs[0].id);
+    expect(sliced.glyphAtIndex(0).id).toBe(firstRunGlyphs[3].id);
+    expect(sliced.glyphAtIndex(3).id).toBe(firstRunGlyphs[6].id);
+    expect(sliced.glyphAtIndex(4).id).toBe(secondRunGlyphs[0].id);
   });
 
   test('should return correct glyph width at index', () => {
@@ -420,6 +418,21 @@ 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 = createLatinString({
       value: 'Lorem ipsum',
@@ -436,6 +449,21 @@ 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[3].xAdvance);
+    expect(sliced.getGlyphWidth(1)).toBe(firstRunPositions[6].xAdvance);
+    expect(sliced.getGlyphWidth(4)).toBe(secondRunPositions[0].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
@@ -450,6 +478,20 @@ 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 = createLatinString({
       value: 'Lorem ipsum',
@@ -468,6 +510,20 @@ 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(1);
+    expect(sliced.glyphIndexAtOffset(9)).toBe(1);
+    expect(sliced.glyphIndexAtOffset(14)).toBe(3);
+    expect(sliced.glyphIndexAtOffset(24)).toBe(4);
+  });
+
   test('should return correct string index for glyph index', () => {
     const string = createLatinString({
       value: 'Lorem ipsum',
@@ -480,6 +536,21 @@ 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 = createLatinString({
       value: 'Lorem ipsum',
@@ -493,6 +564,20 @@ 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, 15);
+
+    expect(sliced.stringIndexForGlyphIndex(0)).toBe(0);
+    expect(sliced.stringIndexForGlyphIndex(3)).toBe(3);
+    expect(sliced.stringIndexForGlyphIndex(4)).toBe(4);
+    expect(sliced.stringIndexForGlyphIndex(5)).toBe(8);
+  });
+
   test('should return correct glyph index for string index', () => {
     const string = createLatinString({
       value: 'Lorem ipsum',
@@ -505,17 +590,44 @@ 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 = 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', () => {

From fd8b93ebbef061d642a6bfcfd0aa144dee8ed84b Mon Sep 17 00:00:00 2001
From: Diego Muracciole <diegomuracciole@gmail.com>
Date: Mon, 26 Mar 2018 18:38:09 -0300
Subject: [PATCH 4/5] Fix models tests

---
 out.pdf                          | Bin 10520 -> 5787 bytes
 src/generators/GlyphGenerator.js |   2 +-
 src/models/GlyphRun.js           |  25 ++------
 src/models/GlyphString.js        |  98 ++++++++++++++---------------
 temp.js                          |   6 ++
 test/models/GlyphRun.test.js     |  66 +++++++++++++-------
 test/models/GlyphString.test.js  | 102 ++++++++++++++++++-------------
 test/utils/glyphRuns.js          |  15 +----
 test/utils/glyphStrings.js       |  30 ++++++---
 9 files changed, 185 insertions(+), 159 deletions(-)

diff --git a/out.pdf b/out.pdf
index 2c4104c8963dbd68852149d78122ec363f9d3179..992f9188fa56f5cf76ddca8a05c62f4366ea29a1 100644
GIT binary patch
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/models/GlyphRun.js b/src/models/GlyphRun.js
index 75f7313..d897ee2 100644
--- a/src/models/GlyphRun.js
+++ b/src/models/GlyphRun.js
@@ -87,28 +87,11 @@ class GlyphRun extends Run {
   }
 
   slice(start, end) {
-    const glyphStart = this.glyphIndices[start];
-    let glyphEnd = this.glyphIndices[end];
+    const glyphs = this.glyphs.slice(start, end);
+    const positions = this.positions.slice(start, end);
+    let stringIndices = this.stringIndices.slice(start, end);
 
-    if (glyphEnd === undefined) {
-      glyphEnd = this.glyphIndices[this.glyphIndices.length - 1] + 1;
-    }
-
-    let glyphs;
-    let positions;
-    let stringIndices;
-
-    if (glyphEnd === 0) {
-      glyphs = [this.glyphs[glyphStart]];
-      positions = [this.positions[glyphStart]];
-      stringIndices = [this.stringIndices[glyphStart]];
-    } else {
-      glyphs = this.glyphs.slice(glyphStart, glyphEnd);
-      positions = this.positions.slice(glyphStart, glyphEnd);
-      stringIndices = this.stringIndices.slice(glyphStart, glyphEnd);
-    }
-
-    stringIndices = stringIndices.map(index => index - this.stringIndices[glyphStart]);
+    stringIndices = stringIndices.map(index => index - this.stringIndices[start]);
 
     start += this.start;
     end += this.start;
diff --git a/src/models/GlyphString.js b/src/models/GlyphString.js
index da7d4a9..ec06cee 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() {
@@ -67,8 +65,8 @@ class GlyphString {
       return this._glyphRunsCache;
     }
 
-    const startRunIndex = this.runIndexAtStringIndex(0);
-    const endRunIndex = this.runIndexAtStringIndex(this.length);
+    const startRunIndex = this.runIndexAtGlyphIndex(0);
+    const endRunIndex = this.runIndexAtGlyphIndex(this.length);
     const startRun = this._glyphRuns[startRunIndex];
     const endRun = this._glyphRuns[endRunIndex];
     const runs = [];
@@ -89,14 +87,23 @@ 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) {
+    index += this.start;
     let count = 0;
 
-    for (let i = 0; i < this.glyphRuns.length; i++) {
-      const run = this.glyphRuns[i];
+    for (let i = 0; i < this._glyphRuns.length; i++) {
+      const run = this._glyphRuns[i];
 
       if (count <= index && index < count + run.glyphs.length) {
         return i;
@@ -105,44 +112,36 @@ class GlyphString {
       count += run.glyphs.length;
     }
 
-    return this.glyphRuns.length - 1;
+    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].start + this.start;
+    let offset = 0;
 
-    for (let i = 0; i < this._glyphRuns.length; i++) {
-      if (this._glyphRuns[i].start <= idx && idx < this._glyphRuns[i].end) {
+    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) {
-    let run;
-    let count = 0;
-
-    for (let i = 0; i < this.glyphRuns.length; i++) {
-      run = this.glyphRuns[i];
-
-      if (count <= index && index < count + run.glyphs.length) {
-        return run.glyphs[index - count];
-      }
-
-      count += run.glyphs.length;
-    }
-
-    return run.glyphs[run.glyphs.length - 1];
+    const run = this.runAtGlyphIndex(index);
+    return run.glyphs[this.start + index - run.start];
   }
 
   positionAtIndex(index) {
@@ -198,15 +197,15 @@ class GlyphString {
     for (let i = 0; i < this.glyphRuns.length; i++) {
       run = this.glyphRuns[i];
 
-      if (count <= index && index < count + run.glyphs.length) {
-        return offset + run.stringIndices[index - count];
+      if (offset <= index && offset + run.length > index) {
+        return count + run.stringIndices[index + this.start - run.start];
       }
 
-      offset += run.stringEnd + 1;
-      count += run.glyphs.length;
+      offset += run.length;
+      count += run.stringEnd + 1;
     }
 
-    return run.stringIndices[run.stringIndices.length - 1];
+    return count + run.stringIndices[run.stringIndices.length - 1];
   }
 
   glyphIndexForStringIndex(index) {
@@ -217,15 +216,15 @@ class GlyphString {
     for (let i = 0; i < this.glyphRuns.length; i++) {
       run = this.glyphRuns[i];
 
-      if (count <= index && index < count + run.length) {
-        return offset + run.glyphIndices[index - count];
+      if (offset <= index && index < offset + run.stringEnd + 1) {
+        return count + run.glyphIndices[index - offset];
       }
 
-      count += run.length;
-      offset += run.glyphs.length;
+      count += run.glyphs.length;
+      offset += run.stringEnd + 1;
     }
 
-    return run.glyphIndices[run.glyphIndices.length - 1];
+    return offset;
   }
 
   codePointAtGlyphIndex(glyphIndex) {
@@ -291,8 +290,11 @@ class GlyphString {
     const glyph = run.attributes.font.glyphForCodePoint(codePoint);
     const idx = this.start + index - run.start;
 
+    if (this._end) {
+      this._end += 1;
+    }
+
     run.glyphs.splice(idx, 0, glyph);
-    run.glyphIndices.splice(idx, 0, run.glyphIndices[idx]);
     run.stringIndices.splice(idx, 0, run.stringIndices[idx]);
     run.positions.splice(idx, 0, {
       xAdvance: glyph.advanceWidth,
@@ -308,10 +310,6 @@ class GlyphString {
       this._glyphRuns[i].end++;
     }
 
-    if (this._end != null) {
-      this._end++;
-    }
-
     this._glyphRunsCache = null;
   }
 
@@ -336,10 +334,6 @@ class GlyphString {
       this._glyphRuns[i].end--;
     }
 
-    if (this._end != null) {
-      this._end--;
-    }
-
     this._glyphRunsCache = null;
   }
 }
diff --git a/temp.js b/temp.js
index 2eba6da..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';
@@ -58,6 +59,11 @@ const string = AttributedString.fromFragments([
   }
 ]);
 
+// 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/models/GlyphRun.test.js b/test/models/GlyphRun.test.js
index 3d52965..42eabb2 100644
--- a/test/models/GlyphRun.test.js
+++ b/test/models/GlyphRun.test.js
@@ -18,6 +18,30 @@ describe('GlyphRun', () => {
     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 } = createLatinRun();
     const scale = attrs.fontSize / openSans.unitsPerEm;
@@ -74,7 +98,7 @@ describe('GlyphRun', () => {
   });
 
   test('should exact slice range return same run', () => {
-    const { glyphRun } = createLatinRun({ start: 0, end: 11 });
+    const { glyphRun } = createLatinRun({ start: 0 });
     const sliced = glyphRun.slice(0, 11);
 
     expect(sliced.start).toBe(0);
@@ -82,7 +106,7 @@ describe('GlyphRun', () => {
   });
 
   test('should slice containing range', () => {
-    const { glyphRun } = createLatinRun({ start: 0, end: 11 });
+    const { glyphRun } = createLatinRun({ start: 0 });
     const sliced = glyphRun.slice(2, 5);
 
     expect(sliced.start).toBe(2);
@@ -90,7 +114,7 @@ describe('GlyphRun', () => {
   });
 
   test('should slice exceeding range', () => {
-    const { glyphRun } = createLatinRun({ start: 0, end: 11 });
+    const { glyphRun } = createLatinRun({ start: 0 });
     const sliced = glyphRun.slice(2, 20);
 
     expect(sliced.start).toBe(2);
@@ -98,7 +122,7 @@ describe('GlyphRun', () => {
   });
 
   test('should slice containing range when start not zero', () => {
-    const { glyphRun } = createLatinRun({ start: 5, end: 11 });
+    const { glyphRun } = createLatinRun({ start: 5 });
     const sliced = glyphRun.slice(2, 5);
 
     expect(sliced.start).toBe(7);
@@ -106,7 +130,7 @@ describe('GlyphRun', () => {
   });
 
   test('should slice exceeding range when start not zero', () => {
-    const { glyphRun } = createLatinRun({ start: 5, end: 11 });
+    const { glyphRun } = createLatinRun({ start: 5 });
     const sliced = glyphRun.slice(2, 20);
 
     expect(sliced.start).toBe(7);
@@ -116,30 +140,30 @@ describe('GlyphRun', () => {
   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, end: 11 });
+    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, end: 21 });
+    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]);
+    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, end: 11 });
+    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, end: 21 });
+    const { glyphRun } = createKhmerRun({ value: 'ខ្ញុំអាចញ៉ាំកញ្ចក់បាន', start: 0 });
     const { glyphs } = glyphRun.slice(0, 21);
 
     expect(glyphs.map(g => g.id)).toEqual([
@@ -165,7 +189,7 @@ describe('GlyphRun', () => {
   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, end: 11 });
+    const { glyphRun } = createLatinRun({ start: 0 });
     const sliced = glyphRun.slice(2, 8);
     const positions = sliced.positions.map(p => round(p.xAdvance));
 
@@ -173,17 +197,17 @@ describe('GlyphRun', () => {
   });
 
   test('should correctly slice positions (non latin)', () => {
-    const { glyphRun } = createKhmerRun({ value: 'ខ្ញុំអាចញ៉ាំកញ្ចក់បាន', start: 0, end: 21 });
+    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]);
+    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, end: 11 });
+    const { glyphRun } = createLatinRun({ start: 0 });
     const sliced = glyphRun.slice(0, 11);
     const positions = sliced.positions.map(p => round(p.xAdvance));
 
@@ -191,7 +215,7 @@ describe('GlyphRun', () => {
   });
 
   test('should exact slice return same positions (non latin)', () => {
-    const { glyphRun } = createKhmerRun({ value: 'ខ្ញុំអាចញ៉ាំកញ្ចក់បាន', start: 0, end: 21 });
+    const { glyphRun } = createKhmerRun({ value: 'ខ្ញុំអាចញ៉ាំកញ្ចក់បាន', start: 0 });
     const sliced = glyphRun.slice(0, 21);
     const positions = sliced.positions.map(p => round(p.xAdvance));
 
@@ -216,35 +240,35 @@ describe('GlyphRun', () => {
   });
 
   test('should correctly slice string indices', () => {
-    const { glyphRun } = createLatinRun({ start: 0, end: 11 });
+    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, end: 21 });
+    const { glyphRun } = createKhmerRun({ value: 'ខ្ញុំអាចញ៉ាំកញ្ចក់បាន', start: 0 });
     const { stringIndices } = glyphRun.slice(1, 8);
 
-    expect(stringIndices).toEqual([0, 2, 3, 4, 5, 6]);
+    expect(stringIndices).toEqual([0, 2, 3, 4, 5, 6, 7]);
   });
 
   test('should exact slice return same string indices', () => {
-    const { glyphRun } = createLatinRun({ start: 0, end: 11 });
+    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, end: 21 });
+    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, end: 11 });
+    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 0d76c90..71bbac8 100644
--- a/test/models/GlyphString.test.js
+++ b/test/models/GlyphString.test.js
@@ -21,12 +21,24 @@ describe('GlyphString', () => {
     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 = 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 = createLatinString({ value: 'Lorem ipsum' });
 
@@ -72,9 +84,9 @@ describe('GlyphString', () => {
 
     expect(string.glyphRuns).toHaveLength(2);
     expect(string.glyphRuns[0].start).toBe(0);
-    expect(string.glyphRuns[0].end).toBe(8);
-    expect(string.glyphRuns[1].start).toBe(8);
-    expect(string.glyphRuns[1].end).toBe(21);
+    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', () => {
@@ -102,8 +114,8 @@ describe('GlyphString', () => {
 
     expect(sliced.glyphRuns).toHaveLength(2);
     expect(sliced.glyphRuns[0].start).toBe(1);
-    expect(sliced.glyphRuns[0].end).toBe(8);
-    expect(sliced.glyphRuns[1].start).toBe(8);
+    expect(sliced.glyphRuns[0].end).toBe(7);
+    expect(sliced.glyphRuns[1].start).toBe(7);
     expect(sliced.glyphRuns[1].end).toBe(15);
   });
 
@@ -212,8 +224,8 @@ describe('GlyphString', () => {
     const sliced = string.slice(4, 11);
 
     expect(sliced.runIndexAtGlyphIndex(0)).toBe(0);
-    expect(sliced.runIndexAtGlyphIndex(3)).toBe(0);
-    expect(sliced.runIndexAtGlyphIndex(4)).toBe(1);
+    expect(sliced.runIndexAtGlyphIndex(2)).toBe(0);
+    expect(sliced.runIndexAtGlyphIndex(3)).toBe(1);
     expect(sliced.runIndexAtGlyphIndex(6)).toBe(1);
   });
 
@@ -235,8 +247,8 @@ describe('GlyphString', () => {
 
     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);
   });
@@ -248,6 +260,8 @@ describe('GlyphString', () => {
     });
 
     expect(string.runIndexAtStringIndex(2)).toBe(0);
+    expect(string.runIndexAtStringIndex(5)).toBe(0);
+    expect(string.runIndexAtStringIndex(6)).toBe(1);
     expect(string.runIndexAtStringIndex(9)).toBe(1);
   });
 
@@ -289,8 +303,7 @@ describe('GlyphString', () => {
 
     expect(sliced.runIndexAtStringIndex(0)).toBe(0);
     expect(sliced.runIndexAtStringIndex(2)).toBe(0);
-    expect(sliced.runIndexAtStringIndex(3)).toBe(0);
-    expect(sliced.runIndexAtStringIndex(4)).toBe(1);
+    expect(sliced.runIndexAtStringIndex(3)).toBe(1);
     expect(sliced.runIndexAtStringIndex(5)).toBe(1);
     expect(sliced.runIndexAtStringIndex(6)).toBe(1);
   });
@@ -313,8 +326,8 @@ describe('GlyphString', () => {
 
     expect(string.runAtStringIndex(2).start).toBe(0);
     expect(string.runAtStringIndex(7).start).toBe(0);
-    expect(string.runAtStringIndex(8).start).toBe(8);
-    expect(string.runAtStringIndex(20).start).toBe(8);
+    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', () => {
@@ -325,8 +338,8 @@ describe('GlyphString', () => {
 
     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);
   });
@@ -339,11 +352,11 @@ describe('GlyphString', () => {
 
     const sliced = string.slice(4, 11);
 
-    expect(sliced.runAtStringIndex(0).start).toBe(0);
-    expect(sliced.runAtStringIndex(1).start).toBe(0);
-    expect(sliced.runAtStringIndex(3).start).toBe(0);
-    expect(sliced.runAtStringIndex(4).start).toBe(8);
-    expect(sliced.runAtStringIndex(6).start).toBe(8);
+    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', () => {
@@ -400,9 +413,9 @@ describe('GlyphString', () => {
     const firstRunGlyphs = sliced._glyphRuns[0].glyphs;
     const secondRunGlyphs = sliced._glyphRuns[1].glyphs;
 
-    expect(sliced.glyphAtIndex(0).id).toBe(firstRunGlyphs[3].id);
-    expect(sliced.glyphAtIndex(3).id).toBe(firstRunGlyphs[6].id);
-    expect(sliced.glyphAtIndex(4).id).toBe(secondRunGlyphs[0].id);
+    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', () => {
@@ -459,9 +472,10 @@ describe('GlyphString', () => {
     const firstRunPositions = sliced._glyphRuns[0].positions;
     const secondRunPositions = sliced._glyphRuns[1].positions;
 
-    expect(sliced.getGlyphWidth(0)).toBe(firstRunPositions[3].xAdvance);
-    expect(sliced.getGlyphWidth(1)).toBe(firstRunPositions[6].xAdvance);
-    expect(sliced.getGlyphWidth(4)).toBe(secondRunPositions[0].xAdvance);
+    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', () => {
@@ -518,10 +532,12 @@ describe('GlyphString', () => {
 
     const sliced = string.slice(4, 11);
 
-    expect(sliced.glyphIndexAtOffset(0)).toBe(1);
-    expect(sliced.glyphIndexAtOffset(9)).toBe(1);
-    expect(sliced.glyphIndexAtOffset(14)).toBe(3);
-    expect(sliced.glyphIndexAtOffset(24)).toBe(4);
+    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', () => {
@@ -570,12 +586,14 @@ describe('GlyphString', () => {
       runs: [[0, 8], [8, 21]]
     });
 
-    const sliced = string.slice(4, 15);
+    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(4);
+    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', () => {
@@ -625,9 +643,9 @@ describe('GlyphString', () => {
 
     const sliced = string.slice(4, 15);
 
-    // expect(sliced.glyphIndexForStringIndex(0)).toBe(0);
-    // expect(sliced.glyphIndexForStringIndex(3)).toBe(3);
-    // expect(sliced.glyphIndexForStringIndex(4)).toBe(4);
+    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', () => {
@@ -775,16 +793,16 @@ describe('GlyphString', () => {
 
     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);
+    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);
+    expect(string.glyphRuns[0].stringIndices[2]).toBe(2);
+    expect(string.glyphRuns[0].stringIndices[3]).toBe(2);
   });
 
   test('should successfully insert glyph for sliced strings', () => {
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 f24be4b..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
+    );
+
+    glyphIndex += glyphs.length;
+
+    return glyphRun;
+  });
 
   return new GlyphString(value, glyphRuns, 0, value.length);
 };

From 346d07fc57fb07f1a49b60e82090ca3b6abed48c Mon Sep 17 00:00:00 2001
From: Diego Muracciole <diegomuracciole@gmail.com>
Date: Tue, 27 Mar 2018 03:41:43 -0300
Subject: [PATCH 5/5] Fix LineFragment

---
 out.pdf                    | Bin 5787 -> 32011 bytes
 src/models/GlyphString.js  |  11 ++++++-----
 src/models/LineFragment.js |   7 +------
 3 files changed, 7 insertions(+), 11 deletions(-)

diff --git a/out.pdf b/out.pdf
index 992f9188fa56f5cf76ddca8a05c62f4366ea29a1..abb1fdad77a0af50f3d76d091314e73b26430b93 100644
GIT binary patch
diff --git a/src/models/GlyphString.js b/src/models/GlyphString.js
index ec06cee..83c4e76 100644
--- a/src/models/GlyphString.js
+++ b/src/models/GlyphString.js
@@ -237,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;
       }
     }
 
@@ -303,11 +304,11 @@ class GlyphString {
       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++;
+      this._glyphRuns[i].start += 1;
+      this._glyphRuns[i].end += 1;
     }
 
     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;
