Skip to content

Commit

Permalink
feat: extra meta & filename & Content-Type support (#4)
Browse files Browse the repository at this point in the history
* feat: contentType support for filename & Content-Type
* feat: implement binary meta
* feat: use new implementation
* chore: lint
* chore: comments
* chore: update utils/lt-code/shared.ts

---------

Co-authored-by: Anthony Fu <[email protected]>
  • Loading branch information
nekomeowww and antfu authored Oct 4, 2024
1 parent a5f68d3 commit 1997c24
Show file tree
Hide file tree
Showing 6 changed files with 165 additions and 17 deletions.
2 changes: 2 additions & 0 deletions app/components/Generate.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { renderSVG } from 'uqr'
const props = withDefaults(defineProps<{
data: Uint8Array
filename?: string
contentType?: string
speed: number
}>(), {
speed: 250,
Expand Down
56 changes: 51 additions & 5 deletions app/components/Scan.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script lang="ts" setup>
import { binaryToBlock, createDecoder } from '~~/utils/lt-code'
import { readFileHeaderMetaFromBuffer } from '~~/utils/lt-code/binary-meta'
import { toUint8Array } from 'js-base64'
import { scan } from 'qr-scanner-wechat'
import { useBytesRate } from '~/composables/timeseries'
Expand Down Expand Up @@ -91,6 +92,7 @@ onMounted(async () => {
}
catch (e) {
error.value = e
console.error(e)
}
},
() => props.speed,
Expand Down Expand Up @@ -124,6 +126,8 @@ async function connectCamera() {
video.value!.play()
}
catch (e) {
console.error(e)
if ((e as Error).name === 'NotAllowedError' || (e as Error).name === 'NotFoundError') {
cameraSignalStatus.value = CameraSignalStatus.NotGranted
return
Expand All @@ -134,9 +138,11 @@ async function connectCamera() {
}
const decoder = ref(createDecoder())
const k = ref(0)
const bytes = ref(0)
const checksum = ref(0)
const cached = new Set<string>()
const startTime = ref(0)
const endTime = ref(0)
Expand All @@ -147,6 +153,10 @@ const status = ref<number[]>([])
const decodedBlocks = computed(() => status.value.filter(i => i === 1).length)
const receivedBytes = computed(() => decoder.value.encodedCount * (decoder.value.meta?.data.length ?? 0))
const filename = ref<string | undefined>()
const contentType = ref<string | undefined>()
const textContent = ref<string | undefined>()
function getStatus() {
const array = Array.from({ length: k.value }, () => 0)
for (let i = 0; i < k.value; i++) {
Expand Down Expand Up @@ -180,6 +190,25 @@ function pluse(index: number) {
el.style.filter = 'none'
}
/**
* Proposed ideal method to convert data to a data URL
*
* @param data - The data to convert
* @param type - The content type of the data
*/
function toDataURL(data: Uint8Array | string | any, type: string): string {
if (type.startsWith('text/')) {
return URL.createObjectURL(new Blob([new TextEncoder().encode(data)], { type: 'text/plain' }))
}
else if (type === 'application/json') {
const json = JSON.stringify(data)
return URL.createObjectURL(new Blob([new TextEncoder().encode(json)], { type: 'application/json' }))
}
else {
return URL.createObjectURL(new Blob([data], { type: 'application/octet-stream' }))
}
}
async function scanFrame() {
if (cameraSignalStatus.value === CameraSignalStatus.NotGranted
|| cameraSignalStatus.value === CameraSignalStatus.NotSupported) {
Expand Down Expand Up @@ -231,13 +260,23 @@ async function scanFrame() {
cached.add(result.text)
k.value = data.k
data.indices.map(i => pluse(i))
const success = decoder.value.addBlock(data)
status.value = getStatus()
if (success) {
endTime.value = performance.now()
const merged = decoder.value.getDecoded()!
dataUrl.value = URL.createObjectURL(new Blob([merged], { type: 'application/octet-stream' }))
const [mergedData, meta] = readFileHeaderMetaFromBuffer(merged)
dataUrl.value = toDataURL(mergedData, meta.contentType)
filename.value = meta.filename
contentType.value = meta.contentType
if (contentType.value.startsWith('text/')) {
textContent.value = new TextDecoder().decode(mergedData)
}
}
// console.log({ data })
// if (Array.isArray(data)) {
Expand Down Expand Up @@ -289,6 +328,8 @@ function now() {

<Collapsable>
<p w-full of-x-auto ws-nowrap px2 py1 font-mono :class="endTime ? 'text-green' : ''">
<span>Filename: {{ filename }}</span><br>
<span>Content-Type: {{ contentType }}</span><br>
<span>Checksum: {{ checksum }}</span><br>
<span>Indices: {{ k }}</span><br>
<span>Decoded: {{ decodedBlocks }}</span><br>
Expand Down Expand Up @@ -322,12 +363,17 @@ function now() {

<Collapsable v-if="dataUrl" label="Download" :default="true">
<div flex="~ col gap-2" max-w-150 p2>
<img :src="dataUrl">
<img v-if="contentType?.startsWith('image/')" :src="dataUrl">
<p v-if="contentType?.startsWith('text/')" :src="dataUrl">
{{ textContent }}
</p>
<a
class="w-max border border-gray:50 rounded-md px2 py1 text-sm hover:bg-gray:10"
:href="dataUrl"
download="foo.png"
>Download</a>
:download="filename"
class="w-max border border-gray:50 rounded-md px2 py1 text-sm hover:bg-gray:10"
>
Download
</a>
</div>
</Collapsable>

Expand Down
20 changes: 18 additions & 2 deletions app/pages/index.vue
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<script lang="ts" setup>
import { appendFileHeaderMetaToBuffer } from '~~/utils/lt-code/binary-meta'
enum ReadPhase {
Idle,
Reading,
Expand All @@ -9,6 +11,9 @@ enum ReadPhase {
const error = ref<any>()
const speed = ref(100)
const readPhase = ref<ReadPhase>(ReadPhase.Idle)
const filename = ref<string | undefined>()
const contentType = ref<string | undefined>()
const data = ref<Uint8Array | null>(null)
async function onFileChange(file?: File) {
Expand All @@ -20,8 +25,16 @@ async function onFileChange(file?: File) {
try {
readPhase.value = ReadPhase.Reading
filename.value = file.name
contentType.value = file.type
const buffer = await file.arrayBuffer()
data.value = new Uint8Array(buffer)
data.value = appendFileHeaderMetaToBuffer(new Uint8Array(buffer), {
filename: filename.value,
contentType: contentType.value,
})
readPhase.value = ReadPhase.Ready
}
catch (e) {
Expand Down Expand Up @@ -59,7 +72,10 @@ async function onFileChange(file?: File) {
</div>
<div v-if="readPhase === ReadPhase.Ready && data" h-full w-full flex justify-center>
<Generate
:speed="speed" :data="data"
:speed="speed"
:data="data"
:filename="filename"
:content-type="contentType"
min-h="[calc(100vh-250px)]"
max-w="[calc(100vh-250px)]"
h-full w-full
Expand Down
91 changes: 91 additions & 0 deletions utils/lt-code/binary-meta.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/**
* Merge multiple Uint8Array into a single Uint8Array
* Each chunk is prefixed with a 4-byte Uint32 to store the length of the chunk
*/
export function mergeUint8Arrays(arrays: Uint8Array[]) {
const totalLength = arrays.reduce((sum, arr) => sum + arr.length + 4, 0) // 4 是為了存每個數組的長度 (Uint32)

const mergedArray = new Uint8Array(totalLength)
let offset = 0

arrays.forEach((arr) => {
const length = arr.length
// Store the length as a 4-byte Uint32
mergedArray[offset++] = (length >> 24) & 0xFF
mergedArray[offset++] = (length >> 16) & 0xFF
mergedArray[offset++] = (length >> 8) & 0xFF
mergedArray[offset++] = length & 0xFF

// Copy data
mergedArray.set(arr, offset)
offset += length
})

return mergedArray
}

/**
* Split a merged Uint8Array into multiple Uint8Array
*/
export function splitUint8Arrays(mergedArray: Uint8Array): Uint8Array[] {
const arrays = []
let offset = 0

while (offset < mergedArray.length) {
// Read chunk length
const length = (mergedArray[offset++]! << 24)
| (mergedArray[offset++]! << 16)
| (mergedArray[offset++]! << 8)
| mergedArray[offset++]!

// Slice the chunk
const arr = mergedArray.slice(offset, offset + length)
arrays.push(arr)
offset += length
}

return arrays
}

export function appendMetaToBuffer<T>(data: Uint8Array, meta: T): Uint8Array {
const json = JSON.stringify(meta)
const metaBuffer = stringToUint8Array(json)
return mergeUint8Arrays([metaBuffer, data])
}

export function appendFileHeaderMetaToBuffer(data: Uint8Array, meta: { filename?: string, contentType?: string }): Uint8Array {
return appendMetaToBuffer(data, meta)
}

export function readMetaFromBuffer<T>(buffer: Uint8Array): [data: Uint8Array, meta: T] {
const splitted = splitUint8Arrays(buffer) as [Uint8Array, Uint8Array]
if (splitted.length !== 2) {
throw new Error('Invalid buffer')
}

const [metaBuffer, data] = splitted
const meta = JSON.parse(uint8ArrayToString(metaBuffer!))
return [data, meta]
}

export function readFileHeaderMetaFromBuffer(buffer: Uint8Array): [data: Uint8Array, meta: { filename?: string, contentType: string }] {
const [data, meta] = readMetaFromBuffer<{ filename?: string, contentType?: string }>(buffer)
if (!meta.contentType) {
meta.contentType = 'application/octet-stream'
}

return [data, meta] as [Uint8Array, { filename?: string, contentType: string }]
}

export function stringToUint8Array(str: string): Uint8Array {
const data = new Uint8Array(str.length)
for (let i = 0; i < str.length; i++) {
data[i] = str.charCodeAt(i)
}

return data
}

export function uint8ArrayToString(data: Uint8Array): string {
return String.fromCharCode(...data)
}
2 changes: 1 addition & 1 deletion utils/lt-code/encoder.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { EncodedBlock } from './shared'
import { getChecksum } from './checksum'

export function createEncoder(data: Uint8Array, indicesSize: number) {
export function createEncoder(data: Uint8Array, indicesSize: number): LtEncoder {
return new LtEncoder(data, indicesSize)
}

Expand Down
11 changes: 2 additions & 9 deletions utils/lt-code/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,13 @@ export function blockToBinary(block: EncodedBlock): Uint8Array {
bytes,
checksum,
])

const binary = new Uint8Array(header.length * 4 + data.length)
let offset = 0
binary.set(new Uint8Array(header.buffer), offset)
offset += header.length * 4
binary.set(data, offset)

return binary
}

Expand Down Expand Up @@ -64,12 +66,3 @@ export function xorUint8Array(a: Uint8Array, b: Uint8Array): Uint8Array {

return result
}

export function stringToUint8Array(str: string): Uint8Array {
const data = new Uint8Array(str.length)
for (let i = 0; i < str.length; i++) {
data[i] = str.charCodeAt(i)
}

return data
}

0 comments on commit 1997c24

Please sign in to comment.