// Copyright (c) 2017, Shellyl_N and Authors
// license: ISC
// https://github.com/shellyln
import { TextEncoding } from "red-agate-util/modules/convert/TextEncoding";
import { Base64 } from "red-agate-util/modules/convert/Base64";
import { Escape } from "red-agate-util/modules/convert/Escape";
import { WordWrap } from "red-agate-util/modules/convert/WordWrap";
import { WebColor } from "./WebColor";
import { VectorCanvas2DGradient,
VectorCanvas2DPattern,
VectorCanvas2D } from "./VectorCanvas2D";
import { Point2D,
Vector2D,
Rect2D,
TransferMatrix2D } from "./TransferMatrix2D";
export interface SvgCanvas2DAsset {
toDef(): string;
}
export interface SvgTextAttributes {
/**
* author's computation of the total sum of all of the advance values
* that correspond to character data within this element,
* including the advance value on the glyph
*/
textLength?: number;
/** 'spacing' | 'spacingAndGlyphs' */
lengthAdjust?: "spacing" | "spacingAndGlyphs";
/** line break on newline(0x0a) */
multiline?: boolean;
/**
* length from baseline to next baseline.
* if writingMode is horizontal then add y coodinate.
* if writingMode is vertical then add x coodinate.
*/
lineHeight?: number;
/** rotation about the current text position (degree) */
rotate?: number;
/** 'lr-tb' | 'rl-tb' | 'tb-rl' | 'lr' | 'rl' | 'tb' */
writingMode?: "lr-tb" | "rl-tb" | "tb-rl" | "lr" | "rl" | "tb";
/** 'auto' | 0 | 90 | 180 | 270 */
glyphOrientationVertical?: "auto" | number;
/** 0 | 90 | 180 | 270 */
glyphOrientationHorizontal?: number;
/** 'ltr' | 'rtl' */
direction?: "ltr" | "rtl";
/** 'normal' | 'embed' | 'bidi-override' */
unicodeBidi?: "normal" | "embed" | "bidi-override";
/** 'none' | [ 'underline' || 'overline' || 'line-through' ] */
textDecoration?: string;
/** use textPath */
textPath?: boolean;
/** */
textPathOffset?: number;
/** 'auto' | length */
kerning?: "auto" | number;
/** 'normal' | length */
letterSpacing?: "normal" | number;
/** 'normal' | length */
wordSpacing?: "normal" | number;
}
export class SvgCanvasImageData {
constructor(
public url: string,
public x: number, public y: number,
public width: number, public height: number) {
}
}
export abstract class SvgCanvas2DGradient {
}
export class SvgCanvas2DLinerGradient extends SvgCanvas2DGradient implements VectorCanvas2DGradient, SvgCanvas2DAsset {
private content: string;
constructor(
private id: string,
private x0: number, private y0: number, private x1: number, private y1: number) {
super();
this.content = `\n`;
}
public addColorStop(offset: number, color: string, opacity: number = 1): void {
this.content += `\n`;
}
public toDef(): string {
return this.content + "";
}
public toString(): string {
return `url(#${this.id})`;
}
}
export class SvgCanvas2DRadialGradient extends SvgCanvas2DGradient implements VectorCanvas2DGradient, SvgCanvas2DAsset {
private content: string;
/** fr is ignored. fr is always 0. */
constructor(
private id: string,
private cx: number, private cy: number, private r: number,
private fx: number, private fy: number) {
super();
this.content = `\n`;
}
public addColorStop(offset: number, color: string, opacity: number = 1): void {
this.content += `\n`;
}
public toDef() {
return this.content + "";
}
public toString() {
return `url(#${this.id})`;
}
}
export class SvgCanvas2DPattern implements VectorCanvas2DPattern, SvgCanvas2DAsset {
constructor(
private id: string,
private imageData: SvgCanvasImageData) {
}
public toDef() {
const content =
``;
return `${content}`;
}
public toString() {
return `url(#${this.id})`;
}
}
/**
* canvas: source-over|source-in|source-out|source-atop|
* destination-over|destination-in|destination-out|destination-atop|
* lighter|copy|xor
* svg: over|in|out|atop|xor|arithmetic
*/
export type GlobalCompositeOperationValue =
"source-over" | "source-in" | "source-out" | "source-atop" |
"destination-over" | "destination-in" | "destination-out" | "destination-atop" |
"lighter" | "copy" | "xor" |
"over" | "in" | "out" | "atop" | "arithmetic";
/**
* text-anchor
* canvas:start|end|left|right|center
* svg:start|middle|end
*/
export type TextAlignValue =
"start" | "end" | "left" | "right" | "center" | "middle";
/**
* dominant-baseline
* canvas:top|hanging|middle|alphabetic|ideographic|bottom
* svg:auto|use-script|no-change|reset-size|ideographic|alphabetic|hanging|mathematical|central|middle|text-after-edge|text-before-edge
* ('auto' is default. 'auto's effective value is depends on writing mode. if mode is horizontal then it is alphabetic. else then it is central)
*
* -------------------- text-before-edge (em square's edge)
* -- -- hanging
* -- X X -- mathematical
* X
* X X
* -- XXXX X -- central (em square's central), nearby of x-height(alphabetic lower case height)
* -- X X X -- middle (1/2 x-height)
* -- X X X -- alphabetic
* pp
* -----------^^------- ideographic, text-after-edge (em square's edge)
*/
export type TextBaselineValue =
"top" | "hanging" | "middle" | "alphabetic" | "ideographic" | "bottom" |
"auto" | "use-script" | "no-change" | "reset-size" |
"mathematical" | "central" | "text-after-edge" | "text-before-edge";
/**
* stroke-linecap
* butt|round|square
*/
export type LineCapValue = "butt" | "round" | "square";
/**
* stroke-linejoin
* miter|round|bevel
*/
export type LineJoinValue = "miter" | "round" | "bevel";
class SvgCanvasGraphicState {
public globalAlpha: number = 1;
/**
* canvas: source-over|source-in|source-out|source-atop|
* destination-over|destination-in|destination-out|destination-atop|
* lighter|copy|xor
* svg: over|in|out|atop|xor|arithmetic
*/
public globalCompositeOperation: GlobalCompositeOperationValue = "source-over";
public shadowBlur: number = 0;
/**
* canvas : <'web-color'>|#000|#000000|rgb(0,0,0)|rgba(0,0,0,0)
* SvgCanvas: rgb(0,0,0)|rgba(0,0,0,0)
*/
public shadowColor: string = "rgba(0,0,0,1)";
public shadowOffsetX: number = 0;
public shadowOffsetY: number = 0;
public filterIdUrl: string | null = null;
/**
* canvas: "italic bold 26px 'Times New Roman'"
* svg: font-family ="Times New Roman"
* font-style =normal|italic|oblique
* font-variant=normal|small-caps
* font-weight =normal|bold|bolder|lighter|100|200|300|400|500|600|700|800|900
* font-stretch=normal|wider|narrower|ultra-condensed|extra-condensed|condensed|
* semi-condensed|semi-expanded|expanded|extra-expanded|ultra-expanded
* font-size =|||
* font-size-adjust=|none
* font =[[<'font-style'>||<'font-variant'>||<'font-weight'>]?<'font-size'>[/<'line-height'>]?<'font-family'>]
*/
public font: string = "normal 12px 'Times New Roman'";
/**
* text-anchor
* canvas:start|end|left|right|center
* svg:start|middle|end
*/
public textAlign: TextAlignValue = "start";
/**
* dominant-baseline
* default is 'auto'.
* canvas:top|hanging|middle|alphabetic|ideographic|bottom
* ('alphabetic' is default)
* svg:auto|use-script|no-change|reset-size|ideographic|alphabetic|hanging|mathematical|central|middle|text-after-edge|text-before-edge
* ('auto' is default. 'auto's effective value is depends on writing mode. if mode is horizontal then it is alphabetic. else then it is central)
*/
public textBaseline: TextBaselineValue = "auto";
/**
* stroke-linecap
* butt|round|square
*/
public lineCap: LineCapValue = "butt";
public lineDashOffset: number = 0;
/**
* stroke-linejoin
* miter|round|bevel
*/
public lineJoin: LineJoinValue = "miter";
public lineWidth: number = 1;
public miterLimit: number = 4;
public strokeStyle: string = "black";
public fillStyle: string = "black";
public ctm: TransferMatrix2D = new TransferMatrix2D();
public currentPoint: Point2D | null = null;
public currentPointOnCtm: Point2D | null = null;
public subpath: Array = [];
public lineDash: number[] = [];
public clipPath: string = "";
constructor(src?: SvgCanvasGraphicState) {
if (! src) return;
for (const key in this) {
if (this.hasOwnProperty(key) && src.hasOwnProperty(key)) {
(this as any)[key] = (src as any)[key];
}
}
this.subpath = Array.from(this.subpath);
}
}
/**
* subset of CanvasRenderingContext2D
*/
export class SvgCanvas implements VectorCanvas2D {
public get globalAlpha(): number {
return this.graphicsStack[this.graphicsStack.length - 1].globalAlpha;
}
public set globalAlpha(value: number) {
this.graphicsStack[this.graphicsStack.length - 1].globalAlpha = value;
}
public get globalCompositeOperation(): GlobalCompositeOperationValue {
return this.graphicsStack[this.graphicsStack.length - 1].globalCompositeOperation;
}
public set globalCompositeOperation(value: GlobalCompositeOperationValue) {
this.filterIdUrl = null;
this.graphicsStack[this.graphicsStack.length - 1].globalCompositeOperation = value;
}
public get shadowBlur(): number {
return this.graphicsStack[this.graphicsStack.length - 1].shadowBlur;
}
public set shadowBlur(value: number) {
this.filterIdUrl = null;
this.graphicsStack[this.graphicsStack.length - 1].shadowBlur = value;
}
public get shadowColor(): string {
return this.graphicsStack[this.graphicsStack.length - 1].shadowColor;
}
public set shadowColor(value: string) {
this.filterIdUrl = null;
this.graphicsStack[this.graphicsStack.length - 1].shadowColor = value;
}
public get shadowOffsetX(): number {
return this.graphicsStack[this.graphicsStack.length - 1].shadowOffsetX;
}
public set shadowOffsetX(value: number) {
this.filterIdUrl = null;
this.graphicsStack[this.graphicsStack.length - 1].shadowOffsetX = value;
}
public get shadowOffsetY(): number {
return this.graphicsStack[this.graphicsStack.length - 1].shadowOffsetY;
}
public set shadowOffsetY(value: number) {
this.filterIdUrl = null;
this.graphicsStack[this.graphicsStack.length - 1].shadowOffsetY = value;
}
private get filterIdUrl(): string | null {
return this.graphicsStack[this.graphicsStack.length - 1].filterIdUrl;
}
private set filterIdUrl(value: string | null) {
this.graphicsStack[this.graphicsStack.length - 1].filterIdUrl = value;
}
/**
* canvas: "italic bold 26px 'Times New Roman'"
* svg: font-family ="Times New Roman"
* font-style =normal|italic|oblique
* font-variant=normal|small-caps
* font-weight =normal|bold|bolder|lighter|100|200|300|400|500|600|700|800|900
* font-stretch=normal|wider|narrower|ultra-condensed|extra-condensed|condensed|
* semi-condensed|semi-expanded|expanded|extra-expanded|ultra-expanded
* font-size =|||
* font-size-adjust=|none
* font =[[<'font-style'>||<'font-variant'>||<'font-weight'>]?<'font-size'>[/<'line-height'>]?<'font-family'>]
*/
public get font(): string {
return this.graphicsStack[this.graphicsStack.length - 1].font;
}
/**
* canvas: "italic bold 26px 'Times New Roman'"
* [<‘font-style’>||||<‘font-weight’>||<‘font-stretch’>]?<‘font-size’>[/<‘line-height’>]?<‘font-family’>
*
* svg: font-family ="Times New Roman"
* font-style =normal|italic|oblique
* font-variant=normal|small-caps
* font-weight =normal|bold|bolder|lighter|100|200|300|400|500|600|700|800|900
* font-stretch=normal|wider|narrower|ultra-condensed|extra-condensed|condensed|
* semi-condensed|semi-expanded|expanded|extra-expanded|ultra-expanded
* font-size =|||
* font-size-adjust=|none
* font =[<'font-style'>||<'font-variant'>||<'font-weight'>]?<'font-size'>[/<'line-height'>]?<'font-family'>
*/
public set font(value: string) {
this.graphicsStack[this.graphicsStack.length - 1].font = value;
}
/**
* text-anchor
* canvas:start|end|left|right|center
* svg:start|middle|end
*/
public get textAlign(): TextAlignValue {
return this.graphicsStack[this.graphicsStack.length - 1].textAlign;
}
/**
* text-anchor
* canvas:start|end|left|right|center
* svg:start|middle|end
*/
public set textAlign(value: TextAlignValue) {
this.graphicsStack[this.graphicsStack.length - 1].textAlign = value;
}
/**
* dominant-baseline
* canvas:top|hanging|middle|alphabetic|ideographic|bottom
* svg:auto|use-script|no-change|reset-size|ideographic|alphabetic|hanging|mathematical|central|middle|text-after-edge|text-before-edge
*/
public get textBaseline(): TextBaselineValue {
return this.graphicsStack[this.graphicsStack.length - 1].textBaseline;
}
/**
* dominant-baseline
* canvas:top|hanging|middle|alphabetic|ideographic|bottom
* svg:auto|use-script|no-change|reset-size|ideographic|alphabetic|hanging|mathematical|central|middle|text-after-edge|text-before-edge
*/
public set textBaseline(value: TextBaselineValue) {
this.graphicsStack[this.graphicsStack.length - 1].textBaseline = value;
}
/**
* stroke-linecap
* butt|round|square
*/
public get lineCap(): LineCapValue {
return this.graphicsStack[this.graphicsStack.length - 1].lineCap;
}
/**
* stroke-linecap
* butt|round|square
*/
public set lineCap(value: LineCapValue) {
this.graphicsStack[this.graphicsStack.length - 1].lineCap = value;
}
public get lineDashOffset(): number {
return this.graphicsStack[this.graphicsStack.length - 1].lineDashOffset;
}
public set lineDashOffset(value: number) {
this.graphicsStack[this.graphicsStack.length - 1].lineDashOffset = value;
}
/**
* stroke-linejoin
* miter|round|bevel
*/
public get lineJoin(): LineJoinValue {
return this.graphicsStack[this.graphicsStack.length - 1].lineJoin;
}
/**
* stroke-linejoin
* miter|round|bevel
*/
public set lineJoin(value: LineJoinValue) {
this.graphicsStack[this.graphicsStack.length - 1].lineJoin = value;
}
public get lineWidth(): number {
return this.graphicsStack[this.graphicsStack.length - 1].lineWidth;
}
public set lineWidth(value: number) {
this.graphicsStack[this.graphicsStack.length - 1].lineWidth = value;
}
public get miterLimit(): number {
return this.graphicsStack[this.graphicsStack.length - 1].miterLimit;
}
public set miterLimit(value: number) {
this.graphicsStack[this.graphicsStack.length - 1].miterLimit = value;
}
public get strokeStyle(): string | SvgCanvas2DLinerGradient | SvgCanvas2DRadialGradient | SvgCanvas2DPattern {
return this.graphicsStack[this.graphicsStack.length - 1].strokeStyle;
}
public set strokeStyle(value: string | SvgCanvas2DLinerGradient | SvgCanvas2DRadialGradient | SvgCanvas2DPattern) {
this.graphicsStack[this.graphicsStack.length - 1].strokeStyle = value.toString();
}
public get fillStyle(): string | SvgCanvas2DLinerGradient | SvgCanvas2DRadialGradient | SvgCanvas2DPattern {
return this.graphicsStack[this.graphicsStack.length - 1].fillStyle;
}
public set fillStyle(value: string | SvgCanvas2DLinerGradient | SvgCanvas2DRadialGradient | SvgCanvas2DPattern) {
this.graphicsStack[this.graphicsStack.length - 1].fillStyle = value.toString();
}
private get ctm(): TransferMatrix2D {
return this.graphicsStack[this.graphicsStack.length - 1].ctm;
}
private set ctm(value: TransferMatrix2D) {
this.graphicsStack[this.graphicsStack.length - 1].ctm = value;
}
private get currentPoint(): Point2D | null {
return this.graphicsStack[this.graphicsStack.length - 1].currentPoint;
}
private set currentPoint(value: Point2D | null) {
this.graphicsStack[this.graphicsStack.length - 1].currentPoint = value;
}
private get currentPointOnCtm(): Point2D | null {
return this.graphicsStack[this.graphicsStack.length - 1].currentPointOnCtm;
}
private set currentPointOnCtm(value: Point2D | null) {
this.graphicsStack[this.graphicsStack.length - 1].currentPointOnCtm = value;
}
private get subpath(): Array {
return this.graphicsStack[this.graphicsStack.length - 1].subpath;
}
private set subpath(value: Array) {
this.graphicsStack[this.graphicsStack.length - 1].subpath = value;
}
private get lineDash(): number[] {
return this.graphicsStack[this.graphicsStack.length - 1].lineDash;
}
private set lineDash(value: number[]) {
this.graphicsStack[this.graphicsStack.length - 1].lineDash = value;
}
private get clipPath(): string {
return this.graphicsStack[this.graphicsStack.length - 1].clipPath;
}
private set clipPath(value: string) {
this.graphicsStack[this.graphicsStack.length - 1].clipPath = value;
}
private get subpathIsEmptyOrClosed(): boolean {
return this.subpath.length === 0 || this.subpath[this.subpath.length - 1] === "Z";
}
private graphicsStack: SvgCanvasGraphicState[] = [];
private assets: Array = [];
private content = "";
private contentSaved = "";
private idCount = 0;
private template: string | null = null;
constructor() {
this.graphicsStack.push(new SvgCanvasGraphicState());
}
public static fromTemplate(template: string): SvgCanvas {
const c = new SvgCanvas();
c.template = template;
c.idCount = new Date().getTime();
return c;
}
public render(viewbox: Rect2D, unit: string = "mm"): string {
const svgns = `xmlns="http://www.w3.org/2000/svg"`;
const xlinkns = `xmlns:xlink="http://www.w3.org/1999/xlink"`;
const vbox = `viewBox="${viewbox.x} ${viewbox.y} ${viewbox.w} ${viewbox.h}"`;
const defs = `\n${
this.assets.map(x => typeof x === "string" ? x : x.toDef()).join("\n")
}`;
if (! this.template) {
return ``;
} else {
let tmpl = this.template.replace(/<\/svg>\s*$/, '');
{
const re = /(