<

Imageboard

A Linkspace Application Tutorial

Available in the pkg or in repository/examples

  • placing images
#!/bin/bash
set -xeuo pipefail
if [ $# -lt 4 ]; then
    echo "Usage: img_file board_name X Y"
    exit 2
fi

IMG_FILE=$1; BOARD=$2; X=$3; Y=$4;
shift 4

IMG_HASH=$(\
           cat "$IMG_FILE" \
               | lk data --write db --write stdout \
               | lk pktf "[hash:str]")
TAG=$(printf "%08d%08d" "$X" "$Y")

lk link "imageboard:$LK_GROUP:/$BOARD" \
   -l "$TAG":"$IMG_HASH" "$@" \
   --write db --write stdout \
    | lk pktf

echo PIDS $(jobs -p)
  • viewing images
#!/bin/bash
set -euo pipefail
BOARD=${1?Usage: board_name [start_stamp] }
if [[ ! -f $BOARD.png ]]; then
    magick convert -size 1000x1000 xc:transparent PNG32:$BOARD.png # Create empty canvas
fi
START_STAMP=${2:-"0"} # If no stamp is given we begin at 0, i.e. unix epoch in microseconds

# We select everything with a create field greater or equal to $START_STAMP
lk watch --db-only "imageboard:$LK_GROUP:/$BOARD" -- "create:>=:[u64:$START_STAMP]" \
    | lk pktf "[/links:[tag:str] [ptr:str]]" \
    | while read REF; do
        X=${REF:0:8}
        Y=${REF:8:8}
        IMG_HASH=${REF: -43}
        echo "Placing $IMG_HASH at $X , $Y"
        lk watch-hash $IMG_HASH --ttl 5s \
            | lk pktf "[data]" --delimiter "" \
            | magick composite -geometry +$X+$Y - PNG32:$BOARD.png PNG32:$BOARD.png
    done
echo "$BOARD: $START_STAMP"
  • streaming images
#!/bin/bash -x 
set -euo pipefail
BINS="$(dirname "${BASH_SOURCE[0]}")"
LK_DOMAIN="imageboard" # set the default domain
LK_GROUP=${LK_GROUP:-"[#:pub]"}
BOARD=${1?Usage: board_name [start_stamp] }
magick convert -size 1000x1000 xc:transparent PNG32:$BOARD.png

# not strictly necessary, but otherwise pull does nothing
lk status watch exchange $LK_GROUP process --write "stdout-expr:exchange - [data]"  || echo "No exchange process active"

echo Pulling $LK_GROUP $BOARD
lk pull "imageboard:$LK_GROUP:/$BOARD" --follow -- "create:>:[now:-1D]" 

$BINS/imageboard.view.sh $BOARD 0 # run once

#On receiving a new packet of interest we repaint the board from that stamp
lk watch --new-only "imageboard:$LK_GROUP:/$BOARD" | \
    lk pktf "[create:str]" | \
    while read STAMP; do
        $BINS/imageboard.view.sh $BOARD $STAMP
    done

# we could use lk watch ::/$BOARD as both the LK_DOMAIN and LK_GROUP were set.

This is a straight-up translation of the bash script. It works, but it could be done better by having only a single python instance running.

  • placing images
#!/bin/env python3
import os
from linkspace import *
import sys
if len(sys.argv) < 5:
    sys.exit('Usage: imagefile boardname x y')
[imagefile,boardname,x,y] = sys.argv[1:]
x = int(x)
y = int(y)
if x > 1000 or y > 1000:
    sys.exit('X and Y coordinates should be < 1000')



imgdata = open(imagefile,'rb').read()
# this will error if the file is to large ( 2^16 - 512 ). 
# To use bigger files you can manually split/merge them, or wait for a convention to stabilize 
datap = lk_datapoint(imgdata)
# we can access the point's fields as bytes such as datap.hash, turn those into b64
# print("Saving image ",base64(datap.hash))
# Alternatively we can use lk_eval/lk_eval2str and use an abe expr
print(lk_eval2str("Using image [hash:str]",datap))


# We make up this scheme for our app
# Tags will be decimal encoded, ptr will point to image data. 
tag = f"{x:08d}{y:08d}".encode() # Everything in linkspace is plain bytes
links = [Link(tag,datap.hash)]
linkp = lk_linkpoint(domain=b"imageboard",
                     path=[boardname.encode()],
                     links=links)
# print(lk_eval2str("Placing new image [pkt]",linkp))

# instance looks for 'path' arg | $LK_DIR env | $HOME/linkspace
lk = lk_open(create=True) 

# write the point to the linkspace instance
_isnew = lk_save(lk,datap)
lk_save(lk,linkp)
  • viewing images
#!/bin/env python3
from linkspace import *
import os
import sys
if len(sys.argv) < 2:
    sys.exit('Usage: boardname ?stamp')

boardfile = sys.argv[1] + ".png"
boardname = sys.argv[1]
create_stamp = int(sys.argv[2]) if len(sys.argv) > 2 else 0

if not os.path.exists(boardfile):
    os.system(f"magick convert -size 1000x1000 xc:transparent PNG32:{boardfile}")

lk = lk_open(create=True)
group_expr = os.environ.get("LK_GROUP","[#:pub]")
group = lk_eval(group_expr)


# You can parse multiple statements as abe.
# The usual ABE scope is available, and you can extend it with argv
query_string = """
domain:=:imageboard
group:=:[0]
spacename:=:/[1]
create:>=:[2/u64]
"""
query = lk_query_parse(lk_query(),query_string,argv=[group_expr,boardname,str(create_stamp)])

# or use templates. if you're just interested in the string
query = lk_query_parse(query,f"create:>=:[:{str(create_stamp)}/u64]")
# Or if you have the exact bytes
create_b = create_stamp.to_bytes(8,byteorder='big')
query = lk_query_parse(query,f"create:>=:[0]",argv=[create_b])
# Or if you're only adding a single statement
query = lk_query_push(query,"create",">=",create_b)

# The query merges overlapping predicates, and errors on conflicting predicates

# Query parsing is somewhat forgiving in that it allows alternative encodings for Group, Domain, and Space. 
# Group can take the b64 no-pad string
# Domain is 16 bytes but does not have to prepend '\0'
# Space takes either a '/' delimited expression, or the 'space' bytes ( as given by the spacename function or pkt.spacename value )
# The other values require the exact number of bytes, in big endian when a number.

# Its worth understanding why these two work. Checkout the guide
create_abe = f"[u64:{str(create_stamp)}]"
assert create_b  == lk_eval(create_abe)
assert lk_encode(create_b,"u64") == create_abe

# we'll collect our entries in here 
image_data = []
def update_image(pkt):
    create = pkt.create # all the links in the packet will have this as their z-index
    for link in pkt.links:
        x = int(str(link.tag[:8],'ascii'))
        y = int(str(link.tag[8:],'ascii'))
        q = lk_hash_query(link.ptr) # shorthand for :mode:hash-asc i:=:[u32:0] hash:=:HASH
        q = lk_query_push(q,"recv","<",lk_eval("[now:+3s]"))

        # we need a uniq id to register this query under.
        qid = bytearray(pkt.hash)
        qid.extend(link.ptr)
        q = lk_query_push(q,"","qid",bytes(qid))

        # print("Looking for ",lk_query_print(q,True))
        # we could get with 'lk_get(lk,q)' but to give new packets a chance to arrive we've set recv < now+3s so we will watch them.
        lk_watch(lk,q,lambda data_pkt : image_data.append([create,x,y,data_pkt,pkt]))

# we only care about the ones we know right now. 
lk_get_all(lk,query, update_image)

# Because we set a timeout ( recv<now+5s ) for all data packets ( in case we're still receiving them )
# we can simply wait until all callbacks are done or dropped.
lk_process_while(lk)

image_data.sort()

import pathlib
pathlib.Path("./fragments").mkdir(parents=True, exist_ok=True)

for [_,x,y,datap,parent] in image_data:
    filename = lk_eval2str("./fragments/[hash:str]",datap)
    try:
        with open(filename, "bx") as f:
            f.write(datap.data)
            f.flush()
            f.close()
    except Exception as e:
        pass
    print(f"placing at {x},{y} the image {filename}")
    os.system(f"magick composite -geometry +{x}+{y} {filename} PNG32:{boardfile} PNG32:{boardfile}")
  • streaming images
#!/bin/env python3
from linkspace import *
import os
import sys
if len(sys.argv) < 2:
    sys.exit('Usage: boardname ?stamp')

boardname = sys.argv[1]
create_stamp = int(sys.argv[2]) if len(sys.argv) > 2 else 0


lk = lk_open(create=True)
ok = lk_status_watch(lk,
               qid=b"ex",
               timeout=lk_eval("[us:+2s]"),
               domain=b"exchange",
               objtype=b"process")
if not ok and lk_process_while(lk,qid=b"ex") == 0:
    sys.exit("No exchange process active?") # not strictly necessary, but otherwise pull does nothing
else:
    print("Exchange ok");


query_string = """
group:=:[#:pub]
domain:=:imageboard
spacename:=:/[0]
create:>=:[now:-1D]
:qid:[0]
"""
query = lk_query_parse(lk_query(),query_string,argv=[boardname])

#We signal the exchange process to gather the data
lk_pull(lk,query)

#we wait for every packet and redraw the painting starting at the 'create' stamp
script_dir = os.path.dirname(os.path.realpath(__file__))
os.system(f"{script_dir}/imageboard.view.py {boardname} 0")

def update_img(pkt):
    create = lk_eval2str("[create:str]",pkt)
    os.system(f"{script_dir}/imageboard.view.py {boardname} {create}")

query = lk_query_parse(query,"i_db:<:[u32:0]") # we only care for packets not currently in the database. The new stuff
lk_watch(lk,query, update_img)
lk_process_while(lk)

Created: 2023-11-05 Sun 09:20