Skip to content

Commit

Permalink
feat: exclude rects on textkit layout container (#1895)
Browse files Browse the repository at this point in the history
  • Loading branch information
diegomura committed Jun 20, 2022
1 parent 9527fe4 commit 37a9a74
Show file tree
Hide file tree
Showing 8 changed files with 246 additions and 8 deletions.
5 changes: 5 additions & 0 deletions .changeset/long-years-cry.md
@@ -0,0 +1,5 @@
---
'@react-pdf/textkit': minor
---

feat: exclude rects on textkit layout container
54 changes: 54 additions & 0 deletions packages/textkit/src/layout/generateLineRects.js
@@ -0,0 +1,54 @@
import intersects from '../rect/intersects';
import partition from '../rect/partition';

const getLineFragment = (lineRect, excludeRect) => {
if (!intersects(excludeRect, lineRect)) return [lineRect];

const eStart = excludeRect.x;
const eEnd = excludeRect.x + excludeRect.width;
const lStart = lineRect.x;
const lEnd = lineRect.x + lineRect.width;

const a = Object.assign({}, lineRect, { width: eStart - lStart });
const b = Object.assign({}, lineRect, { x: eEnd, width: lEnd - eEnd });

return [a, b].filter(r => r.width > 0);
};

const getLineFragments = (rect, excludeRects) => {
let fragments = [rect];

for (let i = 0; i < excludeRects.length; i += 1) {
const excludeRect = excludeRects[i];

fragments = fragments.reduce((acc, fragment) => {
const pieces = getLineFragment(fragment, excludeRect);
return acc.concat(pieces);
}, []);
}

return fragments;
};

const generateLineRects = (container, height) => {
const { excludeRects, ...rect } = container;

if (!excludeRects) return [rect];

const lineRects = [];
const maxY = Math.max(...excludeRects.map(r => r.y + r.height));

let currentRect = rect;

while (currentRect.y < maxY) {
const [lineRect, rest] = partition(currentRect, height);
const lineRectFragments = getLineFragments(lineRect, excludeRects);

currentRect = rest;
lineRects.push(...lineRectFragments);
}

return [...lineRects, currentRect];
};

export default generateLineRects;
20 changes: 16 additions & 4 deletions packages/textkit/src/layout/layoutParagraph.js
@@ -1,5 +1,6 @@
import omit from '../run/omit';
import stringHeight from '../attributedString/height';
import generateLineRects from './generateLineRects';

const ATTACHMENT_CODE = '\ufffc'; // 65532

Expand All @@ -26,14 +27,20 @@ const purgeAttachments = attributedString => {
* @param {Array} attributed strings
* @return {Object} layout blocks
*/
const layoutLines = (rect, lines, indent) => {
const layoutLines = (rects, lines, indent) => {
let rect = rects.shift();
let currentY = rect.y;

return lines.map((line, i) => {
const lineIndent = i === 0 ? indent : 0;
const style = line.runs?.[0]?.attributes || {};
const height = Math.max(stringHeight(line), style.lineHeight);

if (currentY + height > rect.y + rect.height) {
rect = rects.shift();
currentY = rect.y;
}

const newLine = Object.assign({}, line);

delete newLine.syllables;
Expand All @@ -60,12 +67,17 @@ const layoutLines = (rect, lines, indent) => {
* @param {Object} attributed string
* @return {Object} layout block
*/
const layoutParagraph = (engines, options) => (rect, paragraph) => {
const layoutParagraph = (engines, options) => (container, paragraph) => {
const height = stringHeight(paragraph);
const indent = paragraph.runs?.[0]?.attributes?.indent || 0;
const availableWidths = [rect.width - indent, rect.width];
const rects = generateLineRects(container, height);

const availableWidths = rects.map(r => r.width);
availableWidths[0] -= indent;

const lines = engines.linebreaker(options)(paragraph, availableWidths);

return layoutLines(rect, lines, indent);
return layoutLines(rects, lines, indent);
};

export default layoutParagraph;
8 changes: 4 additions & 4 deletions packages/textkit/src/rect/crop.js
@@ -1,14 +1,14 @@
import partition from './partition';

/**
* Crop upper section of rect
*
* @param {Object} rect
* @return {Object} cropped rect
*/
const crop = (height, rect) => {
const y = rect.y + height;
const h = rect.height - height;

return Object.assign({}, rect, { y, height: h });
const [, result] = partition(rect, height);
return result;
};

export default crop;
17 changes: 17 additions & 0 deletions packages/textkit/src/rect/intersects.js
@@ -0,0 +1,17 @@
/**
* Checks if two rects intersect each other
*
* @param {Rect} a
* @param {Rect} b
* @returns {Boolean} rects intersects
*/
const intersects = (a, b) => {
const x = Math.max(a.x, b.x);
const num1 = Math.min(a.x + a.width, b.x + b.width);
const y = Math.max(a.y, b.y);
const num2 = Math.min(a.y + a.height, b.y + b.height);

return num1 >= x && num2 >= y;
};

export default intersects;
12 changes: 12 additions & 0 deletions packages/textkit/src/rect/partition.js
@@ -0,0 +1,12 @@
const partition = (rect, height) => {
const a = Object.assign({}, rect, { height });

const b = Object.assign({}, rect, {
y: rect.y + height,
height: rect.height - height,
});

return [a, b];
};

export default partition;
115 changes: 115 additions & 0 deletions packages/textkit/tests/rect/intersects.test.js
@@ -0,0 +1,115 @@
import intersects from '../../src/rect/intersects';

describe('rect intersects operator', () => {
test('should not intesect on top-left corner', () => {
const a = { x: 50, y: 50, width: 90, height: 110 };
const b = { x: 0, y: 0, width: 30, height: 30 };

expect(intersects(a, b)).toEqual(false);
});

test('should intesect on top-left corner', () => {
const a = { x: 50, y: 50, width: 90, height: 110 };
const b = { x: 0, y: 0, width: 60, height: 60 };

expect(intersects(a, b)).toEqual(true);
});

test('should not intesect on top edge', () => {
const a = { x: 50, y: 50, width: 90, height: 110 };
const b = { x: 50, y: 0, width: 30, height: 30 };

expect(intersects(a, b)).toEqual(false);
});

test('should intesect on top edge', () => {
const a = { x: 50, y: 50, width: 90, height: 110 };
const b = { x: 50, y: 0, width: 30, height: 60 };

expect(intersects(a, b)).toEqual(true);
});

test('should not intesect on top-right corner', () => {
const a = { x: 50, y: 50, width: 90, height: 110 };
const b = { x: 150, y: 0, width: 50, height: 50 };

expect(intersects(a, b)).toEqual(false);
});

test('should intesect on top-right corner', () => {
const a = { x: 50, y: 50, width: 90, height: 110 };
const b = { x: 70, y: 0, width: 50, height: 50 };

expect(intersects(a, b)).toEqual(true);
});

test('should not intesect on top edge', () => {
const a = { x: 50, y: 50, width: 90, height: 110 };
const b = { x: 150, y: 40, width: 30, height: 30 };

expect(intersects(a, b)).toEqual(false);
});

test('should intesect on top edge', () => {
const a = { x: 50, y: 50, width: 90, height: 110 };
const b = { x: 130, y: 40, width: 30, height: 30 };

expect(intersects(a, b)).toEqual(true);
});

test('should not intesect on bottom-right corner', () => {
const a = { x: 50, y: 50, width: 90, height: 110 };
const b = { x: 150, y: 170, width: 50, height: 50 };

expect(intersects(a, b)).toEqual(false);
});

test('should intesect on bottom-right corner', () => {
const a = { x: 50, y: 50, width: 90, height: 110 };
const b = { x: 70, y: 120, width: 50, height: 50 };

expect(intersects(a, b)).toEqual(true);
});

test('should not intesect on bottom edge', () => {
const a = { x: 50, y: 50, width: 90, height: 110 };
const b = { x: 70, y: 180, width: 30, height: 30 };

expect(intersects(a, b)).toEqual(false);
});

test('should intesect on bottom edge', () => {
const a = { x: 50, y: 50, width: 90, height: 110 };
const b = { x: 70, y: 100, width: 30, height: 30 };

expect(intersects(a, b)).toEqual(true);
});

test('should not intesect on bottom-left corner', () => {
const a = { x: 50, y: 50, width: 90, height: 110 };
const b = { x: 0, y: 170, width: 50, height: 50 };

expect(intersects(a, b)).toEqual(false);
});

test('should intesect on bottom-left corner', () => {
const a = { x: 50, y: 50, width: 90, height: 110 };
const b = { x: 0, y: 120, width: 50, height: 50 };

expect(intersects(a, b)).toEqual(true);
});

test('should not intesect on left edge', () => {
const a = { x: 50, y: 50, width: 90, height: 110 };
const b = { x: 0, y: 60, width: 30, height: 30 };

expect(intersects(a, b)).toEqual(false);
});

test('should intesect on left edge', () => {
const a = { x: 50, y: 50, width: 90, height: 110 };
const b = { x: 30, y: 60, width: 30, height: 30 };

expect(intersects(a, b)).toEqual(true);
});
});
23 changes: 23 additions & 0 deletions packages/textkit/tests/rect/partition.test.js
@@ -0,0 +1,23 @@
import partition from '../../src/rect/partition';

describe('rect partition operator', () => {
test('should return empty rect if height 0', () => {
const target = { x: 10, y: 10, width: 90, height: 110 };
const result = partition(target, 0);

expect(result).toEqual([
{ x: 10, y: 10, width: 90, height: 0 },
{ x: 10, y: 10, width: 90, height: 110 },
]);
});

test('should return correct partition', () => {
const target = { x: 10, y: 10, width: 90, height: 110 };
const result = partition(target, 20);

expect(result).toEqual([
{ x: 10, y: 10, width: 90, height: 20 },
{ x: 10, y: 30, width: 90, height: 90 },
]);
});
});

0 comments on commit 37a9a74

Please sign in to comment.