mirror of
https://github.com/IliesAllali/parenthese.git
synced 2026-11-10 15:52:14 +00:00
Import GEDCOM : le fichier est lu avec son vrai encodage
Le navigateur lisait tout en UTF-8. Les fichiers de Heredis PC (ANSI, Windows-1252), des anciens logiciels (ANSEL) ou de Heredis Mac (MACINTOSH) arrivaient avec des accents cassés (« é »). L'encodage est maintenant détecté : marque d'octets, UTF-8 valide, puis la ligne « 1 CHAR », sinon Windows-1252. Décodeur ANSEL inclus (diacritiques placés avant la lettre). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7e7641dc19
commit
2d35bddf27
4 files changed
+131
-7
No files matched your search
@@ -59,6 +59,7 @@ import { useAnnotations } from './hooks/useAnnotations'
|
||||
import AnnotationToolbar from './components/annotations/AnnotationToolbar'
|
||||
import TextInputOverlay from './components/annotations/TextInputOverlay'
|
||||
import { filiations, getChildren, getParents, getPersonUnions, medias, persons, unions } from './data/mockData'
|
||||
import { readGedcomFile } from './utils/gedcomText'
|
||||
import { loadMediaRefs, loadPhotoRefs, pickFirstFaces, prepareImageForUpload, saveMediaRefs, savePhotoRefs, sizeBucket } from './utils/photoImport'
|
||||
import { fileToBase64, getAccountErrorMessage } from './utils/errorMessages'
|
||||
import { getSiblingParentLinks } from './utils/familyLinks'
|
||||
@@ -2373,7 +2374,7 @@ function App() {
|
||||
let imported = null
|
||||
let importError = ''
|
||||
try {
|
||||
const content = await payload.gedcomFile.text()
|
||||
const content = (await readGedcomFile(payload.gedcomFile)).text
|
||||
imported = content.trim()
|
||||
? await importGedcom(createdTree.id, content, token)
|
||||
: null
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
import PzBusy from './PzBusy.jsx'
|
||||
import TreeVisitsSection from './TreeVisitsSection.jsx'
|
||||
import { t } from '../i18n/index.js'
|
||||
import { readGedcomFile } from '../utils/gedcomText.js'
|
||||
|
||||
function normalizeSlug(value) {
|
||||
return String(value || '')
|
||||
@@ -219,12 +220,8 @@ const AdminPanel = ({
|
||||
}
|
||||
}
|
||||
|
||||
const readFileAsText = (file) => new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => resolve(String(reader.result || ''))
|
||||
reader.onerror = () => reject(new Error('read_failed'))
|
||||
reader.readAsText(file)
|
||||
})
|
||||
// Lu avec le bon encodage (ANSI de Heredis, ANSEL, Mac…), sinon les accents arrivent cassés
|
||||
const readFileAsText = async (file) => (await readGedcomFile(file)).text
|
||||
|
||||
const handleImportGedcom = async () => {
|
||||
if (!treeId || !authToken || saving || !gedcomFile) {
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
// Lire un fichier GEDCOM avec le bon encodage. Le navigateur lit tout en UTF-8 par défaut, alors que beaucoup
|
||||
// de logiciels écrivent autre chose : Heredis PC en « ANSI » (Windows-1252), les anciens logiciels en ANSEL,
|
||||
// Heredis Mac en « MACINTOSH ». Lus en UTF-8, leurs accents deviennent « é » ou « � ».
|
||||
//
|
||||
// Ordre : marque d'octets (BOM) ; UTF-8 si le fichier en est un valide (certains logiciels déclarent ANSI
|
||||
// alors qu'ils écrivent de l'UTF-8) ; sinon l'encodage déclaré par la ligne « 1 CHAR » ; sinon Windows-1252.
|
||||
|
||||
const ANSEL_SPECIALS = {
|
||||
0xa1: 'Ł', 0xa2: 'Ø', 0xa3: 'Đ', 0xa4: 'Þ', 0xa5: 'Æ', 0xa6: 'Œ', 0xa7: 'ʹ', 0xa8: '·', 0xa9: '♭',
|
||||
0xaa: '®', 0xab: '±', 0xac: 'Ơ', 0xad: 'Ư', 0xae: 'ʼ', 0xb0: 'ʻ', 0xb1: 'ł', 0xb2: 'ø', 0xb3: 'đ',
|
||||
0xb4: 'þ', 0xb5: 'æ', 0xb6: 'œ', 0xb7: 'ʺ', 0xb8: 'ı', 0xb9: '£', 0xba: 'ð', 0xbc: 'ơ', 0xbd: 'ư',
|
||||
0xc0: '°', 0xc1: 'ℓ', 0xc2: '℗', 0xc3: '©', 0xc4: '♯', 0xc5: '¿', 0xc6: '¡', 0xc7: 'ß', 0xc8: '€',
|
||||
}
|
||||
|
||||
// Diacritiques ANSEL : écrits AVANT la lettre, rendus par un caractère combinant APRÈS elle
|
||||
const ANSEL_COMBINING = {
|
||||
0xe0: '̉', 0xe1: '̀', 0xe2: '́', 0xe3: '̂', 0xe4: '̃', 0xe5: '̄',
|
||||
0xe6: '̆', 0xe7: '̇', 0xe8: '̈', 0xe9: '̌', 0xea: '̊', 0xeb: '︠',
|
||||
0xec: '︡', 0xed: '̕', 0xee: '̋', 0xef: '̐', 0xf0: '̧', 0xf1: '̨',
|
||||
0xf2: '̣', 0xf3: '̤', 0xf4: '̥', 0xf5: '̳', 0xf6: '̲', 0xf7: '̦',
|
||||
0xf8: '̜', 0xf9: '̮', 0xfa: '︢', 0xfb: '︣', 0xfe: '̓',
|
||||
}
|
||||
|
||||
export function decodeAnsel(bytes) {
|
||||
let out = ''
|
||||
let pending = ''
|
||||
for (const byte of bytes) {
|
||||
if (ANSEL_COMBINING[byte]) {
|
||||
pending += ANSEL_COMBINING[byte]
|
||||
continue
|
||||
}
|
||||
const char = byte < 0x80 ? String.fromCharCode(byte) : (ANSEL_SPECIALS[byte] ?? '�')
|
||||
out += char + pending
|
||||
pending = ''
|
||||
}
|
||||
return (out + pending).normalize('NFC')
|
||||
}
|
||||
|
||||
function declaredCharset(bytes) {
|
||||
const head = new TextDecoder('windows-1252').decode(bytes.subarray(0, 8192))
|
||||
const match = head.match(/^\s*1\s+CHAR\s+([^\r\n]+)/m)
|
||||
return match ? match[1].trim().toUpperCase() : ''
|
||||
}
|
||||
|
||||
function isValidUtf8(bytes) {
|
||||
try {
|
||||
new TextDecoder('utf-8', { fatal: true }).decode(bytes)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function decodeGedcomBytes(input) {
|
||||
const bytes = input instanceof Uint8Array ? input : new Uint8Array(input)
|
||||
if (bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf) {
|
||||
return { text: new TextDecoder('utf-8').decode(bytes.subarray(3)), encoding: 'utf-8' }
|
||||
}
|
||||
if (bytes[0] === 0xff && bytes[1] === 0xfe) return { text: new TextDecoder('utf-16le').decode(bytes.subarray(2)), encoding: 'utf-16le' }
|
||||
if (bytes[0] === 0xfe && bytes[1] === 0xff) return { text: new TextDecoder('utf-16be').decode(bytes.subarray(2)), encoding: 'utf-16be' }
|
||||
// UTF-16 sans marque : « 0 HEAD » commence par « 0 » suivi d'un octet nul
|
||||
if (bytes[0] === 0x30 && bytes[1] === 0x00) return { text: new TextDecoder('utf-16le').decode(bytes), encoding: 'utf-16le' }
|
||||
if (bytes[0] === 0x00 && bytes[1] === 0x30) return { text: new TextDecoder('utf-16be').decode(bytes), encoding: 'utf-16be' }
|
||||
|
||||
const declared = declaredCharset(bytes)
|
||||
if (declared === 'ANSEL') return { text: decodeAnsel(bytes), encoding: 'ansel' }
|
||||
if (isValidUtf8(bytes)) return { text: new TextDecoder('utf-8').decode(bytes), encoding: 'utf-8' }
|
||||
if (declared.startsWith('MAC')) return { text: new TextDecoder('macintosh').decode(bytes), encoding: 'macintosh' }
|
||||
return { text: new TextDecoder('windows-1252').decode(bytes), encoding: 'windows-1252' }
|
||||
}
|
||||
|
||||
export async function readGedcomFile(file) {
|
||||
return decodeGedcomBytes(new Uint8Array(await file.arrayBuffer()))
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { decodeAnsel, decodeGedcomBytes } from './gedcomText.js'
|
||||
|
||||
const latin1 = (text) => Uint8Array.from([...text].map((c) => c.charCodeAt(0)))
|
||||
const utf8 = (text) => new TextEncoder().encode(text)
|
||||
|
||||
describe('encodage d’un fichier GEDCOM', () => {
|
||||
it('Heredis PC en ANSI (Windows-1252) : accents corrects', () => {
|
||||
const bytes = latin1('0 HEAD\n1 CHAR ANSI\n0 @I1@ INDI\n1 NAME Hélène /Lefèvre/\n')
|
||||
const { text, encoding } = decodeGedcomBytes(bytes)
|
||||
expect(encoding).toBe('windows-1252')
|
||||
expect(text).toContain('Hélène /Lefèvre/')
|
||||
})
|
||||
|
||||
it('UTF-8 déclaré ANSI : on garde l’UTF-8 qui est valide', () => {
|
||||
const { text, encoding } = decodeGedcomBytes(utf8('0 HEAD\n1 CHAR ANSI\n1 NAME Émile /Ménard/\n'))
|
||||
expect(encoding).toBe('utf-8')
|
||||
expect(text).toContain('Émile /Ménard/')
|
||||
})
|
||||
|
||||
it('UTF-8 avec marque d’octets', () => {
|
||||
const bytes = new Uint8Array([0xef, 0xbb, 0xbf, ...utf8('0 HEAD\n1 NAME Zoé /Bœuf/\n')])
|
||||
expect(decodeGedcomBytes(bytes)).toEqual({ text: '0 HEAD\n1 NAME Zoé /Bœuf/\n', encoding: 'utf-8' })
|
||||
})
|
||||
|
||||
it('UTF-16 (MyHeritage, certains exports Windows)', () => {
|
||||
const text = '0 HEAD\n1 NAME François /Côté/\n'
|
||||
const le = new Uint8Array(text.length * 2 + 2)
|
||||
le[0] = 0xff; le[1] = 0xfe
|
||||
for (let i = 0; i < text.length; i++) { le[2 + i * 2] = text.charCodeAt(i) & 0xff; le[3 + i * 2] = text.charCodeAt(i) >> 8 }
|
||||
expect(decodeGedcomBytes(le).text).toBe(text)
|
||||
})
|
||||
|
||||
it('Heredis Mac en MACINTOSH', () => {
|
||||
// « é » vaut 0x8E en Mac Roman
|
||||
const bytes = Uint8Array.from([...latin1('0 HEAD\n1 CHAR MACINTOSH\n1 NAME Ren'), 0x8e, ...latin1(' /Dupr'), 0x8e, ...latin1('/\n')])
|
||||
expect(decodeGedcomBytes(bytes).text).toContain('René /Dupré/')
|
||||
})
|
||||
|
||||
it('ANSEL : diacritique avant la lettre', () => {
|
||||
// 0xE2 = accent aigu, 0xF0 = cédille, 0xE8 = tréma
|
||||
expect(decodeAnsel(Uint8Array.from([0x52, 0x65, 0x6e, 0xe2, 0x65]))).toBe('René')
|
||||
expect(decodeAnsel(Uint8Array.from([0x46, 0x72, 0x61, 0x6e, 0xf0, 0x63, 0x6f, 0x69, 0x73]))).toBe('François')
|
||||
expect(decodeAnsel(Uint8Array.from([0x4e, 0x6f, 0xe8, 0x65, 0x6c]))).toBe('Noël')
|
||||
const ged = Uint8Array.from([...latin1('0 HEAD\n1 CHAR ANSEL\n1 NAME '), 0x4c, 0xe2, 0x65, 0x6f, 0x6e, ...latin1(' /'), 0xa6, ...latin1('uf/\n')])
|
||||
expect(decodeGedcomBytes(ged)).toEqual({ text: '0 HEAD\n1 CHAR ANSEL\n1 NAME Léon /Œuf/\n', encoding: 'ansel' })
|
||||
})
|
||||
|
||||
it('sans déclaration et pas de l’UTF-8 : Windows-1252', () => {
|
||||
expect(decodeGedcomBytes(latin1('0 HEAD\n1 NAME Jérôme /Hébert/\n')).text).toContain('Jérôme /Hébert/')
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user