Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Non foreignObject SVG export added, fixed bug in png export, added a file for creating a devcontainer and made the project compile #1568

Open
wants to merge 31 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
ffd7592
made the project compilable again!
TomTom2028 Jan 8, 2025
f7765bc
fixed unused suspense
TomTom2028 Jan 8, 2025
36a06b1
Merge pull request #1 from TomTom2028/better-svg
TomTom2028 Jan 9, 2025
d6c0b0f
implemented basic bg and text rendering
TomTom2028 Jan 9, 2025
172b52a
fixed console logs
TomTom2028 Jan 9, 2025
11223ef
fixed svg with height issue
TomTom2028 Jan 9, 2025
6e41f20
added rounded corners and begon work on the box shadow
TomTom2028 Jan 9, 2025
b497992
added correct box shadows and fixed spaces
TomTom2028 Jan 10, 2025
c8c2817
implmented font inlining
TomTom2028 Jan 11, 2025
1732402
made the embedding better and filtered
TomTom2028 Jan 11, 2025
5ff7d07
worked a bit on the font loading
TomTom2028 Jan 12, 2025
ee521f4
remote font rendering working!
TomTom2028 Jan 12, 2025
f48f035
fixed a remaining console log
TomTom2028 Jan 12, 2025
40b14c4
made a not hacky, completly portible fix for the box shadows!
TomTom2028 Jan 13, 2025
4e31cf7
used a different import for woff-2 encoder that has a way smaller was…
TomTom2028 Jan 13, 2025
0976790
removed git hooks from devcontainer because they don't work
TomTom2028 Jan 14, 2025
440058e
typo
TomTom2028 Jan 14, 2025
fa0f0c4
hopefylly working now
TomTom2028 Jan 14, 2025
7091217
tried to change the order
TomTom2028 Jan 14, 2025
ac2df47
made the path non relative
TomTom2028 Jan 14, 2025
6600a65
added the beginnings of svg embeddings
TomTom2028 Jan 17, 2025
a94eb96
fixed the alligment issues of the text!
TomTom2028 Jan 21, 2025
b8b7d3b
moved the filter to the correct spot
TomTom2028 Jan 21, 2025
a46ff44
Merge pull request #2 from TomTom2028/better-svg
TomTom2028 Jan 21, 2025
c6fe7c4
removed the unneeded composite on the shadows
TomTom2028 Jan 23, 2025
f98962a
Merge pull request #3 from TomTom2028/better-svg
TomTom2028 Jan 23, 2025
f8be292
made the CodeMirrrorWrapper work synchronously for improving the ux
TomTom2028 Jan 24, 2025
1a755f4
removed unneeded post processing on svg and fixed the png renderer
TomTom2028 Jan 25, 2025
dfefe1f
added credits and removed seperator from render
TomTom2028 Jan 26, 2025
d91e360
changed the twitter export data url to the custom one
TomTom2028 Jan 26, 2025
6a2cf9c
Merge pull request #4 from TomTom2028/fix-png-problems
TomTom2028 Jan 26, 2025
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
19 changes: 19 additions & 0 deletions .devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/javascript-node
{
"name": "Node.js",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"image": "mcr.microsoft.com/devcontainers/javascript-node:1-22-bookworm",
// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
"forwardPorts": [
3000
],
// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": "yarn install --frozen-lockfile && rm -rf /workspaces/carbon/.git/hooks/"
// Configure tool-specific properties.
// "customizations": {},
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root"
}
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,6 @@ public/service-worker.js
private-key.json
.now
.vercel
*.log
*.log
public/sw.js
public/workbox-*.js
7 changes: 5 additions & 2 deletions components/Carbon.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import hljs from 'highlight.js/lib/core'
import javascript from 'highlight.js/lib/languages/javascript'
import debounce from 'lodash.debounce'
import ms from 'ms'
import { Controlled as CodeMirror } from 'react-codemirror2'

hljs.registerLanguage('javascript', javascript)

Expand All @@ -22,6 +21,7 @@ import {
DEFAULT_SETTINGS,
THEMES_HASH,
} from '../lib/constants'
import CodeMirrorWrapper from './CodeMirrorWrapper'

const SelectionEditor = dynamic(() => import('./SelectionEditor'), {
loading: () => null,
Expand All @@ -30,6 +30,9 @@ const Watermark = dynamic(() => import('./svg/Watermark'), {
loading: () => null,
})




function searchLanguage(l) {
return LANGUAGE_NAME_HASH[l] || LANGUAGE_MODE_HASH[l] || LANGUAGE_MIME_HASH[l]
}
Expand Down Expand Up @@ -209,7 +212,7 @@ class Carbon extends React.PureComponent {
light={light}
/>
) : null}
<CodeMirror
<CodeMirrorWrapper
ref={this.props.editorRef}
className={`CodeMirror__container window-theme__${config.windowTheme}`}
value={this.props.children}
Expand Down
9 changes: 9 additions & 0 deletions components/CodeMirrorWrapper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
let CodeMirrorWrapper
if (typeof document === 'undefined') {
// server context, loading codemirror will cause error, so return a dummy component
CodeMirrorWrapper = () => <></>
} else {
// client context, load the component with require, so it is synchronous
CodeMirrorWrapper = require('react-codemirror2').Controlled
}
export default CodeMirrorWrapper
43 changes: 17 additions & 26 deletions components/Editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ import {
} from '../lib/constants'
import { getRouteState } from '../lib/routing'
import { getSettings, unescapeHtml, formatCode, omit } from '../lib/util'
import domtoimage from '../lib/dom-to-image'
import { nodeToSvg } from '../lib/svg'
import { nodeToPng } from '../lib/png'

const languageIcon = <LanguageIcon />

Expand Down Expand Up @@ -125,6 +126,12 @@ class Editor extends React.Component {
if (className.includes('CodeMirror-cursors')) {
return false
}
if (className.includes('CodeMirror-measure')) {
return false;
}
if (className.includes('handler') && n.role === "separator") {
return false;
}
}
return true
},
Expand All @@ -133,36 +140,20 @@ class Editor extends React.Component {
}

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

if (format === 'blob') {
return domtoimage.toBlob(node, config)
return nodeToPng(node, config)
.then(url => fetch(url))
.then(res => res.blob())
}

// alert('format is blob')
// Twitter and Imgur needs regular dataURLs
return domtoimage.toPng(node, config)
return nodeToPng(node, config) // untested because the twitter thingy doesn't work locally bit it should work
}

tweet = () => {
Expand Down Expand Up @@ -469,8 +460,8 @@ class Editor extends React.Component {
}

Editor.defaultProps = {
onUpdate: () => {},
onReset: () => {},
onUpdate: () => { },
onReset: () => { },
}

export default Editor
135 changes: 135 additions & 0 deletions lib/fonts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// font parsing based on https://github.com/tsayen/dom-to-image
import { parse } from 'opentype.js';
// eslint is bullshitting here. woff2-encoder/decompress is valid (see https://github.com/itskyedo/woff2-encoder)
// eslint-disable-next-line import/no-unresolved
import decompress from 'woff2-encoder/decompress';

// will download and fetch fonts that are loaded, usable for drawing!
const URL_REGEX = /url\(['"]?([^'"]+?)['"]?\)/g

export class FontRepo {


constructor() {
this.fonts = {}
}



async refresh() {
// this can only run on the client!
if (typeof document === 'undefined') {
return
}


const stylesheets = Array.from(document.styleSheets)

const fontData = []
// fetch all the fonts
for (const sheet of stylesheets) {
try {
const rules = sheet.cssRules
for (const rule of rules) {
if (rule.constructor.name == 'CSSFontFaceRule' && rule.style.getPropertyValue('src').includes('url')) {
const font = rule.style.getPropertyValue('font-family').toLowerCase().replace(/['"]/g, '')

// check the global font repo if we already have this font
if (this.fonts[font] !== undefined) {
continue
}

const src = rule.style.getPropertyValue('src')
const regexResult = src.match(URL_REGEX)
if (regexResult == undefined) {
continue;
}
const url = src.match(URL_REGEX)[0].replace(URL_REGEX, '$1')
// TODO: dom-to-image has a edge case where the stylesheet has a different url or somehting? check there if this blows up
fontData.push({ font, url })
}
}
} catch (e) {
//console.error(e)
// not all the css rules can be read and this is fine!
// (it's not that important, as long as we get the fonts we don't care)
}
}


const uInt8FontData = await Promise.all(fontData.map(async ({ font, url }) => {
// if it is a data url, just convert to a Uint8Array
if (url.startsWith('data:')) {
const base64 = url.split(',')[1]
const binary = atob(base64)
const bytes = new Uint8Array(binary.length)
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i)
}
return { font, data: bytes }
}

// if it is a url, fetch it
const response = await fetch(url)
const blob = await response.blob()
const buffer = await blob.arrayBuffer()
return { font, data: new Uint8Array(buffer) }
}))
const toAddFonts = await Promise.all(uInt8FontData.map(async ({ font, data }) => {
const format = FontRepo.detectFontFormat(data);
let openTypeFont = null;
try {
if (format === 'WOFF2') {
const decompressed = await decompress(data);
openTypeFont = parse(decompressed.buffer);
} else if (format !== null) {
openTypeFont = parse(data.buffer)
}
} catch (e) {
//console.error('Failed to parse font', font, e)
}

return { font, openTypeFont }
}))

toAddFonts.forEach(({ font, openTypeFont }) => {
if (openTypeFont === null) {
return;
}
this.fonts[font] = openTypeFont
})
//console.log(this.fonts);
}

static detectFontFormat(uint8Array) {

const first4BytesHex = Array.from(uint8Array.slice(0, 4)) // Extract first 4 bytes
.map(byte => byte.toString(16).padStart(2, '0')) // Convert to hex, pad to 2 digits
.join(''); // Combine into a single string

switch (first4BytesHex) {
case '774f4632': // WOFF2
return 'WOFF2';
case '774f4646': // WOFF
return 'WOFF';
case '00010000': // TTF
case '4f54544f': // OTF
return 'TTF/OTF';
default:
throw new Error(`Unknown font format: ${first4BytesHex}`);
}
}

getFont(fontFamily) {
// split the font family by comma and go through each font, if we have it, return it else zero
const fonts = fontFamily.split(',').map(font => font.trim().toLowerCase()).map(font => font.replace(/['"]/g, '').toLowerCase())
for (const font of fonts) {
if (this.fonts[font] !== undefined) {
return this.fonts[font]
}
}
return null
}

}

36 changes: 36 additions & 0 deletions lib/png.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { nodeToSvg } from "./svg"
import { blobToDataURL } from "./util";
export async function nodeToPng(node, config) {
const svgBlob = await nodeToSvg(node, config)
.then(uri => uri.slice(uri.indexOf(',') + 1))
.then(data => new Blob([data], { type: 'image/svg+xml' }))
const svgDataUri = await blobToDataURL(svgBlob);

// https://zooper.pages.dev/articles/how-to-convert-a-svg-to-png-using-canvas
const canvas = document.createElement('canvas')
const transformStyle = config.style.transform;
const scale = parseFloat(transformStyle.match(/scale\(([^)]+)\)/)[1]) // multiplyer!

const width = node.getBoundingClientRect().width * scale
const height = node.getBoundingClientRect().height * scale

canvas.width = width
canvas.height = height
await drawImgToCanvas(svgDataUri, canvas, scale)
const pngUrl = canvas.toDataURL('image/png')
return pngUrl
}

function drawImgToCanvas(imgUrl, canvas, scale = 1) {
return new Promise((resolve, reject) => {
const img = new Image()
img.onload = () => {
const ctx = canvas.getContext('2d')
ctx.scale(scale, scale)
ctx.drawImage(img, 0, 0)
resolve()
}
img.onerror = reject
img.src = imgUrl
})
}
Loading