SSD1306Ascii icon indicating copy to clipboard operation
SSD1306Ascii copied to clipboard

fontWidth() does not include padding for fixed-sized fonts

Open bxparks opened this issue 6 years ago • 7 comments

It seems like there are 2 types of fixed-width fonts, differentiated by the first 2 bytes of the font table:

  • 0x0, 0x0 - 1 pixel padding (most of them)
  • 0x0, 0x1 - 0 pixel padding (e.g. font8x8.h, cp437font8x8.h)

For fonts with 1-pixel padding, the fontWidth() returns the font width without the padding.

I need to calculate the effective font width including the padding because I want to calculate the rectangular coordinates that go into clear(uint8_t c0, uint8_t c1, uint8_t r0, uint8_t r1) which erases a specific character of a fixed width font. If the fontWidth() included the padding (including the magFactor()), then erasing the N'th character (using a 0-based offset) would be:

clear(N * fontWidth(), (N+1) * fontWidth() - 1, 0, fontRows() - 1);

(The reason I want to erase a specific character is because some fonts are incomplete, and do not include the ASCII 0x20 "space" character).

Unfortunately, I cannot arbitrarily add a 1-pixel padding, because some fonts don't have padding. The current API does not allow me to determine whether a specific font has padding. In fact, the current API does not even have a getFont() method to retrieve the current font.

Proposed Solutions

Option 1

Add a fontWidthPadded() method which includes the padding for both padded and non-padded fonts. The implementation would be something like:

uint8_t fontWidthPadded() {
  if (m_font == nullptr) return 0;
  uint16_t size = readFontByte(m_font) << 8;                                    
  size |= readFontByte(m_font+1);                                                 
  uint8_t padding = (size == 1) ? 0 : 1; 
  return m_magFactor * (padding + readFontByte(m_font + FONT_FIXED_WIDTH));
}

(Here, I'm assuming that all proportional fonts have a 1-pixel padding. I haven't researched your code in sufficient detail to be sure.)

Option 2

Add a fontPadding() method which returns the font padding size. The implementation would be something like:

uint8_t fontPadding() {
  if (m_font == nullptr) return 0;
  uint16_t size = readFontByte(m_font) << 8;                                    
  size |= readFontByte(m_font+1);                                                 
  uint8_t padding = (size == 1) ? 0 : 1; 
  return padding * m_magFactor;
}

Option 3

Add a uint8_t* getFont() (corresponding to the setFont()) so that I can at least retrieve the current font. This will allow me to extract the first 2 bytes of the current font, which allows me to manually calculate whether or not I need to add the padding.

Thanks for your library, and your consideration of this small API change.

bxparks avatar Jun 04 '18 03:06 bxparks

This program reads the first two bytes of a font.

#include "SSD1306Ascii.h"
void setup() {
  Serial.begin(9600);
  Serial.println(readFontByte(font8x8));
  Serial.println(readFontByte(font8x8 + 1));
}
void loop() {}

Output:

0 1

greiman avatar Jun 04 '18 12:06 greiman

It's unclear to me what information you are conveying in your response:

  • Are you teaching me how to read 2 bytes from a pointer? It should be clear that I already know how to do that.
  • Are you telling me that the magic encoding for the existence of the padding is in the first two bytes of the font table? My first sentence in my post says that I already know that.

There's no new information in your reply. So I'm forced to read between the lines:

  • Are you saying that the calling client code should be responsible for extracting this magic encoding and figure out the padding? I think that breaks encapsulation badly, and this logic should be part of the library instead of being in the client code.
  • But let's say you are right, and the client code should be responsible for making this calculation. I'm using multiple fonts. How do I get the current font, given a pointer/reference to a SSD1306Ascii object? In the current API I can't.
  • Do I use a global variable to pass this information, out-of-band, into the place where this padding calculation is made, which could be a unknown number of call stacks away from where the setFont() call was made? That means that every time I call setFont() I have to update that global variable, instead of just calling getFont() which should be a one-liner in SSD1306Ascii.h.

Anyway, I don't want to waste your time or my time if you don't have any interest in this. It's easy enough to fork the code.

bxparks avatar Jun 05 '18 18:06 bxparks

Sorry you are upset with my reply.

I see your problem but I don't like any of the solutions you propose. They only works for fixed width fonts and if you use multiple fonts it becomes very complicated.

Most users define fields on their display and use setCursor() to go to the field, setFont, and then clear/write the field. Then font/field info is local to the code that writes the display.

greiman avatar Jun 05 '18 20:06 greiman

In In typography "padding" is called letter-spacing.

Currently it is one pixel for "padded" fonts and zero for old PC based fonts.

I think I will add a letter-spacing feature that will also solve your problem.

I will add member functions void setLetterSpacing(uint8_t pixels) and uint8_t letterSpacing() to get the current value..

letter-spacing will not include magFactor but you can use magFactor() to get it.

Update: I now include magFactor for letterSpacing(). You set the value without magnification.

greiman avatar Jun 06 '18 12:06 greiman

Added letter-spacing.

uint8_t SSD1306Ascii::letterSpacing() inline Returns letter-spacing in pixels before magnification.

void SSD1306Ascii::setLetterSpacing ( uint8_t pixels) inline Set letter-spacing. setFont() will restore default letter-spacing.

Parameters [in] pixels letter-spacing in pixels before magnification.

Also examples LetterSpacingSpi and LetterSpacingWire.

greiman avatar Jun 06 '18 15:06 greiman

Added my solution to clearing characters in a field.

Functions are void clearField(uint8_t col, uint8_t row, uint8_t n) and uint8_t fieldWidth(uint8_t n).

Here is example code.

void setup() {
  // Use next line if no RST_PIN or reset is not required.
  // oled.begin(&Adafruit128x64, CS_PIN, DC_PIN);  
  oled.begin(&Adafruit128x64, CS_PIN, DC_PIN, RST_PIN);
  oled.setFont(System5x7);
  oled.clear();
  
  // Start row and column of field.
  uint8_t col = 20;
  uint8_t row = 2;
  
  // Set cursor to start of field. 
  oled.setCursor(col, row);
  
  // print test pattern.
  oled.print("123456789");
  delay(1000);
  
  // Clear 4 characters starting at character 2.
  oled.clearField(col + oled.fieldWidth(2), row, 4);
  delay(1000);
  
  // Print three characters in cleared area.
  oled.print("ABC");
}

greiman avatar Jun 07 '18 12:06 greiman

Thanks for making me think about better support for fields. I will add one more function for better support of proportional fonts.

size_t strWidth(const char* str) returns the width of a string for proportional fonts. fieldWidth(uint8_t n) returns the width of a field that is wide enough to hold any n character string.

fieldWidth(1) is the same as your fontWidthPadded().

letterSpacing() is the same as your fontPadding()

I added added const uint8_t* font() to return the current font so you have all three options.

Here is one last example to demonstrate use of the new functions.

// Display values for six ADCs.
#include <SPI.h>
#include "SSD1306Ascii.h"
#include "SSD1306AsciiSpi.h"

// pin definitions
#define CS_PIN  7
#define RST_PIN 8
#define DC_PIN  9

SSD1306AsciiSpi oled;

uint8_t col[2]; // Columns for ADC values.
uint8_t rows;   // Rows per line.
//------------------------------------------------------------------------------
void setup() {
  // Use next line if no RST_PIN or reset is not required.
  // oled.begin(&Adafruit128x64, CS_PIN, DC_PIN);  
  oled.begin(&Adafruit128x64, CS_PIN, DC_PIN, RST_PIN);
  oled.setFont(System5x7);
  oled.clear();

  // Setup form.  Could use F() macro to save RAM on AVR.
  oled.println("ADC0: 9999 ADC1: 9999");
  oled.println("ADC2: 9999 ADC3: 9999");
  oled.println("ADC4: 9999 ADC5: 9999");
  
  // Calculate columns for ADC values.  No RAM is used by the strings.
  // The gcc compiler replaces strlen() with 6 and 17.
  col[0] = oled.fieldWidth(strlen("ADC0: "));
  col[1] = oled.fieldWidth(strlen("ADC0: 9999 ADC1: "));
  rows = oled.fontRows();
  delay(3000);  
}
//------------------------------------------------------------------------------
void loop() {
  for (uint8_t i = 0; i < 6; i++) {  
    oled.clearField(col[i%2], rows*(i/2), 4);    
    oled.print(analogRead(i));
  }
  delay(1000);
}

greiman avatar Jun 08 '18 12:06 greiman