Skip to content

Commit d6b40a3

Browse files
committed
Merge PR carbon-app#1568 but keep Carbon.js from current branch (PR carbon-app#1579)
2 parents aa41313 + 458d1b9 commit d6b40a3

File tree

9 files changed

+439
-28
lines changed

9 files changed

+439
-28
lines changed

.devcontainer.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
2+
// README at: https://github.com/devcontainers/templates/tree/main/src/javascript-node
3+
{
4+
"name": "Node.js",
5+
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
6+
"image": "mcr.microsoft.com/devcontainers/javascript-node:1-22-bookworm",
7+
// Features to add to the dev container. More info: https://containers.dev/features.
8+
// "features": {},
9+
// Use 'forwardPorts' to make a list of ports inside the container available locally.
10+
"forwardPorts": [
11+
3000
12+
],
13+
// Use 'postCreateCommand' to run commands after the container is created.
14+
"postCreateCommand": "yarn install --frozen-lockfile && rm -rf /workspaces/carbon/.git/hooks/"
15+
// Configure tool-specific properties.
16+
// "customizations": {},
17+
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
18+
// "remoteUser": "root"
19+
}

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,6 @@ public/service-worker.js
1313
private-key.json
1414
.now
1515
.vercel
16-
*.log
16+
*.log
17+
public/sw.js
18+
public/workbox-*.js

components/Editor.js

Lines changed: 17 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ import {
3434
} from '../lib/constants'
3535
import { getRouteState } from '../lib/routing'
3636
import { getSettings, unescapeHtml, formatCode, omit } from '../lib/util'
37-
import domtoimage from '../lib/dom-to-image'
37+
import { nodeToSvg } from '../lib/svg'
38+
import { nodeToPng } from '../lib/png'
3839

3940
const languageIcon = <LanguageIcon />
4041

@@ -125,6 +126,12 @@ class Editor extends React.Component {
125126
if (className.includes('CodeMirror-cursors')) {
126127
return false
127128
}
129+
if (className.includes('CodeMirror-measure')) {
130+
return false;
131+
}
132+
if (className.includes('handler') && n.role === "separator") {
133+
return false;
134+
}
128135
}
129136
return true
130137
},
@@ -133,36 +140,20 @@ class Editor extends React.Component {
133140
}
134141

135142
if (format === 'svg') {
136-
return domtoimage
137-
.toSvg(node, config)
138-
.then(dataURL =>
139-
dataURL
140-
.replace(/&nbsp;/g, '&#160;')
141-
// https://github.com/tsayen/dom-to-image/blob/fae625bce0970b3a039671ea7f338d05ecb3d0e8/src/dom-to-image.js#L551
142-
.replace(/%23/g, '#')
143-
.replace(/%0A/g, '\n')
144-
// https://stackoverflow.com/questions/7604436/xmlparseentityref-no-name-warnings-while-loading-xml-into-a-php-file
145-
.replace(/&(?!#?[a-z0-9]+;)/g, '&amp;')
146-
// remove other fonts which are not used
147-
.replace(
148-
// current font-family used
149-
new RegExp(
150-
'@font-face\\s+{\\s+font-family: (?!"*' + this.state.fontFamily + ').*?}',
151-
'g'
152-
),
153-
''
154-
)
155-
)
143+
return nodeToSvg(node, config)
156144
.then(uri => uri.slice(uri.indexOf(',') + 1))
157145
.then(data => new Blob([data], { type: 'image/svg+xml' }))
146+
.catch(console.error)
158147
}
159148

160149
if (format === 'blob') {
161-
return domtoimage.toBlob(node, config)
150+
return nodeToPng(node, config)
151+
.then(url => fetch(url))
152+
.then(res => res.blob())
162153
}
163-
154+
// alert('format is blob')
164155
// Twitter and Imgur needs regular dataURLs
165-
return domtoimage.toPng(node, config)
156+
return nodeToPng(node, config) // untested because the twitter thingy doesn't work locally bit it should work
166157
}
167158

168159
tweet = () => {
@@ -469,8 +460,8 @@ class Editor extends React.Component {
469460
}
470461

471462
Editor.defaultProps = {
472-
onUpdate: () => {},
473-
onReset: () => {},
463+
onUpdate: () => { },
464+
onReset: () => { },
474465
}
475466

476467
export default Editor

lib/fonts.js

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
// font parsing based on https://github.com/tsayen/dom-to-image
2+
import { parse } from 'opentype.js';
3+
// eslint is bullshitting here. woff2-encoder/decompress is valid (see https://github.com/itskyedo/woff2-encoder)
4+
// eslint-disable-next-line import/no-unresolved
5+
import decompress from 'woff2-encoder/decompress';
6+
7+
// will download and fetch fonts that are loaded, usable for drawing!
8+
const URL_REGEX = /url\(['"]?([^'"]+?)['"]?\)/g
9+
10+
export class FontRepo {
11+
12+
13+
constructor() {
14+
this.fonts = {}
15+
}
16+
17+
18+
19+
async refresh() {
20+
// this can only run on the client!
21+
if (typeof document === 'undefined') {
22+
return
23+
}
24+
25+
26+
const stylesheets = Array.from(document.styleSheets)
27+
28+
const fontData = []
29+
// fetch all the fonts
30+
for (const sheet of stylesheets) {
31+
try {
32+
const rules = sheet.cssRules
33+
for (const rule of rules) {
34+
if (rule.constructor.name == 'CSSFontFaceRule' && rule.style.getPropertyValue('src').includes('url')) {
35+
const font = rule.style.getPropertyValue('font-family').toLowerCase().replace(/['"]/g, '')
36+
37+
// check the global font repo if we already have this font
38+
if (this.fonts[font] !== undefined) {
39+
continue
40+
}
41+
42+
const src = rule.style.getPropertyValue('src')
43+
const regexResult = src.match(URL_REGEX)
44+
if (regexResult == undefined) {
45+
continue;
46+
}
47+
const url = src.match(URL_REGEX)[0].replace(URL_REGEX, '$1')
48+
// TODO: dom-to-image has a edge case where the stylesheet has a different url or somehting? check there if this blows up
49+
fontData.push({ font, url })
50+
}
51+
}
52+
} catch (e) {
53+
//console.error(e)
54+
// not all the css rules can be read and this is fine!
55+
// (it's not that important, as long as we get the fonts we don't care)
56+
}
57+
}
58+
59+
60+
const uInt8FontData = await Promise.all(fontData.map(async ({ font, url }) => {
61+
// if it is a data url, just convert to a Uint8Array
62+
if (url.startsWith('data:')) {
63+
const base64 = url.split(',')[1]
64+
const binary = atob(base64)
65+
const bytes = new Uint8Array(binary.length)
66+
for (let i = 0; i < binary.length; i++) {
67+
bytes[i] = binary.charCodeAt(i)
68+
}
69+
return { font, data: bytes }
70+
}
71+
72+
// if it is a url, fetch it
73+
const response = await fetch(url)
74+
const blob = await response.blob()
75+
const buffer = await blob.arrayBuffer()
76+
return { font, data: new Uint8Array(buffer) }
77+
}))
78+
const toAddFonts = await Promise.all(uInt8FontData.map(async ({ font, data }) => {
79+
const format = FontRepo.detectFontFormat(data);
80+
let openTypeFont = null;
81+
try {
82+
if (format === 'WOFF2') {
83+
const decompressed = await decompress(data);
84+
openTypeFont = parse(decompressed.buffer);
85+
} else if (format !== null) {
86+
openTypeFont = parse(data.buffer)
87+
}
88+
} catch (e) {
89+
//console.error('Failed to parse font', font, e)
90+
}
91+
92+
return { font, openTypeFont }
93+
}))
94+
95+
toAddFonts.forEach(({ font, openTypeFont }) => {
96+
if (openTypeFont === null) {
97+
return;
98+
}
99+
this.fonts[font] = openTypeFont
100+
})
101+
//console.log(this.fonts);
102+
}
103+
104+
static detectFontFormat(uint8Array) {
105+
106+
const first4BytesHex = Array.from(uint8Array.slice(0, 4)) // Extract first 4 bytes
107+
.map(byte => byte.toString(16).padStart(2, '0')) // Convert to hex, pad to 2 digits
108+
.join(''); // Combine into a single string
109+
110+
switch (first4BytesHex) {
111+
case '774f4632': // WOFF2
112+
return 'WOFF2';
113+
case '774f4646': // WOFF
114+
return 'WOFF';
115+
case '00010000': // TTF
116+
case '4f54544f': // OTF
117+
return 'TTF/OTF';
118+
default:
119+
throw new Error(`Unknown font format: ${first4BytesHex}`);
120+
}
121+
}
122+
123+
getFont(fontFamily) {
124+
// split the font family by comma and go through each font, if we have it, return it else zero
125+
const fonts = fontFamily.split(',').map(font => font.trim().toLowerCase()).map(font => font.replace(/['"]/g, '').toLowerCase())
126+
for (const font of fonts) {
127+
if (this.fonts[font] !== undefined) {
128+
return this.fonts[font]
129+
}
130+
}
131+
return null
132+
}
133+
134+
}
135+

lib/png.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { nodeToSvg } from "./svg"
2+
import { blobToDataURL } from "./util";
3+
export async function nodeToPng(node, config) {
4+
const svgBlob = await nodeToSvg(node, config)
5+
.then(uri => uri.slice(uri.indexOf(',') + 1))
6+
.then(data => new Blob([data], { type: 'image/svg+xml' }))
7+
const svgDataUri = await blobToDataURL(svgBlob);
8+
9+
// https://zooper.pages.dev/articles/how-to-convert-a-svg-to-png-using-canvas
10+
const canvas = document.createElement('canvas')
11+
const transformStyle = config.style.transform;
12+
const scale = parseFloat(transformStyle.match(/scale\(([^)]+)\)/)[1]) // multiplyer!
13+
14+
const width = node.getBoundingClientRect().width * scale
15+
const height = node.getBoundingClientRect().height * scale
16+
17+
canvas.width = width
18+
canvas.height = height
19+
await drawImgToCanvas(svgDataUri, canvas, scale)
20+
const pngUrl = canvas.toDataURL('image/png')
21+
return pngUrl
22+
}
23+
24+
function drawImgToCanvas(imgUrl, canvas, scale = 1) {
25+
return new Promise((resolve, reject) => {
26+
const img = new Image()
27+
img.onload = () => {
28+
const ctx = canvas.getContext('2d')
29+
ctx.scale(scale, scale)
30+
ctx.drawImage(img, 0, 0)
31+
resolve()
32+
}
33+
img.onerror = reject
34+
img.src = imgUrl
35+
})
36+
}

0 commit comments

Comments
 (0)