Задача: используя наименьшее возможное количество ресурсов, отрендерить осмысленный текст.


  • Насколько маленьким может быть читаемый шрифт?
  • Сколько памяти понадобится, чтобы его хранить?
  • Сколько кода понадобится, чтобы его использовать?

Посмотрим, что у нас получится. Спойлер:



Введение в битмэпы


Компьютеры представляют растровые изображения в виде битмэпов. Речь не о формате .bmp, а о способе хранения пикселей в памяти. Для понимания происходящего нам надо кое-что про этот способ узнать.


Слои


Изображение обычно содержит несколько слоёв, расположенных один поверх другого. Чаще всего они соответствуют координатам цветового пространства RGB. Один слой для красного, один для зелёного и один для синего. Если формат изображения поддерживает прозрачность, то для неё создаётся четвёртый слой, обычно называемый альфа. Грубо говоря, цветное изображение — это три (или четыре, если есть альфа-канал) чёрно-белых, расположенных одно над другим.


  • RGB — не единственное цветовое пространство; формат JPEG, например, использует YUV. Но в этой статье остальные цветовые пространства нам не понадобятся, поэтому мы их не рассматриваем.

Набор слоёв может быть представлен в памяти двумя способами. Либо они хранятся по отдельности, либо значения из разных слоёв перемежаются. В последнем случае слои называются каналами, и именно так устроено большинство современных форматов.


Допустим, у нас есть рисунок 4x4, содержащий три слоя: R для красного, G для зелёного и B для синего компонента каждого из пикселей. Он может быть представлен вот так:


  R R R R
  R R R R
  R R R R
  R R R R

  G G G G
  G G G G
  G G G G
  G G G G

  B B B B
  B B B B
  B B B B
  B B B B

Все три слоя хранятся по отдельности. Перемежающийся формат выглядит иначе:


  RGB RGB RGB RGB
  RGB RGB RGB RGB
  RGB RGB RGB RGB
  RGB RGB RGB RGB

  • каждая тройка символов соответствует ровно одному пикселю
  • значения в пределах тройки идут в порядке RGB. Иногда может использоваться и другой порядок (например, BGR), но этот — самый распространённый.

Для простоты я расположил пиксели в виде двухмерной матрицы, потому что так понятнее, где в изображении находится та или иная тройка. Но на самом деле память компьютера не двумерная, а одномерная, поэтому рисунок 4х4 будет храниться вот так:


RGB RGB RGB RGB RGB RGB RGB RGB RGB RGB RGB RGB RGB RGB RGB RGB

bpp


Аббревиатурой bpp обозначается количество битов или байт на пиксель (bits/bytes per pixel). Вам могло где-то попадаться на глаза 24bpp или 3bpp. Эти две характеристики означают одно и то же — 24 бита на пиксель или 3 байта на пиксель. Так как в байте всегда 8 бит, по величине значения можно догадаться, о какой из единиц идёт речь.


Представление в памяти


24bpp, он же 3bpp — самый распостранённый формат для хранения цветов. Вот так на уровне отдельных битов выглядит один пиксель в порядке RGB.


бит     0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
пиксель R  R  R  R  R  R  R  R  G  G  G  G  G  G  G  G  B  B  B  B  B  B  B  B

  • Один байт для R, один для G и один для B, итого три байта.
  • Каждый из них содержит значение от 0 до 255.

Так что если данный пиксель имеет следующий цвет:


  • R 255
  • G 80
  • B 100

Тогда в первом байте хранится 255, во втором 80, а в третьем — 100.


Чаще всего эти значения представляются в шестнадцатеричном виде. Скажем, #ff5064. Так гораздо удобнее и компактнее: R = 0xff (т.е. R=255 в десятеричном представлении), G = 0x50 (=G=80), B=0x64 (=B=100).


  • У шестнадцатеричного представления есть одно полезное свойство. Так как каждый байт цвета представлен двумя символами, каждый символ кодирует ровно пол-байта, или четыре бита. 4 бита, кстати, называются ниббл.

Ширина строки


Когда пиксели идут один за другим и каждый содержит больше одного канала, в данных легко запутаться. Неизвестно, когда заканчивается одна строка и начинается следующая, поэтому для интерпретации файла с битмэпом нужно знать размер изображения и bpp. В нашем случае рисунок имеет ширину w = 4 пикселя и каждый из этих пикселей содержит 3 байта, поэтому строка кодируется 12 (в общем случае w*bpp) байтами.


  • Строка не всегда кодируется ровно w*bpp байтами; часто в неё добавляются "скрытые" пиксели, чтобы довести ширину изображения до какой-то величины. Например, масштабировать рисунки быстрее и удобнее, когда их размер в пикселях равен степени двойки. Поэтому файл может содержать (доступное пользователю) изображение 120х120 пикселей, но храниться как изображение 128х128. При выводе изображения на экран эти пиксели игнорируются. Впрочем, нам о них знать не обязательно.

Координата любого пикселя (x, y) в одномерном представлении — (y * w + x) * bpp. Это, в общем-то, очевидно: y — номер строки, каждая строка содержит w пикселей, так что y * w — это начало нужной строки, а+x переносит нас к нужному x в её пределах. А так как координаты не в байтах, а в пикселях, всё это умножается на размер пикселя bpp, в данном случае в байтах. Так как пиксель имеет ненулевой размер, нужно прочитать ровно bpp байт, начиная с полученной координаты, и у нас будет полное представление нужного пикселя.


Атлас шрифта


Реально существующие мониторы отображают не пиксель как единое целое, а три субпикселя — красный, синий и зелёный. Если посмотреть на монитор под увеличением, то вы увидите что-то вроде вот этого:




Нас интересует LCD, так как, скорее всего, именно с такого монитора вы и читаете этот текст. Разумеется, есть подводные камни:


  • Не все матрицы используют именно такой порядок субпикселей, бывает и BGR.
  • Если повернуть монитор (например, смотреть на телефоне в альбомной ориентации), то паттерн тоже повернётся и шрифт перестанет работать.
  • Разные ориентации матрицы и расположение субпикселей потребуют переделки самого шрифта.
  • В частности, он не работает на AMOLED-дисплеях, использующих расположение PenTile. Такие дисплеи чаще всего используются в мобильных устройствах.

Использование связанных с субпикселями хаков для увеличения разрешения называется субпиксельным рендерингом. О его применении в типографике можно почитать, например, здесь.


К счастью для нас, Мэтт Сарнов уже догадался использовать субпиксельный рендеринг для создания крошечного шрифта millitext. Вручную он создал вот эту крошечную картинку:



Которая, если очень внимательно вглядываться в монитор, выглядит вот так:



А вот она же, программно увеличенная в 12 раз:



Отталкиваясь от его работы, я создал атлас шрифта, в котором каждому символу соответствует колонка 1x5 пикселей. Порядок символов следующий:


0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ


Тот же атлас, увеличенный в 12 раз:



С 36 используемыми символами получается ровно 36х5 пикселей. Если считать, что каждый пиксель занимает 3 байта, то нам нужно 36*5*3 = 540 байт на то, чтобы хранить весь рисуонк (прим. пер.: в оригинале запутанная серия правок по поводу альфа-канала, удаления метаданных и т.п. В переводе я её опустил и использую только окончательный вариант файла). PNG-файл, пропущенный через pngcrush и optipng, занимает даже меньше:


# wc -c < font-crushed.png
390

Но можно добиться ещё меньшего размера, если использовать слегка другой подход


Сжатие


Внимательный читатель мог заметить, что атлас использует всего 7 цветов:


  1. #ffffff
  2. #ff0000
  3. #00ff00
  4. #0000ff
  5. #00ffff
  6. #ff00ff
  7. #ffff00

Палитра


В таких ситуациях часто проще создать палитру. Тогда для каждого пикселя можно хранить не три байта цвета, а только номер цвета в палитре. В нашем случае для выбора из 7 цветов будет достаточно 3 бит (7 < 2^3). Если каждому пикселю сопоставить трёхбитное значение, то весь атлас поместится в 68 байт.


  • Читатель, разбирающийся в сжатии данных, может ответить, что вообще-то есть такая штука как "дробные биты" и в нашем случае достаточно 2.875 бит на пиксель. Такой плотности можно добиться с помощью чёрной магии, известной как арифметическое кодирование. Мы этого делать не будем, потому что арифметическое кодирование — сложная штука, а 68 байт — это и так немного.

Выравнивание


У трёхбитного кодирования есть один серьёзный недостаток. Пиксели не могут быть равномерно распределены по 8-битным байтам, что важно, потому что байт — минимальный адресуемый участок памяти. Допустим, мы хотим сохранить три пикселя:


A B C

Если каждый занимает 3 бита, то на их хранение уйдёт 2 байта (- обозначает неиспользуемые биты):


bit   0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15
pixel A  A  A  B  B  B  C  C  C  -  -  -  -  -  -  -

Что важно, пиксель C не просто оставляет кучу пустого места; он разорван между двумя байтами. Когда мы начнём добавлять следующие пиксели, они могут быть расположены как угодно относительно границ байтов. Простейшим решением будет использовать по нибблу на пиксель, потому что 8 прекрасно делится на 4 и позволяет помещать в каждый байт ровно по два пикселя. Но это увеличит размер атласа на треть, с 68 байт до 90 байт.


  • На самом деле файл можно сделать ещё меньше, используя кодирование палиндромов, интервальное кодирование и другие методики сжатия. Как и арифметическое кодирование, эти методики мы отложим до следующей статьи.

Битовый буфер


К счастью, в работе с 3-битными значениями нет ничего принципиально невозможного. Нужно просто следить за тем, какую позицию внутри байта мы пишем или читаем в данный момент. Следующий простенький класс конвертирует 3-битный поток данных в массив байтов.


  • Из соображений читаемости код написан на JS, но тот же метод генерализуется на другие языки.
  • Используется порядок от младшего байта к старшему (Little Endian)

class BitBuffer {
  constructor(bytes) {
    this.data = new Uint8Array(bytes);
    this.offset = 0;
  }
  write(value) {
    for (let i = 0; i < 3; ) {
      // bits remaining
      const remaining = 3 - i;

      // bit offset in the byte i.e remainder of dividing by 8
      const bit_offset = this.offset & 7;

      // byte offset for a given bit offset, i.e divide by 8
      const byte_offset = this.offset >> 3;

      // max number of bits we can write to the current byte
      const wrote = Math.min(remaining, 8 - bit_offset);

      // mask with the correct bit-width
      const mask = ~(0xff << wrote);

      // shift the bits we want to the start of the byte and mask off the rest
      const write_bits = value & mask;

      // destination mask to zero all the bits we're changing first
      const dest_mask = ~(mask << bit_offset);
      value >>= wrote;

      // write it
      this.data[byte_offset] = (this.data[byte_offset] & dest_mask) | (write_bits << bit_offset);

      // advance
      this.offset += wrote;
      i += wrote;
    }
  }
  to_string() {
    return Array.from(this.data, (byte) => ('0' + (byte & 0xff).toString(16)).slice(-2)).join('');
  }
};

Давайте загрузим и закодируем файл с атласом:


const PNG = require('png-js');
const fs = require('fs');

// this is our palette of colors
const Palette = [
  [0xff, 0xff, 0xff],
  [0xff, 0x00, 0x00],
  [0x00, 0xff, 0x00],
  [0x00, 0x00, 0xff],
  [0x00, 0xff, 0xff],
  [0xff, 0x00, 0xff],
  [0xff, 0xff, 0x00]
];

// given a color represented as [R, G, B], find the index in palette where that color is
function find_palette_index(color) {
  const [sR, sG, sB] = color;
  for (let i = 0; i < Palette.length; i++) {
    const [aR, aG, aB] = Palette[i];
    if (sR === aR && sG === aG && sB === aB) {
      return i;
    }
  }
  return -1;
}

// build the bit buffer representation
function build(cb) {
  const data = fs.readFileSync('subpixels.png');
  const image = new PNG(data);
  image.decode(function(pixels) {
    // we need 3 bits per pixel, so w*h*3 gives us the # of bits for our buffer
    // however BitBuffer can only allocate bytes, dividing this by 8 (bits for a byte)
    // gives us the # of bytes, but that division can result in 67.5 ... Math.ceil
    // just rounds up to 68. this will give the right amount of storage for any
    // size atlas.
    let result = new BitBuffer(Math.ceil((image.width * image.height * 3) / 8));
    for (let y = 0; y < image.height; y++) {
      for (let x = 0; x < image.width; x++) {
        // 1D index as described above
        const index = (y * image.width + x) * 4;
        // extract the RGB pixel value, ignore A (alpha)
        const color = Array.from(pixels.slice(index, index + 3));
        // write out 3-bit palette index to the bit buffer
        result.write(find_palette_index(color));
      }
    }
    cb(result);
  });
}

build((result) => console.log(result.to_string()));

Как и ожидалось, атлас поместился в 68 байт, что в 6 раз меньше PNG-файла.


(прим. пер.: автор несколько лукавит: он не сохранил палитру и размер изображения, что по моим прикидкам потребует 23 байт при фиксированном размере палитры и увеличит размер изображения до 91 байта)


Теперь давайте сконвертируем изображение в строку, чтобы можно было вставить его в исходный код. По сути, метод to_string это и делает: он представляет содержимое каждого байта в виде шестнадцатеричного числа.


305000000c0328d6d4b24cb46d516d4ddab669926a0ddab651db76150060009c0285
e6a0752db59054655bd7b569d26a4ddba053892a003060400d232850b40a6b61ad00

Но получившаяся строка всё ещё довольно длинная, потому что мы ограничили себя алфавитом из 16 символов. Можно заменить его на base64, в котором символов вчетверо больше.


to_string() {
  return Buffer.from(this.data).toString('base64');
}

В base64 атлас выглядит вот так:


MFAAAAwDKNbUsky0bVFtTdq2aZJqDdq2Udt2FQBgAJwCheagdS21kFRlW9e1adJqTdugU4kqADBgQA0jKFC0CmthrQA=

Эту строку можно захардкодить в JS-модуль и использовать для растеризации текста.


Растеризация


Для экономии памяти в каждый момент будет декодироваться только одна буква.


const Alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
const Atlas = Uint8Array.from(Buffer.from('MFAAAAwDKNbUsky0bVFtTdq2aZJqDdq2Udt2FQBgAJwCheagdS21kFRlW9e1adJqTdugU4kqADBgQA0jKFC0CmthrQA=', 'base64'));
const Palette = [
  [0xff, 0xff, 0xff],
  [0xff, 0x00, 0x00],
  [0x00, 0xff, 0x00],
  [0x00, 0x00, 0xff],
  [0x00, 0xff, 0xff],
  [0xff, 0x00, 0xff],
  [0xff, 0xff, 0x00]
];

// at the given bit offset |offset| read a 3-bit value from the Atlas
read = (offset) => {
  let value = 0;
  for (let i = 0; i < 3; ) {
    const bit_offset = offset & 7;
    const read = Math.min(3 - i, 8 - bit_offset);
    const read_bits = (Atlas[offset >> 3] >> bit_offset) & (~(0xff << read));
    value |= read_bits << i;
    offset += read;
    i += read;
  }
  return value;
};

// for a given glyph |g| unpack the palette indices for the 5 vertical pixels
unpack = (g) => {
  return (new Uint8Array(5)).map((_, i) =>
    read(Alphabet.length*3*i + Alphabet.indexOf(g)*3));
};

// for given glyph |g| decode the 1x5 vertical RGB strip
decode = (g) => {
  const rgb = new Uint8Array(5*3); 
  unpack(g).forEach((value, index) =>
    rgb.set(Palette[value], index*3));
  return rgb;
}

Функция decode принимает на вход символ и возвращает соответствующий столбец исходного изображения. Впечатляет тут то, что на декодирование одного символа уходит всего 5 байт памяти, плюс ~1.875 байт на то, чтобы прочитать нужный кусок массива, т.е. в среднем 6.875 на букву. Если прибавить 68 байт на хранение массива и 36 байт на хранение алфавита, то получится, что теоретически можно рендерить текст со 128 байтами RAM.


  • Такое возможно, если переписать код на C или ассемблере. На фоне оверхеда JS это экономия на спичках.

Осталось только собрать эти столбцы в единое целое и вернуть картинку с текстом.


print = (t) => {
  const c = t.toUpperCase().replace(/[^\w\d ]/g, '');
  const w = c.length * 2 - 1, h = 5, bpp = 3; // * 2 for whitespace
  const b = new Uint8Array(w * h * bpp);
  [...c].forEach((g, i) => {
    if (g !== ' ') for (let y = 0; y < h; y++) {
      // copy each 1x1 pixel row to the the bitmap
      b.set(decode(g).slice(y * bpp, y * bpp + bpp), (y * w + i * 2) * bpp);
    }
  });
  return {w: w, h: h, data: b};
};

Это и будет минимальный возможный шрифт.


const fs = require('fs');
const result = print("Breaking the physical limits of fonts");
fs.writeFileSync(`${result.w}x${result.h}.bin`, result.data);

Добавим немножко imagemagick, чтоб получить изображение в читаемом формате:


# convert -size 73x5 -depth 8 rgb:73x5.bin done.png

И вот финальный результат:



Оно же, увеличенное в 12 раз:



Оно же, снятое макросъёмкой с плохо откалиброванного монитора:



И наконец, оно же на мониторе получше:


Комментарии (63)


  1. JTG
    19.07.2019 17:28
    +18

    Пост будет неполным без ссылки на «Властелина Колец», набранного этим шрифтом files.morr.cc/lotr1.png


    1. VereNick
      23.07.2019 07:36

      Хм, есть пруфы что это «Властелин Колец»?


      1. synedra Автор
        23.07.2019 07:38

        Первые слова текста "THE LORD OF THE RINGS PART ONE THE FELLOWSHIP OF THE RING J.R.R. TOLKIEN". Что-то мне подсказывает, что это таки не Библия короля Якова.


        1. Anynickname
          23.07.2019 20:43

          Скачал счас ВК в txt, получилось порядка 2.8 Мб. Стало быть 2.8М символов (с учетом пробелов, пунктуации и переносов строк). Там в конце куча аппендиксов, в начале содержание, номера страниц и глав, т.ч. если всё это сократить, то можно допустить, что основной объем текста будет в р-не 2+ М.

          По данным интернетов, в трилогии порядка 500к слов, что косвенно подтверждает оценку в 2М или даже 2,5М символов (при средней длине слова в английском в р-не 4 символов). По крайней мере расхождение результата с фактом при такой оценке вряд ли будет в разы.

          Приведённый файл, если обрезать ~20пикс. поля, имеет разрешение 3460х3460 пикселов, при размере символа (5+1)*(1+1)=12 кв. пикс. выходит чуть меньше миллиона символов, что в 2 раза меньше ожидаемой цифры.

          Что не так?


  1. KvanTTT
    19.07.2019 17:49
    +1

    Которая, если очень внимательно вглядываться в монитор, выглядит вот так:

    Я бы сказал, что для этого нужно смотреть в монитор под лупой.


    1. synedra Автор
      19.07.2019 18:15
      +3

      У меня получалось разобрать отдельные глифы невооружённым взглядом. Зависит от разрешения монитора, наверное.


      1. DistortNeo
        19.07.2019 22:15
        +1

        Ага. Попытался — вообще без шансов. Монитор 4K, 140 ppi. Невооружённым глазом субпиксели вообще не видно — потому всем так ненавистный ClearType работает просто замечательно. Только при макросъёмке при примерно с 10 попытки получилось сделать чёткий кадр.


      1. Moskus
        19.07.2019 23:06
        +1

        На экране с высоким разрешением (телефон HD, настольный монитор 4k, Retina) или мониторе с матовым покрытием невооруженным глазом такое прочитать, практически, невозможно.


    1. lyadnov
      19.07.2019 19:35

      можно еще через камеру смартфона разглядывать


    1. pda0
      19.07.2019 20:14
      +1

      Так вот что там разглядывал Бастрыкин! А мы-то, дураки, зря потешались над ним…


  1. stalinets
    19.07.2019 19:21
    +7

    Самые мелкие шрифты, с которыми я сталкивался в реале (помимо условий кредитов =)) — это шрифты из J2ME приложений для мобильников середины 2000-х годов. У меня, например, был Siemens C65, с экранчиком 130*130 пикселей, и вот такие шрифты были на нём вполне читабельными (хотя и считались «мелкими»):


    Эти картинки и есть шрифт, я их сейчас вытащил из тех самых .jar файлов, которые запускал на своём сименсе в 2005-м.
    Такой шрифт можно было установить в ридере для книг, или добавить в конструкторе для Jimm ICQ.


    1. mistergrim
      19.07.2019 19:53

      С 36 используемыми символами получается ровно 36х5 пикселей. Если считать, что каждый пиксель занимает 3 байта, то нам нужно 36*5*3 = 540 байт на то, чтобы хранить весь рисуонк
      Ну и зачем это? Если можно сделать нормальный шрифт 8?5, в котором каждый символ будет занимать 5 байтов.
      Имхо, идея интересна лишь тем, как впихнуть на экран низкого разрешения как можно больше символов.


      1. synedra Автор
        19.07.2019 20:07

        Незачем, помимо эстетической ценности. Я даже представить себе не могу таких обстоятельств, когда надо будет настолько экономить байты, но при этом будет доступен многоцветный LCD-экранчик.


        1. ardraeiss
          20.07.2019 21:31

          Например, если делать в стиле «ностальгии»/«ретро». Есть же сейчас мода на «пиксельные игры», где под капотом нормальные SDL или 3D сидят.
          Взять такой шрифт, порендерить квадратами, занести результат на экране модели условно древней мобилки — и готов элемент окружения «а-ля нулевые» для какой-нибудь игры, причём вполне допускающий осмысленную сюжетную интерактивность(кто сказал «Steins;Gate»?).


      1. saege5b
        19.07.2019 21:12
        +1

        Шпаргалки :)


      1. cheburen
        20.07.2019 16:01

        Это неплохой шрифт для стеганографии, при вертикальной записи, простая линия на сайте превращается в текст при внимательном рассмотрении.


        1. mistergrim
          20.07.2019 16:02
          +2

          Смысл стеганографии в том, чтобы она и при внимательном рассмотрении была необнаружима.


    1. iLLuzor
      19.07.2019 21:48
      +1

      А я такие шрифты тогда рисовал. Особенно весело было с китайскими.


    1. engine9
      19.07.2019 23:41

      Ох, такую ностальгию вызвали! Помню, была софтина, позволявшая много-много текста запихнуть в приложение и потом его довольно комфортно просматривать. Я так шпоры делал.


  1. Alexey2005
    19.07.2019 21:30
    +2

    Англоговорящим-то хорошо, у них буквы компактные. А вот в русском есть такие монстры, как например Ы, Щ, Ж, Ю, Д, которые ещё попробуй вмести даже в 4x8, чтоб оно не сливалось в бесформенную кляксу.


    1. Anynickname
      19.07.2019 22:01
      +1

      В данном шрифте больше всего места займёт Ё (2х7, или 14 кв. пикс.), что больше, чем Щ (по-минимуму, 6 кв. пикс.).


    1. juray
      21.07.2019 01:12

      В шрифте 3х5 с «Ж» и «Ю» извратились — набок положили
      image


      1. KvanTTT
        21.07.2019 12:39
        +1

        Это выглядит неплохо и оригинально.


        1. Lain_13
          23.07.2019 15:40

          Пожалуй лучше всего то, что это совершенно бес проблем читается в контексте. Можно даже не заметить, что они боком повёрнуты.


          1. KvanTTT
            23.07.2019 15:49

            Тсект буедт лгкео чиаттсья даже елси перамешеть все бвукы втнури свлоа, крмое пвоерй и подесленй :)


            1. Lain_13
              23.07.2019 17:06

              И это тоже. Удивительно как легко мозг адаптируется.


  1. Rim13
    19.07.2019 21:47
    +3

    ". Мы этого делать не будем, потому что арифметическое кодирование — сложная штука, а 68 байт — это и так немного."

    Если уж начали делать минимализм(который никому не нужен), то делайте его полноценным.


    1. synedra Автор
      20.07.2019 06:37

      Это правда. Мне тоже в статье не хватило выжимания ещё десятка байт каким-нибудь хитрым сжатием.


      1. Rim13
        20.07.2019 09:27

        «На самом деле файл можно сделать ещё меньше, используя кодирование палиндромов, интервальное кодирование и другие методики сжатия. Как и арифметическое кодирование, эти методики мы отложим до следующей статьи.»

        А есть это статья?


        1. synedra Автор
          21.07.2019 08:18

          Насколько мне известно, нет. В репозитории оригинала последний коммит что-то около месяца назад, так что, может быть, ещё пишется.


          1. KvanTTT
            21.07.2019 12:46

            Я видел пост еще в конце 2018 года: 1px-wide font. Правда эта статья намного более подробная.


  1. resetme
    19.07.2019 22:23

    Получается что в таком шрифте 5 позиций для кодирования. Это мне напомнило азбуку Морзе. Если записать те же буквы вертикальными столбцами в азбуке Морзе, то читать будет даже проще.


    1. Tyusha
      20.07.2019 15:41
      +1

      Вы ничего не поняли. Вопрос не в том, чтобы закодировать азбуку, а в том, чтобы создать шрифт.


  1. kinguru
    19.07.2019 22:28

    Помойму это не очень читаемо :)


    1. KvanTTT
      20.07.2019 01:42
      +3

      Вы перепутали слова: очень не читаемо)


  1. sidristij
    19.07.2019 23:08
    +7

    Вы сейчас открыли новые возможности для написания неудобных пунктов договоров мелким шрифтом


    1. FluktLight
      21.07.2019 08:18

      Это не мелкий текст, а просто подложка документа…
      Подписывайте быстрее.


  1. engine9
    19.07.2019 23:43
    +2

    Элегантно! Напомнило как делали в старину дополнительные цвета.

    Наглядно



    1. JKot
      20.07.2019 07:34

      Вот только эта техника была продемонстрирована в 2015, а не в старину.


      1. engine9
        20.07.2019 18:18

        О как. Спасибо, что поправили.


      1. sacred1972
        21.07.2019 23:42
        +1

        Да, такое количество цветов на CGA — недавнее открытие. Но более 4-х (что превышает документированные возможности) использовалось во многих старых играх nerdlypleasures.blogspot.com/2013/11/ibm-pc-color-composite-graphics.html
        Ну доступно это было, конечно только на композитных мониторах.


  1. vanxant
    20.07.2019 03:08

    В детстве баловался шрифтами типа 3х6 для Спектрума, и они были намного читаемее чем вот это вот.


  1. Crevice
    20.07.2019 06:56

    Сфотографировал телефоном с монитора. Не ожидал, что будет так отлично видно.
    image


  1. aik
    20.07.2019 09:08

    То, что в статье приводится как «программно увеличенное» нечитаемо вообще, даже если знаешь, что там написано.
    А вот в оригинальном размере читаемо только под лупой. Действительно разборчиво. Но под лупой, которую надо подносить к монитору.


    1. Maximuzzz
      20.07.2019 15:04
      +9

      Так оно и не должно быть читаемо, по сути «программно увеличенное» изображение показывает как именно (каким цветом) закодирован символ. Каждый цветной пиксель разбивается дисплеем на 3 составляющие, в виду технических особенностей, и получается «магия». Собственно в статье об этом и рассказывается.


  1. McAaron
    20.07.2019 12:03
    -1

    1) Насколько маленьким может быть читаемый шрифт?
    Очень неопределнный вопрос. Сразу возникает уточняющий вопрос:
    — на каком устройстве? Например, используя лазерный принтер, можно печатать на качественной пленке с разрешением 1200x600 dpi — это выдает по паспорту мой принтер и я его использовал для печати миры, чтобы проверить возможности фотосканера Epson Perfection Photo 4870 в проходящем свете. Если кодировать прямо в postscript, можно печатать отдельными пикселями вышеуказанного размера. «Шахматная доска» из пикселей остается разборчивой. То же самое получается на хорошей мелованной бумаге. И это получается на устройстве типа ширпотреб. Есть еще более «тонкое» устройство — степпер. Каждый жук и жаба, проектирующий чипы и заказывающий их на фабах, считает своим долгом что-нибудь написать мелкими буквами на кремнии.

    2) Сколько памяти понадобится, чтобы его хранить?
    Все зависит от представления. Для растрового представления достаточно широкого набора разборчиво читаемых символов хватает поля 5x7. Это 35 бит. Если сжать без потерь, будет где-то 16-19, в зависимоти от алгоритма сжатия. Если с потерями, но без потери разборчивости на латинке, можно догнать до 7 бит на символ. Если использовать словарь для фильтрации вариантов, можно догнать до 5.

    3) Сколько кода понадобится, чтобы его использовать?
    Смотря что понимать под кодом. ЕМНИП, программа рендеринга шахматного поля с клеткой в один пиксель на постскрипте (файл, который отправлялся на принтер командой lp), весил чуть более 300 байт.
    Если просто печатать текст шрифтом по умолчанию, на устройство, в том числе и на экран, то на символ ASCII идет ровно один байт.


  1. vk2
    20.07.2019 12:29
    +7

    Вспоминается БК 0010-01
    image

    (на черно-белом мониторе выглядело прекрасно, разумеется)


    1. Kirhgoff
      21.07.2019 03:02

      Всегда мечтал о цветном мониторе для моей бкшки :(


      1. engine9
        21.07.2019 10:32

        Такие вообще были?


        1. Kirhgoff
          21.07.2019 11:17

          Ну вот мужчина выше скриншот запостил. Но вообще, понятия не имею. Мечтают ведь о том, чего нет. Мне кажется я где-то увидел цветной монитор, возможно у другого компьютера и возжелал такого же. Помню только, как я мечтал разными цветами рисовать в своей программе.


        1. vk2
          21.07.2019 13:02

          Да, и поставлялись очень широко (я его неоднократно видел в разных компьютерных классах). Нашел фотографию такого, как у был меня. Кстати, я-то как раз хотел черно-белый, чтобы с узенькими символами работать.

          image


  1. firk
    20.07.2019 20:53
    -1

    С 36 используемыми символами получается ровно 36х5 пикселей. Если считать, что каждый пиксель занимает 3 байта, то нам нужно 3653 = 540 байт на то, чтобы хранить весь рисуонк

    Сжатие

    Палитра

    итд

    Я конечно понимаю что в итоге автор статьи пришёл к верному решению, но количество попутно совершённых при этом заведомо лишних действий удивляет.


    Всё же просто: символы состоят из 3х5 точек, каждая точка либо включена либо нет (1 бит). То есть это монохромная картинка 3х5 с 1 битом на пиксель. То что её выводят на монитор в субпиксельном режиме — для кодирования дела нисколько не меняет.


  1. Maccimo
    20.07.2019 22:01

    Банальные чёрные поля добавляют читабельности:
    Если для компонентов пикселя не использовать исключительно 0x00 и 0xFF, а подбирать значения в зависимости от соседних точек и восприятия разных цветов человеком, то наверняка можно сделать ещё читабельнее.


  1. afendiko
    21.07.2019 08:21

    Проблемы со шрифтом на компе не существует. А вот, например текст на бумаге, написанный очень мелким шрифтом, реально раздражает. Иногда приходиться фотографировать его, чтобы прочитать.


  1. seri0shka
    21.07.2019 12:20
    +1

    По факту на один символ требуется 5 пикселей по вертикали, ещё один добавляется для междустрочного интервала- уже 6 пикселей, и плюс ещё столько же (!) для интервала между буквами. Итого 12 пикселей занимает один символ. Возмутимо много.
    Можно для интервала между буквами использовать 1 субпиксель, это в 1.5 раза уменьшит занимаемое место.

    нарисовал
    image


    1. KvanTTT
      21.07.2019 12:44

      А можно картинку в оригинальном масштабе? Подозреваю, что качество так будет хуже, потому что слова вообще будут сливаться.


      1. seri0shka
        21.07.2019 13:03

        Нельзя. Делал в графическом редакторе.


      1. seri0shka
        21.07.2019 13:57
        +1

        Оказывается, можно даже в редакторе, вот картинка:

        Вы открыли во мне новые возможности.

        фото с дисплея


    1. synedra Автор
      22.07.2019 07:30

      Неплохо, но тогда на каждый символ нужно три глифа в зависимости от его позиции по субпикселям. Посмотрите на, например, L в слове Hello. Это либо раздует атлас втрое, либо (что логичнее) потребует хранить чёрно-белый атлас и генерировать колонки на лету.


      1. seri0shka
        22.07.2019 10:53

        хранить чёрно-белый атлас и генерировать колонки на лету.
        Именно!
        Насколько маленьким может быть читаемый шрифт?
        Сколько памяти понадобится, чтобы его хранить?
        Сколько кода понадобится, чтобы его использовать?
        Если во главу угла поставить первый вопрос, то мы получили требуемое.


  1. KvanTTT
    21.07.2019 12:47

    А нет ли онлайн сервиса, в котором можно сгенерировать произвольный текст таким шрифтом?


    1. developerxyz
      21.07.2019 14:29

      Вроде вот он, но он почему-то не работает…


  1. KvanTTT
    21.07.2019 12:50

    В качестве дальнейшего развития можно попробовать генерировать субпиксельные анимации. Сделать гифку или даже игру.