Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
- Fix garbled text copying in Chrome/Edge for PDFs with >256 unique characters (#1659)
- Fix Link accessibility issues
- Fix Table Accessibility Issue: Operator CS/cs not allowed in this current state
- Add pageLayout option to control how pages are displayed in PDF viewers
- Fix Interlaced PNG with indexed transparency rendered incorrectly
- Fix SVG path parser incorrectly handle arc flags without separators
- Add pageLayout option to control how pages are displayed in PDF viewers
- Preserve existing PageMode instead of overwriting when adding outlines
- Support outlines that jump to specific page positions with custom zoom level

Expand Down
240 changes: 171 additions & 69 deletions lib/path.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,89 +25,191 @@ const parameters = {
z: 0,
};

const parse = function (path) {
let cmd;
const ret = [];
let args = [];
let curArg = '';
let foundDecimal = false;
let params = 0;

for (let c of path) {
if (parameters[c] != null) {
params = parameters[c];
if (cmd) {
// save existing command
if (curArg.length > 0) {
args[args.length] = +curArg;
}
ret[ret.length] = { cmd, args };
const isCommand = function (c) {
return c in parameters;
};

args = [];
curArg = '';
foundDecimal = false;
}
const isWsp = function (c) {
const codePoint = c.codePointAt(0);
return (
codePoint === 0x20 ||
codePoint === 0x9 ||
codePoint === 0xd ||
codePoint === 0xa
);
};

const isDigit = function (c) {
const codePoint = c.codePointAt(0);
if (codePoint == null) {
return false;
}
return 48 <= codePoint && codePoint <= 57;
};

cmd = c;
} else if (
[' ', ','].includes(c) ||
(c === '-' && curArg.length > 0 && curArg[curArg.length - 1] !== 'e') ||
(c === '.' && foundDecimal)
) {
if (curArg.length === 0) {
const readNumber = function (string, cursor) {
let i = cursor;
let value = '';
let state = 'none';
for (; i < string.length; i += 1) {
const c = string[i];
if (c === '+' || c === '-') {
if (state === 'none') {
state = 'sign';
value += c;
continue;
}
if (state === 'e') {
state = 'exponent_sign';
value += c;
continue;
}
}
if (isDigit(c)) {
if (state === 'none' || state === 'sign' || state === 'whole') {
state = 'whole';
value += c;
continue;
}
if (state === 'decimal_point' || state === 'decimal') {
state = 'decimal';
value += c;
continue;
}
if (state === 'e' || state === 'exponent_sign' || state === 'exponent') {
state = 'exponent';
value += c;
continue;
}
}
if (c === '.') {
if (state === 'none' || state === 'sign' || state === 'whole') {
state = 'decimal_point';
value += c;
continue;
}
}
if (c === 'E' || c === 'e') {
if (
state === 'whole' ||
state === 'decimal_point' ||
state === 'decimal'
) {
state = 'e';
value += c;
continue;
}
}
break;
}
const number = Number.parseFloat(value);
if (Number.isNaN(number)) {
return [cursor, null];
}
// step back to delegate iteration to parent loop
return [i - 1, number];
};

if (args.length === params) {
// handle reused commands
ret[ret.length] = { cmd, args };
args = [+curArg];
// parse is based on the path parser from SVGO
// https://github.com/svg/svgo/blob/main/lib/path.js
// License: MIT

// handle assumed commands
if (cmd === 'M') {
cmd = 'L';
}
if (cmd === 'm') {
cmd = 'l';
const parse = function (path) {
const pathData = [];
let command = null;
let args = [];
let argsCount = 0;
let canHaveComma = false;
let hadComma = false;
for (let i = 0; i < path.length; i += 1) {
const c = path.charAt(i);
if (isWsp(c)) {
continue;
}
// allow comma only between arguments
if (canHaveComma && c === ',') {
if (hadComma) {
break;
}
hadComma = true;
continue;
}
if (isCommand(c)) {
if (hadComma) {
return pathData;
}
if (command == null) {
// moveto should be leading command
if (c !== 'M' && c !== 'm') {
return pathData;
}
} else {
args[args.length] = +curArg;
// stop if previous command arguments are not flushed
if (args.length !== 0) {
return pathData;
}
}

foundDecimal = c === '.';

// fix for negative numbers or repeated decimals with no delimeter between commands
curArg = ['-', '.'].includes(c) ? c : '';
} else {
curArg += c;
if (c === '.') {
foundDecimal = true;
command = c;
args = [];
argsCount = parameters[command];
canHaveComma = false;
// flush command without arguments
if (argsCount === 0) {
pathData.push({ command, args });
}
continue;
}
}

// add the last command
if (curArg.length > 0) {
if (args.length === params) {
// handle reused commands
ret[ret.length] = { cmd, args };
args = [+curArg];

// handle assumed commands
if (cmd === 'M') {
cmd = 'L';
// avoid parsing arguments if no command detected
if (command == null) {
return pathData;
}
// read next argument
let newCursor = i;
let number = null;
if (command === 'A' || command === 'a') {
const position = args.length;
if (position === 0 || position === 1) {
// allow only positive number without sign as first two arguments
if (c !== '+' && c !== '-') {
[newCursor, number] = readNumber(path, i);
}
}
if (cmd === 'm') {
cmd = 'l';
if (position === 2 || position === 5 || position === 6) {
[newCursor, number] = readNumber(path, i);
}
if (position === 3 || position === 4) {
// read flags
if (c === '0') {
number = 0;
}
if (c === '1') {
number = 1;
}
}
} else {
args[args.length] = +curArg;
[newCursor, number] = readNumber(path, i);
}
if (number == null) {
return pathData;
}
args.push(number);
canHaveComma = true;
hadComma = false;
i = newCursor;
// flush arguments when necessary count is reached
if (args.length === argsCount) {
pathData.push({ command, args });
// subsequent moveto coordinates are treated as implicit lineto commands
if (command === 'M') {
command = 'L';
}
if (command === 'm') {
command = 'l';
}
args = [];
}
}

ret[ret.length] = { cmd, args };

return ret;
return pathData;
};

const apply = function (commands, doc) {
Expand All @@ -117,8 +219,8 @@ const apply = function (commands, doc) {
// run the commands
for (let i = 0; i < commands.length; i++) {
const c = commands[i];
if (typeof runners[c.cmd] === 'function') {
runners[c.cmd](doc, c.args);
if (typeof runners[c.command] === 'function') {
runners[c.command](doc, c.args);
}
}
};
Expand Down
Loading