Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ class DisplayEditFormImpl(FlaskForm):
name = StringField('Display Name', validators=[DataRequired()])
if current_app.config['ENABLE_DISPLAY_APPROVAL']:
form_status = SelectField("Status", choices=[(k, v) for k, v in DISP_STATUS.items()], validators=[DataRequired()])
image_format = SelectField("Image Format", choices=[('BMP', 'BMP'), ('JPEG', 'JPEG'), ('PNG', 'PNG')], validators=[DataRequired()])
image_bit_depth = SelectField("Image Bit Depth", choices=[(None, 'Default'), (1, '1 bit (monochrome)'), (16, '16 bit'), (24, '24 bit')], validators=[Optional()])
playlist = QuerySelectField('Playlist',
validators=[Optional()],
query_factory=lambda: Playlist.query.order_by(Playlist.name.asc()),
Expand Down
29 changes: 25 additions & 4 deletions app/lib/image.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import List, Tuple
from typing import List, Tuple, Optional

from PIL import Image

Expand All @@ -19,11 +19,10 @@ def convert_palette(palette: List[int]) -> List[int]:
return out


def convert_colors(color_spec: str, input_path: str):
def convert_colors__cs(color_spec: str, in_im):
"""Initial conversion to what's specified in color_spec"""
cs = COLOR_SPEC.get(color_spec, COLOR_SPEC['1b'])

in_im = Image.open(input_path).convert('RGB')

if cs['bits'] == 1:
mode = '1'
elif cs['bits'] < 16:
Expand Down Expand Up @@ -52,3 +51,25 @@ def convert_colors(color_spec: str, input_path: str):
out_im.paste(in_im, tuple([0, 0] + list(in_im.size)))

return out_im


def convert_colors__bits(bit_depth: Optional[int], in_im):
"""Second conversion to actual bit depth"""
if bit_depth:
if bit_depth >= 16:
return in_im.convert('RGB')
else:
# must be 1, 16, 24 - this case would be 1 bit
out_im = Image.new('1', in_im.size)
in_im = in_im.convert('L')
in_im = in_im.point(lambda p: 255 if p >= 170 else 0)
out_im.paste(in_im, tuple([0, 0] + list(in_im.size)))
return out_im
return in_im


def convert_colors(bit_depth: Optional[int], color_spec: str, input_path: str):
im = Image.open(input_path).convert('RGB')
im = convert_colors__cs(color_spec, im)
im = convert_colors__bits(bit_depth, im)
return im
25 changes: 25 additions & 0 deletions app/lib/jinja.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,31 @@ def color_spec(value):
return label(cs['name'], level='default')


@jfilter()
def image_format(value):
level = 'default'
if value == 'BMP':
level = 'info'
if value == 'JPEG':
level = 'primary'
if value == 'PNG':
level = 'success'
return label(value, level=level)


@jfilter()
def image_bit_depth(value):
level = 'default'
if value is not None:
if value >= 24:
level = 'success'
elif value >= 16:
level = 'info'
elif value >= 1:
level = 'primary'
return label(value or 'Default', level=level)


@jfilter()
def fixed(value, d=2):
if value is not None:
Expand Down
5 changes: 5 additions & 0 deletions app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,8 @@ class Display(Base):
last_seen_at = db.Column(sau.ArrowType(), nullable=False, default=arrow.utcnow)
display_spec = db.Column(sau.ChoiceType(choices=[(k, v['name']) for k, v in DISPLAY_SPEC.items()]), nullable=False)
color_spec = db.Column(sau.ChoiceType(choices=[(k, v['name']) for k, v in COLOR_SPEC.items()]), nullable=False)
image_format = db.Column(sau.ChoiceType(choices=[('BMP', 'BMP'), ('JPEG', 'JPEG'), ('PNG', 'PNG')]), nullable=False, default='BMP', server_default='BMP')
image_bit_depth = db.Column(db.Integer())
width = db.Column(db.Integer(), nullable=False, default=0)
height = db.Column(db.Integer(), nullable=False, default=0)
playlist_id = db.Column(db.BigInteger().with_variant(db.Integer, "sqlite"), db.ForeignKey(Playlist.id, onupdate='CASCADE', ondelete='SET NULL', name='fk_display_playlist'))
Expand Down Expand Up @@ -365,6 +367,9 @@ def sync(cls):
'name': key,
'status': 'pending' if current_app.config['ENABLE_DISPLAY_APPROVAL'] else 'active',
'approval_code': cls.generate_approval_code(),
# Set image details on create only; this allows for editing later
'image_format': request.args.get('i', 'BMP'),
'image_bit_depth': request.args.get('ib'),
})
display = cls.query.filter(cls.key == key).first()
if display:
Expand Down
5 changes: 5 additions & 0 deletions app/templates/display/list.html.j2
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
<th>Last Seen</th>
<th>Type</th>
<th>Colors</th>
<th>Image</th>
<th>Dimensions</th>
{% if config['ENABLE_DISPLAY_APPROVAL'] %}
<th>Status</th>
Expand All @@ -38,6 +39,10 @@
<td>{{ d.last_seen_at |dt |typed_label }}</td>
<td>{{ d.display_spec |display_spec }}</td>
<td>{{ d.color_spec |color_spec }}</td>
<td style="white-space: nowrap;">
{{ d.image_format |image_format }}
{{ d.image_bit_depth |image_bit_depth }}
</td>
<td>{{ d.width }}x{{ d.height }}</td>
{% if config['ENABLE_DISPLAY_APPROVAL'] %}
<td>
Expand Down
7 changes: 4 additions & 3 deletions app/views/display.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,14 @@ def render():
'--path', path,
'--browser', current_app.config['BROWSER'],
])
im = convert_colors(screen.display.color_spec, path)
im = convert_colors(screen.display.image_bit_depth, screen.display.color_spec, path)
out = BytesIO()
im.save(out, 'bmp')
fmt = screen.display.image_format.code.lower()
im.save(out, fmt)
l = out.tell()
out.seek(0)
payload = out
headers.update({'Content-length': l, 'Content-type': 'image/bmp'})
headers.update({'Content-length': l, 'Content-type': f'image/{fmt}'})
finally:
if os.path.exists(path):
os.unlink(path)
Expand Down
35 changes: 35 additions & 0 deletions migrations/versions/2026_01_23_1414-84a28e9fdbb7_image_format.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""image format

Revision ID: 84a28e9fdbb7
Revises: 6bda9090d0d3
Create Date: 2026-01-23 14:14:40.125158

"""
from alembic import op
import sqlalchemy as sa

import sqlalchemy_utils

# revision identifiers, used by Alembic.
revision = '84a28e9fdbb7'
down_revision = '6bda9090d0d3'
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('display', schema=None) as batch_op:
batch_op.add_column(sa.Column('image_format', sa.String(length=8), server_default='BMP', nullable=False))
batch_op.add_column(sa.Column('image_bit_depth', sa.Integer(), nullable=True))

# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('display', schema=None) as batch_op:
batch_op.drop_column('image_bit_depth')
batch_op.drop_column('image_format')

# ### end Alembic commands ###