Download parser.js from PellelNitram/xournalpp_htr_data_annotation_tool: direct link, hf CLI and curl.
- Browser
- Download file 5.23 kB
-
https://huggingface.co/spaces/PellelNitram/xournalpp_htr_data_annotation_tool/resolve/main/parser.js
- Command line
-
hf download hf://spaces/PellelNitram/xournalpp_htr_data_annotation_tool/parser.js
-
curl -L -o parser.js https://huggingface.co/spaces/PellelNitram/xournalpp_htr_data_annotation_tool/resolve/main/parser.js
5.23 kB
| // parser.js - Parse .xopp (gzipped XML) and .xoj (plain XML) files | |
| export async function parseFile(file) { | |
| const buffer = await file.arrayBuffer(); | |
| const sha256 = await computeSha256(buffer); | |
| let xmlString; | |
| try { | |
| xmlString = await decompressGzip(buffer); | |
| } catch { | |
| xmlString = new TextDecoder().decode(buffer); | |
| } | |
| const parser = new DOMParser(); | |
| const doc = parser.parseFromString(xmlString, 'text/xml'); | |
| const parserError = doc.querySelector('parsererror'); | |
| if (parserError) { | |
| throw new Error('Failed to parse XML: ' + parserError.textContent); | |
| } | |
| const result = extractDocument(doc); | |
| result.sha256 = sha256; | |
| return result; | |
| } | |
| async function computeSha256(buffer) { | |
| const hashBuffer = await crypto.subtle.digest('SHA-256', buffer); | |
| const hashArray = Array.from(new Uint8Array(hashBuffer)); | |
| return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); | |
| } | |
| async function decompressGzip(buffer) { | |
| const ds = new DecompressionStream('gzip'); | |
| const writer = ds.writable.getWriter(); | |
| writer.write(new Uint8Array(buffer)); | |
| writer.close(); | |
| const reader = ds.readable.getReader(); | |
| const chunks = []; | |
| while (true) { | |
| const { done, value } = await reader.read(); | |
| if (done) break; | |
| chunks.push(value); | |
| } | |
| const totalLength = chunks.reduce((sum, c) => sum + c.length, 0); | |
| const result = new Uint8Array(totalLength); | |
| let offset = 0; | |
| for (const chunk of chunks) { | |
| result.set(chunk, offset); | |
| offset += chunk.length; | |
| } | |
| return new TextDecoder().decode(result); | |
| } | |
| function extractDocument(doc) { | |
| const root = doc.documentElement; | |
| const pages = []; | |
| const pageElements = root.querySelectorAll('page'); | |
| for (let pageIdx = 0; pageIdx < pageElements.length; pageIdx++) { | |
| const pageEl = pageElements[pageIdx]; | |
| const width = parseFloat(pageEl.getAttribute('width') || '612'); | |
| const height = parseFloat(pageEl.getAttribute('height') || '792'); | |
| const strokes = []; | |
| let globalStrokeIdx = 0; | |
| // Each layer is tracked separately so stroke_indices in the schema | |
| // refer to positions within the layer, matching the source XML. | |
| const layers = pageEl.querySelectorAll('layer'); | |
| for (let layerIdx = 0; layerIdx < layers.length; layerIdx++) { | |
| const layer = layers[layerIdx]; | |
| const strokeElements = layer.querySelectorAll('stroke'); | |
| for (let inLayerIdx = 0; inLayerIdx < strokeElements.length; inLayerIdx++) { | |
| const stroke = parseStroke( | |
| strokeElements[inLayerIdx], | |
| globalStrokeIdx, | |
| layerIdx, | |
| inLayerIdx | |
| ); | |
| if (stroke) { | |
| strokes.push(stroke); | |
| globalStrokeIdx++; | |
| } | |
| // inLayerIdx always advances so it matches the XML element position, | |
| // even when a stroke element is skipped (no points). | |
| } | |
| } | |
| pages.push({ index: pageIdx, width, height, strokes }); | |
| } | |
| return { pages }; | |
| } | |
| function parseStroke(strokeEl, globalIndex, layerIndex, indexInLayer) { | |
| const tool = strokeEl.getAttribute('tool') || 'pen'; | |
| const colorAttr = strokeEl.getAttribute('color') || 'black'; | |
| const widthAttr = strokeEl.getAttribute('width') || '1.0'; | |
| const widths = widthAttr.trim().split(/\s+/).map(Number); | |
| const baseWidth = widths[0]; | |
| const text = strokeEl.textContent.trim(); | |
| if (!text) return null; | |
| const values = text.split(/\s+/).map(Number); | |
| if (values.length < 4) return null; | |
| const points = []; | |
| for (let i = 0; i < values.length - 1; i += 2) { | |
| points.push({ x: values[i], y: values[i + 1] }); | |
| } | |
| let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; | |
| for (const p of points) { | |
| if (p.x < minX) minX = p.x; | |
| if (p.y < minY) minY = p.y; | |
| if (p.x > maxX) maxX = p.x; | |
| if (p.y > maxY) maxY = p.y; | |
| } | |
| return { | |
| index: globalIndex, // flat index across all layers, used for selection | |
| layerIndex, // which <layer> element within the page (0-based) | |
| indexInLayer, // position within the layer's <stroke> elements (0-based) | |
| tool, | |
| color: resolveColor(colorAttr), | |
| width: baseWidth, | |
| widths: widths.length > 1 ? widths : null, | |
| points, | |
| bbox: { minX, minY, maxX, maxY }, | |
| }; | |
| } | |
| function resolveColor(colorAttr) { | |
| const named = { | |
| black: '#000000', | |
| blue: '#3333cc', | |
| red: '#ff0000', | |
| green: '#008000', | |
| gray: '#808080', | |
| lightblue: '#00c0ff', | |
| lightgreen: '#00ff00', | |
| magenta: '#ff00ff', | |
| orange: '#ff8000', | |
| yellow: '#ffff00', | |
| white: '#ffffff', | |
| }; | |
| if (named[colorAttr]) return named[colorAttr]; | |
| if (colorAttr.startsWith('#')) { | |
| return colorAttr.substring(0, 7); | |
| } | |
| if (colorAttr.startsWith('rgba') || colorAttr.startsWith('rgb')) { | |
| return colorAttr; | |
| } | |
| return '#000000'; | |
| } | |