Skip to content

Commit f31856e

Browse files
committed
feat: contentType support for filename & Content-Type
Signed-off-by: Neko Ayaka <[email protected]>
1 parent a5f68d3 commit f31856e

File tree

5 files changed

+171
-15
lines changed

5 files changed

+171
-15
lines changed

app/components/Generate.vue

+13-3
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,28 @@
11
<script lang="ts" setup>
22
import type { EncodedBlock } from '~~/utils/lt-code'
3-
import { blockToBinary, createEncoder } from '~~/utils/lt-code'
4-
import { fromUint8Array } from 'js-base64'
3+
import { binaryToBlock, blockToBinary, createEncoder } from '~~/utils/lt-code'
4+
import { fromUint8Array, toUint8Array } from 'js-base64'
55
import { renderSVG } from 'uqr'
66
77
const props = withDefaults(defineProps<{
88
data: Uint8Array
9+
filename?: string
10+
contentType?: string
911
speed: number
1012
}>(), {
1113
speed: 250,
1214
})
1315
16+
const abBase64 = fromUint8Array(new TextEncoder().encode(JSON.stringify({
17+
filename: props.filename,
18+
contentType: props.contentType,
19+
content: fromUint8Array(props.data),
20+
})))
21+
22+
const data = new Uint8Array(new TextEncoder().encode(abBase64))
23+
1424
const count = ref(0)
15-
const encoder = createEncoder(props.data, 1000)
25+
const encoder = createEncoder(data, 1000, 'application/json')
1626
const svg = ref<string>()
1727
const block = shallowRef<EncodedBlock>()
1828

app/components/Scan.vue

+88-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script lang="ts" setup>
2-
import { binaryToBlock, createDecoder } from '~~/utils/lt-code'
2+
import { binaryToBlock, ContentType, createDecoder } from '~~/utils/lt-code'
33
import { toUint8Array } from 'js-base64'
44
import { scan } from 'qr-scanner-wechat'
55
import { useBytesRate } from '~/composables/timeseries'
@@ -91,6 +91,7 @@ onMounted(async () => {
9191
}
9292
catch (e) {
9393
error.value = e
94+
console.error(e)
9495
}
9596
},
9697
() => props.speed,
@@ -124,6 +125,8 @@ async function connectCamera() {
124125
video.value!.play()
125126
}
126127
catch (e) {
128+
console.error(e)
129+
127130
if ((e as Error).name === 'NotAllowedError' || (e as Error).name === 'NotFoundError') {
128131
cameraSignalStatus.value = CameraSignalStatus.NotGranted
129132
return
@@ -134,9 +137,11 @@ async function connectCamera() {
134137
}
135138
136139
const decoder = ref(createDecoder())
140+
137141
const k = ref(0)
138142
const bytes = ref(0)
139143
const checksum = ref(0)
144+
140145
const cached = new Set<string>()
141146
const startTime = ref(0)
142147
const endTime = ref(0)
@@ -147,6 +152,10 @@ const status = ref<number[]>([])
147152
const decodedBlocks = computed(() => status.value.filter(i => i === 1).length)
148153
const receivedBytes = computed(() => decoder.value.encodedCount * (decoder.value.meta?.data.length ?? 0))
149154
155+
const filename = ref<string | undefined>()
156+
const contentType = ref<string | undefined>()
157+
const textContent = ref<string | undefined>()
158+
150159
function getStatus() {
151160
const array = Array.from({ length: k.value }, () => 0)
152161
for (let i = 0; i < k.value; i++) {
@@ -180,6 +189,49 @@ function pluse(index: number) {
180189
el.style.filter = 'none'
181190
}
182191
192+
/**
193+
* Proposed ideal processing method for decoded data from LT codes
194+
*
195+
* @param data - The decoded data from LT codes
196+
* @param type - The content type of the merged data, specified when encoding
197+
*/
198+
function toData(data: Uint8Array, type: ContentType): Uint8Array | string | any {
199+
// Binary data, no need to process
200+
if (type === ContentType.Binary) {
201+
return data
202+
}
203+
// Text data, decode and return
204+
else if (type === ContentType.Text) {
205+
return new TextDecoder().decode(data)
206+
}
207+
// Base64 encoded JSON data, decode and return
208+
else {
209+
const decodedFromBase64 = toUint8Array(new TextDecoder().decode(data))
210+
const decodedJSONBodyStr = new TextDecoder().decode(decodedFromBase64)
211+
const decodedJSONBody = JSON.parse(decodedJSONBodyStr)
212+
return decodedJSONBody
213+
}
214+
}
215+
216+
/**
217+
* Proposed ideal method to convert data to a data URL
218+
*
219+
* @param data - The data to convert
220+
* @param type - The content type of the data
221+
*/
222+
function toDataURL(data: Uint8Array | string | any, type: ContentType): string {
223+
if (type === ContentType.Binary) {
224+
return URL.createObjectURL(new Blob([data], { type: 'application/octet-stream' }))
225+
}
226+
else if (type === ContentType.Text) {
227+
return URL.createObjectURL(new Blob([new TextEncoder().encode(data)], { type: 'text/plain' }))
228+
}
229+
else {
230+
const json = JSON.stringify(data)
231+
return URL.createObjectURL(new Blob([new TextEncoder().encode(json)], { type: 'application/json' }))
232+
}
233+
}
234+
183235
async function scanFrame() {
184236
if (cameraSignalStatus.value === CameraSignalStatus.NotGranted
185237
|| cameraSignalStatus.value === CameraSignalStatus.NotSupported) {
@@ -231,13 +283,36 @@ async function scanFrame() {
231283
232284
cached.add(result.text)
233285
k.value = data.k
286+
234287
data.indices.map(i => pluse(i))
235288
const success = decoder.value.addBlock(data)
236289
status.value = getStatus()
237290
if (success) {
238291
endTime.value = performance.now()
292+
239293
const merged = decoder.value.getDecoded()!
240-
dataUrl.value = URL.createObjectURL(new Blob([merged], { type: 'application/octet-stream' }))
294+
295+
const mergedData = toData(merged, data.contentType)
296+
dataUrl.value = toDataURL(mergedData, data.contentType)
297+
298+
if (data.contentType === ContentType.Text) {
299+
textContent.value = mergedData
300+
}
301+
if (data.contentType === ContentType.JSON) {
302+
const jsonBody = mergedData as unknown as {
303+
filename: string
304+
contentType: string
305+
content: string
306+
}
307+
308+
filename.value = jsonBody.filename
309+
contentType.value = jsonBody.contentType
310+
311+
const payloadData = toUint8Array(jsonBody.content)
312+
if (contentType.value.startsWith('text/')) {
313+
textContent.value = new TextDecoder().decode(payloadData)
314+
}
315+
}
241316
}
242317
// console.log({ data })
243318
// if (Array.isArray(data)) {
@@ -289,6 +364,8 @@ function now() {
289364

290365
<Collapsable>
291366
<p w-full of-x-auto ws-nowrap px2 py1 font-mono :class="endTime ? 'text-green' : ''">
367+
<span>Filename: {{ filename }}</span><br>
368+
<span>Content-Type: {{ contentType }}</span><br>
292369
<span>Checksum: {{ checksum }}</span><br>
293370
<span>Indices: {{ k }}</span><br>
294371
<span>Decoded: {{ decodedBlocks }}</span><br>
@@ -322,12 +399,17 @@ function now() {
322399

323400
<Collapsable v-if="dataUrl" label="Download" :default="true">
324401
<div flex="~ col gap-2" max-w-150 p2>
325-
<img :src="dataUrl">
402+
<img v-if="contentType?.startsWith('image/')" :src="dataUrl">
403+
<p v-if="contentType?.startsWith('text/')" :src="dataUrl">
404+
{{ textContent }}
405+
</p>
326406
<a
327-
class="w-max border border-gray:50 rounded-md px2 py1 text-sm hover:bg-gray:10"
328407
:href="dataUrl"
329-
download="foo.png"
330-
>Download</a>
408+
:download="filename"
409+
class="w-max border border-gray:50 rounded-md px2 py1 text-sm hover:bg-gray:10"
410+
>
411+
Download
412+
</a>
331413
</div>
332414
</Collapsable>
333415

app/pages/index.vue

+12-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ enum ReadPhase {
99
const error = ref<any>()
1010
const speed = ref(100)
1111
const readPhase = ref<ReadPhase>(ReadPhase.Idle)
12+
13+
const filename = ref<string | undefined>()
14+
const contentType = ref<string | undefined>()
1215
const data = ref<Uint8Array | null>(null)
1316
1417
async function onFileChange(file?: File) {
@@ -20,8 +23,13 @@ async function onFileChange(file?: File) {
2023
2124
try {
2225
readPhase.value = ReadPhase.Reading
26+
27+
filename.value = file.name
28+
contentType.value = file.type
29+
2330
const buffer = await file.arrayBuffer()
2431
data.value = new Uint8Array(buffer)
32+
2533
readPhase.value = ReadPhase.Ready
2634
}
2735
catch (e) {
@@ -59,7 +67,10 @@ async function onFileChange(file?: File) {
5967
</div>
6068
<div v-if="readPhase === ReadPhase.Ready && data" h-full w-full flex justify-center>
6169
<Generate
62-
:speed="speed" :data="data"
70+
:speed="speed"
71+
:data="data"
72+
:filename="filename"
73+
:content-type="contentType"
6374
min-h="[calc(100vh-250px)]"
6475
max-w="[calc(100vh-250px)]"
6576
h-full w-full

utils/lt-code/encoder.ts

+14-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import type { EncodedBlock } from './shared'
21
import { getChecksum } from './checksum'
2+
import { type EncodedBlock, mapContentType } from './shared'
33

4-
export function createEncoder(data: Uint8Array, indicesSize: number) {
5-
return new LtEncoder(data, indicesSize)
4+
export function createEncoder(data: Uint8Array, indicesSize: number, contentType: string = 'application/octet-stream'): LtEncoder {
5+
return new LtEncoder(data, indicesSize, contentType)
66
}
77

88
export class LtEncoder {
@@ -14,6 +14,7 @@ export class LtEncoder {
1414
constructor(
1515
public readonly data: Uint8Array,
1616
public readonly indicesSize: number,
17+
public readonly contentType: string = 'application/octet-stream',
1718
) {
1819
this.indices = sliceData(data, indicesSize)
1920
this.k = this.indices.length
@@ -30,10 +31,20 @@ export class LtEncoder {
3031
}
3132
}
3233

34+
console.log({
35+
k: this.k,
36+
bytes: this.bytes,
37+
checksum: this.checksum,
38+
contentType: mapContentType(this.contentType),
39+
indices,
40+
data,
41+
})
42+
3343
return {
3444
k: this.k,
3545
bytes: this.bytes,
3646
checksum: this.checksum,
47+
contentType: mapContentType(this.contentType),
3748
indices,
3849
data,
3950
}

utils/lt-code/shared.ts

+44-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,18 @@
1+
export enum ContentType {
2+
/**
3+
* Binary data
4+
*/
5+
Binary,
6+
/**
7+
* Plain text
8+
*/
9+
Text,
10+
/**
11+
* JSON data
12+
*/
13+
JSON,
14+
}
15+
116
export interface EncodedHeader {
217
/**
318
* Number of original data blocks
@@ -11,6 +26,11 @@ export interface EncodedHeader {
1126
* Checksum, CRC32 and XOR of k
1227
*/
1328
checksum: number
29+
/**
30+
* Content Type, @see ContentType .
31+
* For converting Content-Type string to ContentType enum, use @see mapContentType .
32+
*/
33+
contentType: number
1434
}
1535

1636
export interface EncodedBlock extends EncodedHeader {
@@ -19,19 +39,23 @@ export interface EncodedBlock extends EncodedHeader {
1939
}
2040

2141
export function blockToBinary(block: EncodedBlock): Uint8Array {
22-
const { k, bytes, checksum, indices, data } = block
42+
const { k, bytes, checksum, indices, data, contentType } = block
2343
const header = new Uint32Array([
2444
indices.length,
2545
...indices,
2646
k,
2747
bytes,
2848
checksum,
49+
contentType,
2950
])
51+
52+
console.log('to:', bytes, checksum)
3053
const binary = new Uint8Array(header.length * 4 + data.length)
3154
let offset = 0
3255
binary.set(new Uint8Array(header.buffer), offset)
3356
offset += header.length * 4
3457
binary.set(data, offset)
58+
3559
return binary
3660
}
3761

@@ -44,13 +68,16 @@ export function binaryToBlock(binary: Uint8Array): EncodedBlock {
4468
k,
4569
bytes,
4670
checksum,
47-
] = headerRest.slice(degree) as [number, number, number]
71+
contentType,
72+
] = headerRest.slice(degree) as [number, number, number, number]
73+
console.log('from:', bytes, checksum)
4874
const data = binary.slice(4 * (degree + 4))
4975

5076
return {
5177
k,
5278
bytes,
5379
checksum,
80+
contentType,
5481
indices,
5582
data,
5683
}
@@ -73,3 +100,18 @@ export function stringToUint8Array(str: string): Uint8Array {
73100

74101
return data
75102
}
103+
104+
/**
105+
* Convert Content-Type string to @see ContentType enum.
106+
*
107+
* @param contentType Content-Type string
108+
* @returns {ContentType} enum
109+
*/
110+
export function mapContentType(contentType: string): ContentType {
111+
if (contentType.startsWith('text/'))
112+
return ContentType.Text
113+
if (contentType.startsWith('application/json'))
114+
return ContentType.JSON
115+
116+
return ContentType.Binary
117+
}

0 commit comments

Comments
 (0)