Article technique

Exporter DBF, SYLK et DIF sans page de code déclarée

Des noms de villes chinoises exportés en .dbf sur un PC occidental peuvent revenir en points d'interrogation depuis un export qui signalait le succès, parce que les fichiers dBASE, SYLK et DIF n'ont nulle part où enregistrer un encodage. Un écrivain prudent utilise la page de code ANSI de l'hôte et contrôle chaque nom et chaque valeur contre elle avant le premier octet ; HotXLS refuse ce que cette page ne peut pas tenir

La page de code de l'hôte est le seul encodage que ces fichiers aient

Les trois formats viennent des premières années du PC : dBASE d'Ashton-Tate, SYLK de l'époque Multiplan de Microsoft, DIF de Software Arts, la société VisiCalc. Chacun stocke le texte en octets simples et suppose que le lecteur partage la page de code de l'écrivain — un pari raisonnable quand chaque machine du bureau tournait le même DOS. Sur Windows aujourd'hui, cette page est ce que renvoie GetACP : 1252 sur une installation d'Europe de l'Ouest, 936 en chinois simplifié, 1251 en cyrillique. dBASE IV et FoxPro ont plus tard mis une marque de pilote de langue à l'octet 29 de l'en-tête de table, mais la disposition dBASE III qu'HotXLS écrit laisse cet octet à zéro, et SYLK, DIF et le texte .prn aligné par espaces n'ont aucun champ d'en-tête pour elle. La page de code est donc convenue hors du fichier. HotXLS utilise la page de code ANSI de l'hôte en écriture et en lecture, et contrôle avant d'écrire si chaque caractère a un octet dans cette page

FormatExtension sur SaveAs / OpenEncodage déclaré dans le fichierTexte que la page hôte ne peut pas tenir
Table dBASE III.dbf, plus un fichier mémo .dbt quand nécessaireNon (l'octet 29 reste à zéro)EXlsDbf est levée avant d'écrire, et les .dbf / .dbt tout juste créés sont supprimés
SYLK.slk, .sylkNonSaveAs renvoie -1 et ne crée aucun fichier
DIF.difNonSaveAs renvoie -1 et ne crée aucun fichier
Texte formaté.prnNonSaveAs renvoie -1 et ne crée aucun fichier

Dix octets pour un nom de champ dBASE, comptés dans la page de code

Chaque limite de taille dans une table dBASE est un compte d'octets. C'est là que les pages de code multioctets font mal. Un descripteur de champ garde le nom en onze octets — dix pour le nom, un pour son NUL terminal — et place la lettre de type juste derrière à l'offset 11, la largeur à 16 et le compte de décimales à 17. Sous la page de code 936, un caractère chinois fait deux octets, donc un nom chinois de dix caractères en fait vingt. Copiez ces octets sans les mesurer et ils traversent le NUL, la lettre de type et la largeur ; aucun lecteur ne peut récupérer ce descripteur ensuite, même si le nom a passé une vérification de dix caractères. Gardez les noms de champs dBASE en ASCII même sur un poste CJK. Les outils xBase, les pilotes ODBC et les logiciels SIG qui lisent les tables d'attributs de shapefiles se calent tous sur ces noms, et un nom qui ne se décode que sous 936 devient des octets différents sur chaque machine tournant une autre page

Disposition du descripteur de champ dBASE III DBF avec HotXLS en Delphi : un emplacement de nom de dix octets plus NUL, lettre de type à l'offset 11, largeur à 16 et décimales à 17, où AddField('CITY') tient mais un nom chinois de dix caractères sous la page de code 936 déborde sur vingt octets
Dans un descripteur de champ dBASE de 32 octets, un nom de champ chinois qui passe une vérification de dix caractères déborde sur le NUL, la lettre de type et la largeur sous la page de code 936
OctetsContenu dBASE III
0Version : 0x03, ou 0x83 quand un fichier mémo .dbt accompagne la table
1–3Dernière mise à jour : année depuis 1900, mois, jour
4–7Compte d'enregistrements, 32 bits little-endian
8–11Longueur d'en-tête et longueur d'enregistrement, mots de 16 bits
32 et suivantsUn descripteur de 32 octets par champ, puis 0x0D ; les enregistrements suivent, chacun derrière un drapeau de suppression

Les largeurs sont aussi des octets. Un champ caractère (C) plafonne à 254 octets, les valeurs N sont du texte aligné à droite avec les décimales déclarées, les valeurs D font huit caractères en forme YYYYMMDD, et une valeur trop large pour son champ devient une rangée d'astérisques, la convention que dBASE lui-même emploie. Le texte plus long part dans un mémo (M) portant un numéro de bloc vers le compagnon .dbt : un en-tête de 512 octets, puis des blocs de 512 octets, chaque mémo terminé par 0x1A 0x1A. TXLSXWorkbook.SaveAs avec un nom .dbf transforme la première ligne utilisée de la feuille active en noms de champs, déduit C, N, L ou D par colonne, retombe sur C pour les colonnes mixtes et déplace le texte de plus de 254 caractères vers M. Quand les largeurs doivent être contrôlées en octets, construisez la table vous-même avec TXlsDbfTable et dimensionnez chaque champ C sur la longueur encodée de sa plus longue valeur

uses
  Windows, SysUtils, Variants, lxDbf;

procedure WriteCustomerTable(const FileName: string);
var
  Table: TXlsDbfTable;
  Row: Integer;
begin
  Table := TXlsDbfTable.Create;
  try
    // Noms ASCII d'au plus dix octets ; les largeurs sont des octets dans la page de code hôte
    Table.AddField('CUSTNO', 'N', 6, 0);
    Table.AddField('CITY', 'C', 40, 0);
    Table.AddField('BALANCE', 'N', 12, 2);
    Table.AddField('OPENED', 'D', 8, 0);

    Row := Table.AddRow;
    Table.SetValue(Row, 0, 1001);
    Table.SetValue(Row, 1, WideString(#$5317#$4EAC#$5E02));  // 北京市, 6 octets en CP936
    Table.SetValue(Row, 2, 1234.5);
    Table.SetValue(Row, 3, EncodeDate(2026, 11, 12));

    try
      XlsWriteDbfFile(FileName, Table);
    except
      on E: EXlsDbf do
        // Attendu sur un hôte 1252 : le message nomme le champ "CITY" et la ligne 1,
        // et aucun .dbf ne reste sur disque
        raise Exception.CreateFmt('DBF export refused under code page %d: %s',
          [GetACP, E.Message]);
    end;
  finally
    Table.Free;
  end;
end;

Attendu : sur un hôte 936 la table est écrite et se rouvre avec les quatre champs. Sur un hôte 1252, XlsWriteDbfFile lève EXlsDbf en nommant le champ et la ligne à partir de un. XlsWriteDbfStream convertit chaque nom de champ et chaque valeur C ou M vers la page hôte et en retour, compare, et mesure chaque nom contre ses dix octets avant que l'en-tête ne parte, si bien qu'une table refusée laisse le flux intact. La forme fichier va un pas plus loin et supprime le .dbf, et tout .dbt, qu'elle avait déjà créés

Comment tester si une chaîne survit à la page de code de l'hôte ?

Convertissez-la en AnsiString, reconvertissez cela en WideString, et comparez avec l'original. Un caractère que la page n'a pas revient en remplacement, d'ordinaire ?, si bien que la comparaison échoue précisément quand l'export perdrait des données. Les écrivains emploient la même conversion, donc la vérification et l'écrivain ne peuvent pas être en désaccord sur la page de code. Gardez aussi la longueur en octets : c'est le nombre à opposer à un emplacement de nom de dix octets ou à une largeur de champ

uses
  Variants;

// Vrai quand chaque caractère de V survit à la page de code ANSI de l'hôte ;
// ByteLen est la longueur encodée à comparer à une largeur de champ
function FitsHostCodePage(const V: Variant; out ByteLen: Integer): Boolean;
var
  W: WideString;
  A: AnsiString;
begin
  W := VarToWideStr(V);   // VarToStr peut convertir avant que cette vérification ne voie le texte
  A := AnsiString(W);
  ByteLen := Length(A);
  Result := WideString(A) = W;
end;

Le commentaire sur VarToWideStr porte la vraie leçon. En Delphi 2007 et avant, string est AnsiString partout, et sous Free Pascal l'unité Variants est compilée avec le type string propre à la RTL, AnsiString, quel que soit le mode de votre unité. Sur ces compilateurs, VarToStr a déjà mis un ? là où était chaque caractère chinois. Un garde construit sur AnsiString(VarToStr(V)) compare alors des points d'interrogation avec des points d'interrogation et accepte tout. TStringList a le même genre de problème là-bas : assignez du texte large à sa propriété Text pour couper des lignes et chaque caractère passe par la page de code de l'hôte en chemin. XlsParseSlk, XlsParseDifText et XlsParsePrnText prennent une entrée WideString et gardent chaque caractère sur n'importe quel hôte. Si vous construisez aussi avec Lazarus, voyez quel type de chaîne les unités HotXLS compilent sous Free Pascal

Vérification aller-retour de la page de code ANSI hôte par HotXLS pour l'export DBF, SYLK, DIF et PRN en Delphi : AnsiString puis retour en WideString garde le texte chinois sur un hôte 936 mais donne des points d'interrogation sur 1252, refusé avec EXlsDbf ou SaveAs renvoyant -1
Convertir chaque valeur vers la page de code hôte et en retour expose les caractères perdus avant qu'un octet n'atteigne le disque, pourvu que le texte ait été lu avec VarToWideStr plutôt que VarToStr

Les enregistrements SYLK et DIF, ligne par ligne

SYLK est un enregistrement par ligne, champs séparés par des points-virgules. HotXLS écrit un en-tête ID;PHotXLS, un enregistrement C;Yligne;Xcol;Kvaleur par cellule et un terminateur E. Le texte est quoté avec les guillemets incrustés doublés, ce qui protège aussi un point-virgule dans une valeur, et le lecteur accepte l'échappement ;; que certains producteurs emploient à la place. Pas de booléen. Les écrivains propres au format envoient 1 et 0, donc c'est ce qu'écrit SaveAs ; les erreurs partent en texte comme #N/A, les dates en numéros de série. Ces deux lettres en haut sont la façon dont Excel reconnaît SYLK — et pourquoi il prend un CSV dont la première cellule lit ID pour un fichier SYLK. Construction et parse travaillent toutes deux sur WideString, si bien que l'aller-retour en mémoire ci-dessous donne la même réponse sur n'importe quel hôte

uses
  SysUtils, lxSlk;

function SylkRoundTripsInMemory: Boolean;
var
  Cells, Back: TXLSSlkCells;
  Text: WideString;
  I: Integer;
begin
  SetLength(Cells, 3);
  Cells[0].Row := 1; Cells[0].Col := 1; Cells[0].Kind := slkText;
  Cells[0].Text := WideString(#$4E0A#$6D77'; Pudong');   // 上海; Pudong
  Cells[1].Row := 1; Cells[1].Col := 2; Cells[1].Kind := slkText;
  Cells[1].Text := 'Pipe 12" steel';
  Cells[2].Row := 2; Cells[2].Col := 1; Cells[2].Kind := slkNumber;
  Cells[2].Number := 1234.5;

  Text := XlsBuildSlk(Cells);           // K"上海; Pudong", K"Pipe 12"" steel", K1234.5
  Result := XlsParseSlk(Text, Back) = Length(Cells);
  for I := 0 to High(Cells) do
    if Result then
      Result := (Back[I].Row = Cells[I].Row) and (Back[I].Col = Cells[I].Col) and
        (Back[I].Kind = Cells[I].Kind) and (Back[I].Text = Cells[I].Text) and
        ((Cells[I].Kind <> slkNumber) or (Back[I].Number = Cells[I].Number));
end;

DIF est plus bavard. Quatre groupes d'en-tête, TABLE, VECTORS (compte de colonnes), TUPLES (compte de lignes) et DATA, prennent trois lignes chacun ; chaque élément de données prend ensuite deux lignes, une ligne type,nombre et une ligne portant la chaîne quotée ou un indicateur de valeur. Une ligne s'ouvre avec -1,0 et BOT, et -1,0 avec EOD ferme les données. Un nombre est 0,valeur plus V, un logique 0,1 plus TRUE ou 0,0 plus FALSE, une chaîne 1,0 plus son texte quoté. En lecture, HotXLS mappe les indicateurs NA et ERROR vers #N/A et #VALUE! et traite NULL comme vide. Une feuille nommée Orders dont la seule ligne porte Beijing, 1234.5 et True exporte comme :

TABLE
0,1
"Orders"
VECTORS
0,3
""
TUPLES
0,1
""
DATA
0,0
""
-1,0
BOT
1,0
"Beijing"
0,1234.5
V
0,1
TRUE
-1,0
EOD

Une virgule décimale casse les trois formats

La page de code n'est pas le seul réglage machine qui fuite dans ces fichiers. Sur un poste avec des paramètres régionaux allemands ou français, FloatToStr transforme 1234.5 en 1234,5. En DIF, la ligne d'élément devient 0,1234,5 et perd sa forme type,valeur. En SYLK, K1234,5 n'est pas un nombre. Dans un champ N dBASE, les lecteurs xBase attendent un point. HotXLS écrit et lit les trois avec TFormatSettings.Invariant : SYLK obtient le jeton le plus court qui se relit vers le même double, DIF un FloatToStr invariant avec .0 ajouté aux nombres entiers, les champs numériques dBASE leurs décimales déclarées, alignées à droite. Le parse ignore aussi les paramètres régionaux, si bien qu'un fichier venu d'une machine à virgule décimale se lit pareil partout

Export SYLK et DIF avec HotXLS en Delphi : enregistrements C;Y;X;K avec en-tête ID;PHotXLS et booléen en 1, paires d'éléments DIF type,nombre entre BOT et EOD, et TFormatSettings.Invariant gardant 1234.5 intact là où une virgule décimale casse DIF, SYLK et dBASE
Écrire la ligne Orders en enregistrements SYLK et paires d'éléments DIF montre où des paramètres régionaux à virgule décimale corrompraient chaque format, et pourquoi HotXLS formate les nombres avec des réglages invariants

Lire des tables qu'autres programmes ont écrites

Ouvrir la table de quelqu'un d'autre met la question de la page de code sur la machine de lecture. XlsReadDbfFile, et TXLSXWorkbook.Open sur un nom .dbf, acceptent les octets de version 0x03, 0x83, 0x8B, 0x30, 0x31 et 0x32, couvrant les tables dBASE III et IV et Visual FoxPro, y compris les types binaires FoxPro I (entier 32 bits), B (double), Y (monétaire multiplié par 10 000) et T (jour julien plus millisecondes). Le texte mémo vient d'un .dbt à côté de la table, ou d'un .fpt avec la disposition de blocs FoxPro quand aucun .dbt n'existe ; l'extension décide, si bien qu'aucun compagnon n'est lu pour l'autre. Les données caractères se décodent dans la page hôte. Une table dBASE III écrite par un programme DOS sous la page de code 866, avec l'octet 29 laissé à zéro, montre donc les mauvaises lettres sur une machine 1252. Quand vous connaissez le producteur, lancez l'import là où sa page de code est la page système. Les fichiers SYLK, DIF et .prn suivent la même règle, parce que Open décode leurs octets dans la page hôte avant que le parseur de texte large ne les voie

Quel format doit porter le texte non ASCII entre locales ?

Aucun de ces quatre, au choix. Utilisez-les quand le système recevant insiste — un écran d'import ERP, une application xBase, un pipeline SIG — et quand les données sont en ASCII ou que les deux bouts partagent une page de code. Tout le reste devrait voyager dans un format qui déclare son encodage : XLSX, SpreadsheetML 2003, ou CSV, qu'HotXLS écrit en UTF-8 avec marque d'ordre d'octet (l'export CSV et TSV a ses propres pièges de transport). Comme SaveAs choisit le format d'après l'extension, votre code doit gérer les deux styles de refus dès la première table : une exception pour .dbf, un résultat de -1 pour les formats texte

uses
  SysUtils, lxHandleX, lxDbf;

function SaveForLegacyImport(const BaseName: string): string;
var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
begin
  Book := TXLSXWorkbook.Create;
  try
    Sheet := Book.Sheets.Add('Orders');   // la première feuille ajoutée devient active
    Sheet.Cells[1, 1].Value := 'CUSTNO';
    Sheet.Cells[1, 2].Value := 'CITY';
    Sheet.Cells[2, 1].Value := 1001;
    Sheet.Cells[2, 2].Value := WideString(#$5317#$4EAC);  // 北京

    Result := BaseName + '.slk';
    if Book.SaveAs(Result) <> 1 then
    begin
      // La page de code active ne peut pas tenir le texte : aucun .slk créé
      Result := BaseName + '.xlsx';
      Book.SaveAs(Result);
    end;

    try
      Book.SaveAs(BaseName + '.dbf');
    except
      on E: EXlsDbf do
        Result := Result + ' (no .dbf: ' + E.Message + ')';
    end;
  finally
    Book.Free;
  end;
end;

Sur un hôte 936, cette fonction produit un .slk et un .dbf qui portent tous deux 北京 ; sur un hôte 1252, elle produit un .xlsx et rapporte pourquoi le .dbf a été refusé. Les noms d'entrées RTF, HTML et ZIP dans le composant tableur Delphi HotXLS suivent des règles d'encodage qui leur sont propres