Compare commits

...

35 Commits

Author SHA1 Message Date
9db00dd64c Fix die-cut label printing
All checks were successful
Alpine 3.21 Success
2025-05-01 15:57:59 +02:00
3832ca749f README.adoc: circumvent an issue with libasciidoc
All checks were successful
Alpine 3.19 Success
2022-05-01 18:18:31 +02:00
f37c194ab8 Bump Go modules to 1.17 2021-08-19 05:35:58 +02:00
269e6514df Add support for printing on red/black tapes
My QL-800 refuses to print black only on a red/black tape.

I really don't like the added `rb` function parameter.
It would be best to be able to recognize such tapes, however
nothing of the sort is documented in official Brother documentation.

makeBitmapData{,RB} also look like they could be simplified further.
2021-06-10 02:37:14 +02:00
9734cdd16e bdf: make it possible to set the drawing colour
Intended for red and black tapes.
2021-06-09 23:18:06 +02:00
8215b59834 Update dependencies
Somehow we have got rid of a lot of indirect nonsense.
2021-06-08 03:44:07 +02:00
6fc00ba3dd Name change 2020-10-10 14:19:35 +02:00
93b29a5346 sklad: close the DB FD once finished reading
Fixes a resource leak.
2020-10-10 14:18:22 +02:00
696ea89530 label-tool: respect font ascent and descent
Also try to load the values from the BDF font file.
2019-04-26 11:40:43 +02:00
e56482d73f label-tool: send pages as UTF-8 2019-04-25 21:54:58 +02:00
2f47b3f5da label-tool: make it possible to print free form text
Also commit the label subpackage that we forgot to.
2019-04-25 21:54:57 +02:00
154f3147e3 label-tool: allow choosing from multiple fonts 2019-04-25 20:35:00 +02:00
5cef6b240a bdf-preview: style corrections 2019-04-25 19:59:43 +02:00
81927e9017 sklad: proper validations on container update 2019-04-22 13:56:08 +02:00
88560a8fbf sklad: prefill form with last values on error
Since the browser's back button cannot be used because of our
fascist caching policy.
2019-04-22 13:55:07 +02:00
301d035425 sklad: use Request.URL when self-redirecting 2019-04-22 11:43:37 +02:00
04e66d7888 sklad: redirect to GET on successful DB changes 2019-04-22 10:14:28 +02:00
7d2ca09d1b sklad: show the context when deleting containers
Do not try to show the deleted container.
2019-04-22 10:09:08 +02:00
4ad4bcf9e7 sklad: fix index update when changing parent 2019-04-19 13:46:05 +02:00
2d1e01a1a2 sklad: make it possible to update the parent 2019-04-19 13:40:47 +02:00
1bd7a9d735 sklad: always try to shut down cleanly 2019-04-19 12:26:27 +02:00
6c6cec6298 sklad: highlight matches in the search 2019-04-18 05:59:13 +02:00
cbf8678681 sklad: make containers always link to self 2019-04-18 05:34:01 +02:00
0936963aaf Avoid DB data races from different goroutines 2019-04-16 19:53:50 +02:00
e4ae5d0001 Add a LICENSE file 2019-04-16 11:33:50 +02:00
21d01f4c4b sklad: clean up templates 2019-04-16 03:56:53 +02:00
885d161cf5 README.adoc: add a representative picture 2019-04-16 00:09:01 +02:00
a4a399b812 sklad: prevent creating container cycles 2019-04-16 00:07:00 +02:00
2bd4f5921c sklad: open label printing in a new window/tab 2019-04-15 23:31:13 +02:00
1713bd1f06 sklad: link to container in print confirmation 2019-04-15 10:54:27 +02:00
f8bb344aab sklad: use textarea placeholder also for editing 2019-04-15 10:54:27 +02:00
0b32fa576f sklad: show whole container descriptions 2019-04-15 10:54:27 +02:00
82c6c34ea5 sklad: trim spaces from user-supplied attributes 2019-04-15 03:57:53 +02:00
5b7113905e sklad: support running under a prefix 2019-04-15 03:45:41 +02:00
dc0536c011 sklad: breadcrumbs for all containers 2019-04-15 02:57:04 +02:00
22 changed files with 669 additions and 361 deletions

12
LICENSE Normal file
View File

@@ -0,0 +1,12 @@
Copyright (c) 2019 - 2021, Přemysl Eric Janouch <p@janouch.name>
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

View File

@@ -9,6 +9,8 @@ The project also contains reusable Go packages with a Brother QL-series USB
printer driver and a simple BDF bitmap font renderer, as well as a few related printer driver and a simple BDF bitmap font renderer, as well as a few related
utilities for previewing, printing and debugging. utilities for previewing, printing and debugging.
image::sklad.png[align="center"]
Building and Running Building and Running
-------------------- --------------------
Build dependencies: Go + Build dependencies: Go +
@@ -17,6 +19,7 @@ Runtime dependencies: Linux, a Brother QL label printer connected over USB
$ go get -u https://janouch.name/sklad/cmd/sklad $ go get -u https://janouch.name/sklad/cmd/sklad
You will need to bootstrap the database by writing a minimal 'db.json': You will need to bootstrap the database by writing a minimal 'db.json':
.... ....
{ {
"Password": "login-password", "Password": "login-password",

View File

@@ -49,6 +49,8 @@ func (g *glyph) At(x, y int) color.Color {
// Font represents a particular bitmap font. // Font represents a particular bitmap font.
type Font struct { type Font struct {
Name string Name string
Ascent int // needn't be present in the font
Descent int // needn't be present in the font
glyphs map[rune]glyph glyphs map[rune]glyph
fallback glyph fallback glyph
} }
@@ -64,11 +66,13 @@ func (f *Font) FindGlyph(r rune) (glyph, bool) {
// DrawString draws the specified text string onto dst horizontally along // DrawString draws the specified text string onto dst horizontally along
// the baseline starting at dp, using black color. // the baseline starting at dp, using black color.
func (f *Font) DrawString(dst draw.Image, dp image.Point, s string) { func (f *Font) DrawString(dst draw.Image, dp image.Point,
color color.Color, s string) {
src := image.NewUniform(color)
for _, r := range s { for _, r := range s {
g, _ := f.FindGlyph(r) g, _ := f.FindGlyph(r)
draw.DrawMask(dst, g.bounds.Add(dp), draw.DrawMask(dst, g.bounds.Add(dp),
image.Black, image.ZP, &g, g.bounds.Min, draw.Over) src, image.ZP, &g, g.bounds.Min, draw.Over)
dp.X += g.advance dp.X += g.advance
} }
} }
@@ -192,17 +196,23 @@ func (p *bdfParser) readLine() bool {
return true return true
} }
func (p *bdfParser) readCharEncoding() int { func (p *bdfParser) readIntegerArgument() int {
if len(p.tokens) < 2 { if len(p.tokens) < 2 {
panic("insufficient arguments") panic("insufficient arguments")
} }
if i, err := strconv.Atoi(p.tokens[1]); err != nil { if i, err := strconv.Atoi(p.tokens[1]); err != nil {
panic(err) panic(err)
} else { } else {
return i // Some fonts even use -1 for things outside the encoding. return i
} }
} }
// Some fonts even use -1 for things outside the encoding.
func (p *bdfParser) readCharEncoding() int { return p.readIntegerArgument() }
// XXX: Ignoring vertical advance since we only expect purely horizontal fonts.
func (p *bdfParser) readDwidth() int { return p.readIntegerArgument() }
func (p *bdfParser) parseProperties() { func (p *bdfParser) parseProperties() {
// The wording in the specification suggests that the argument // The wording in the specification suggests that the argument
// with the number of properties to follow isn't reliable. // with the number of properties to follow isn't reliable.
@@ -210,22 +220,14 @@ func (p *bdfParser) parseProperties() {
switch p.tokens[0] { switch p.tokens[0] {
case "DEFAULT_CHAR": case "DEFAULT_CHAR":
p.defaultChar = p.readCharEncoding() p.defaultChar = p.readCharEncoding()
case "FONT_ASCENT":
p.font.Ascent = p.readIntegerArgument()
case "FONT_DESCENT":
p.font.Descent = p.readIntegerArgument()
} }
} }
} }
// XXX: Ignoring vertical advance since we only expect purely horizontal fonts.
func (p *bdfParser) readDwidth() int {
if len(p.tokens) < 2 {
panic("insufficient arguments")
}
if i, err := strconv.Atoi(p.tokens[1]); err != nil {
panic(err)
} else {
return i
}
}
func (p *bdfParser) readBBX() image.Rectangle { func (p *bdfParser) readBBX() image.Rectangle {
if len(p.tokens) < 5 { if len(p.tokens) < 5 {
panic("insufficient arguments") panic("insufficient arguments")

View File

@@ -3,6 +3,7 @@ package main
import ( import (
"html/template" "html/template"
"image" "image"
"image/color"
"image/draw" "image/draw"
"image/png" "image/png"
"log" "log"
@@ -27,12 +28,12 @@ var tmpl = template.Must(template.New("list").Parse(`
<th>Name</th> <th>Name</th>
<th>Preview</th> <th>Preview</th>
<tr> <tr>
{{range $k, $v := . }} {{- range $k, $v := . }}
<tr> <tr>
<td>{{ $k }}</td> <td>{{ $k }}</td>
<td><img src='?name={{ $k }}'></td> <td><img src='?name={{ $k }}'></td>
</tr> </tr>
{{end}} {{- end }}
</table> </table>
</body></html> </body></html>
`)) `))
@@ -82,12 +83,12 @@ func main() {
img := image.NewRGBA(super) img := image.NewRGBA(super)
draw.Draw(img, super, image.White, image.ZP, draw.Src) draw.Draw(img, super, image.White, image.ZP, draw.Src)
font.DrawString(img, image.ZP, font.Name) font.DrawString(img, image.ZP, color.Black, font.Name)
fonts[filename] = fontItem{Font: font, Preview: img} fonts[filename] = fontItem{Font: font, Preview: img}
} }
log.Println("Starting server") log.Println("starting server")
http.HandleFunc("/", handle) http.HandleFunc("/", handle)
log.Fatal(http.ListenAndServe(":8080", nil)) log.Fatal(http.ListenAndServe(":8080", nil))
} }

View File

@@ -2,6 +2,7 @@ package main
import ( import (
"image" "image"
"image/color"
"image/draw" "image/draw"
"image/png" "image/png"
"log" "log"
@@ -26,7 +27,7 @@ func main() {
img := image.NewRGBA(super) img := image.NewRGBA(super)
draw.Draw(img, super, image.White, image.ZP, draw.Src) draw.Draw(img, super, image.White, image.ZP, draw.Src)
font.DrawString(img, image.ZP, font.Name) font.DrawString(img, image.ZP, color.Black, font.Name)
fo, err := os.Create("out.png") fo, err := os.Create("out.png")
if err != nil { if err != nil {

View File

@@ -4,6 +4,8 @@ import (
"errors" "errors"
"html/template" "html/template"
"image" "image"
"image/color"
"image/draw"
"image/png" "image/png"
"log" "log"
"net/http" "net/http"
@@ -16,17 +18,28 @@ import (
"janouch.name/sklad/ql" "janouch.name/sklad/ql"
) )
var font *bdf.Font var tmplFont = template.Must(template.New("font").Parse(`
<!DOCTYPE html>
<html><body>
<h1>PT-CBP label printing tool</h1>
<h2>Choose font</h2>
{{ range $i, $f := . }}
<p><a href='?font={{ $i }}'>
<img src='?font={{ $i }}&amp;preview' title='{{ $f.Path }}'></a>
{{ end }}
</body></html>
`))
var tmpl = template.Must(template.New("form").Parse(` var tmplForm = template.Must(template.New("form").Parse(`
<!DOCTYPE html> <!DOCTYPE html>
<html><body> <html><body>
<h1>PT-CBP label printing tool</h1> <h1>PT-CBP label printing tool</h1>
<table><tr> <table><tr>
<td valign=top> <td valign=top>
<img border=1 src='?img&amp;scale={{.Scale}}&amp;text={{.Text}}'> <img border=1 src='?font={{ .FontIndex }}&amp;scale={{ .Scale }}{{/*
*/}}&amp;text={{ .Text }}&amp;render'>
</td> </td>
<td valign=top> <td valign=top><form>
<fieldset> <fieldset>
{{ if .Printer }} {{ if .Printer }}
@@ -59,21 +72,38 @@ var tmpl = template.Must(template.New("form").Parse(`
{{ end }} {{ end }}
</fieldset> </fieldset>
<fieldset> <fieldset>
<p>Font: {{ .Font.Name }} <legend>Font</legend>
</fieldset> <p>{{ .Font.Name }} <a href='?'>Change</a>
<form><fieldset> <input type=hidden name=font value='{{ .FontIndex }}'>
<p><label for=text>Text:</label> <p><label for=scale>Scale:</label>
<input id=text name=text value='{{.Text}}'>
<label for=scale>Scale:</label>
<input id=scale name=scale value='{{.Scale}}' size=1> <input id=scale name=scale value='{{.Scale}}' size=1>
</fieldset>
<fieldset>
<legend>Label</legend>
<p><textarea name=text>{{.Text}}</textarea>
<p>Kind:
<input type=radio id=kind-text name=kind value=text
{{ if eq .Kind "text" }} checked{{ end }}>
<label for=kind-text>plain text (horizontal)</label>
<input type=radio id=kind-qr name=kind value=qr
{{ if eq .Kind "qr" }} checked{{ end }}>
<label for=kind-qr>QR code (vertical)</label>
<p><input type=submit value='Update'> <p><input type=submit value='Update'>
<input type=submit name=print value='Update and Print'> <input type=submit name=print value='Update and Print'>
</fieldset></form> </fieldset>
</td> </form></td>
</tr></table> </tr></table>
</body></html> </body></html>
`)) `))
type fontItem struct {
Path string
Font *bdf.Font
Preview image.Image
}
var fonts = []*fontItem{}
func getPrinter() (*ql.Printer, error) { func getPrinter() (*ql.Printer, error) {
printer, err := ql.Open() printer, err := ql.Open()
if err != nil { if err != nil {
@@ -96,8 +126,30 @@ func getStatus(printer *ql.Printer) error {
} }
func handle(w http.ResponseWriter, r *http.Request) { func handle(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil { if r.Method == http.MethodGet {
w.Header().Set("Cache-Control", "no-store")
}
var (
font *fontItem
fontIndex int
err error
)
if fontIndex, err = strconv.Atoi(r.FormValue("font")); err == nil {
font = fonts[fontIndex]
} else {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := tmplFont.Execute(w, fonts); err != nil {
http.Error(w, err.Error(), 500) http.Error(w, err.Error(), 500)
}
return
}
if _, ok := r.Form["preview"]; ok {
w.Header().Set("Content-Type", "image/png")
if err := png.Encode(w, font.Preview); err != nil {
http.Error(w, err.Error(), 500)
}
return return
} }
@@ -126,37 +178,50 @@ func handle(w http.ResponseWriter, r *http.Request) {
InitErr error InitErr error
MediaInfo *ql.MediaInfo MediaInfo *ql.MediaInfo
Font *bdf.Font Font *bdf.Font
FontIndex int
Text string Text string
Scale int Scale int
Kind string
}{ }{
Printer: printer, Printer: printer,
PrinterErr: printerErr, PrinterErr: printerErr,
InitErr: initErr, InitErr: initErr,
MediaInfo: mediaInfo, MediaInfo: mediaInfo,
Font: font, Font: font.Font,
FontIndex: fontIndex,
Text: r.FormValue("text"), Text: r.FormValue("text"),
Kind: r.FormValue("kind"),
} }
var err error
params.Scale, err = strconv.Atoi(r.FormValue("scale")) params.Scale, err = strconv.Atoi(r.FormValue("scale"))
if err != nil { if err != nil {
params.Scale = 3 params.Scale = 3
} }
if params.Kind == "" {
params.Kind = "text"
}
var img image.Image var img image.Image
if mediaInfo != nil { if mediaInfo != nil {
if params.Kind == "qr" {
img = &imgutil.LeftRotate{Image: label.GenLabelForHeight( img = &imgutil.LeftRotate{Image: label.GenLabelForHeight(
font, params.Text, mediaInfo.PrintAreaPins, params.Scale)} font.Font, params.Text, mediaInfo.PrintAreaPins, params.Scale)}
} else {
img = label.GenLabelForWidth(
font.Font, params.Text, mediaInfo.PrintAreaPins, params.Scale)
}
if r.FormValue("print") != "" { if r.FormValue("print") != "" {
if err := printer.Print(img); err != nil { if err := printer.Print(img, false); err != nil {
log.Println("print error:", err) log.Println("print error:", err)
} }
} }
} }
if _, ok := r.Form["img"]; !ok { if _, ok := r.Form["render"]; !ok {
w.Header().Set("Content-Type", "text/html") w.Header().Set("Content-Type", "text/html; charset=utf-8")
tmpl.Execute(w, &params) if err := tmplForm.Execute(w, &params); err != nil {
http.Error(w, err.Error(), 500)
}
return return
} }
@@ -173,24 +238,32 @@ func handle(w http.ResponseWriter, r *http.Request) {
} }
func main() { func main() {
if len(os.Args) != 3 { if len(os.Args) < 3 {
log.Fatalf("usage: %s ADDRESS BDF-FILE\n", os.Args[0]) log.Fatalf("usage: %s ADDRESS BDF-FILE...\n", os.Args[0])
} }
address, bdfPath := os.Args[1], os.Args[2] address, bdfPaths := os.Args[1], os.Args[2:]
for _, path := range bdfPaths {
var err error fi, err := os.Open(path)
fi, err := os.Open(bdfPath)
if err != nil { if err != nil {
log.Fatalln(err) log.Fatalln(err)
} }
font, err := bdf.NewFromBDF(fi)
font, err = bdf.NewFromBDF(fi) if err != nil {
log.Fatalf("%s: %s\n", path, err)
}
if err := fi.Close(); err != nil { if err := fi.Close(); err != nil {
log.Fatalln(err) log.Fatalln(err)
} }
if err != nil {
log.Fatalln(err) r, _ := font.BoundString(font.Name)
super := r.Inset(-3)
img := image.NewRGBA(super)
draw.Draw(img, super, image.White, image.ZP, draw.Src)
font.DrawString(img, image.ZP, color.Black, font.Name)
fonts = append(fonts, &fontItem{Path: path, Font: font, Preview: img})
} }
log.Println("starting server") log.Println("starting server")

View File

@@ -17,6 +17,7 @@ import (
var scale = flag.Int("scale", 1, "integer upscaling") var scale = flag.Int("scale", 1, "integer upscaling")
var rotate = flag.Bool("rotate", false, "print sideways") var rotate = flag.Bool("rotate", false, "print sideways")
var redblack = flag.Bool("redblack", false, "red and black print")
func main() { func main() {
flag.Usage = func() { flag.Usage = func() {
@@ -82,7 +83,7 @@ func main() {
log.Fatalln("the image is too high,", dy, ">", mi.PrintAreaLength, "pt") log.Fatalln("the image is too high,", dy, ">", mi.PrintAreaLength, "pt")
} }
if err := p.Print(img); err != nil { if err := p.Print(img, *redblack); err != nil {
log.Fatalln(err) log.Fatalln(err)
} }
} }

View File

@@ -49,14 +49,14 @@
<h1>sklad</h1> <h1>sklad</h1>
{{ block "HeaderControls" . }} {{ block "HeaderControls" . }}
<a href=/>Obaly</a> <a href="container">Obaly</a>
<a href=/series>Řady</a> <a href="series">Řady</a>
<form method=get action=/search> <form method=get action="search">
<input type=text name=q autofocus><input type=submit value="Hledat"> <input type=text name=q autofocus><input type=submit value="Hledat">
</form> </form>
<form method=post action=/logout> <form method=post action="logout">
<input type=submit value="Odhlásit"> <input type=submit value="Odhlásit">
</form> </form>
{{ end }} {{ end }}

View File

@@ -12,6 +12,8 @@
<p>Chyba: Řadu u neprázdných obalů nelze měnit. <p>Chyba: Řadu u neprázdných obalů nelze měnit.
{{ else if .ErrorCannotChangeNumber }} {{ else if .ErrorCannotChangeNumber }}
<p>Chyba: Číslo obalu v řadě nelze měnit. <p>Chyba: Číslo obalu v řadě nelze měnit.
{{ else if .ErrorWouldContainItself }}
<p>Chyba: Obal by obsahoval sám sebe.
{{ else if .ErrorContainerInUse }} {{ else if .ErrorContainerInUse }}
<p>Chyba: Obal se používá. <p>Chyba: Obal se používá.
{{ else if .Error }} {{ else if .Error }}
@@ -19,36 +21,42 @@
{{ end }} {{ end }}
{{ if .Container }} {{ if .Container }}
<section> <section>
<header> <header>
<h2>{{ .Container.Id }}</h2> <h2><a href="container?id={{ .Container.Id }}">{{ .Container.Id }}</a>
<form method=post action="/label?id={{ .Container.Id }}"> {{- range .Container.Path }}
<small>&laquo; <a href="container?id={{ . }}">{{ . }}</a></small>
{{- end }}
</h2>
<form method=post action="label?id={{ .Container.Id }}" target=_blank>
<input type=submit value="Vytisknout štítek"> <input type=submit value="Vytisknout štítek">
</form> </form>
<form method=post action="/?id={{ .Container.Id }}&amp;remove"> <form method=post action="container?id={{ .Container.Id }}&amp;remove">
<input type=submit value="Odstranit"> <input type=submit value="Odstranit">
</form> </form>
</header> </header>
<form method=post action="container?id={{ .Container.Id }}">
<form method=post action="/?id={{ .Container.Id }}"> {{- $description := or .NewDescription .Container.Description }}
<textarea name=description rows=5> <textarea name=description rows="{{ max 5 (lines $description) }}"
{{ .Container.Description }} placeholder="Popis obalu nebo jeho obsahu">
{{- $description -}}
</textarea> </textarea>
<footer> <footer>
<div> <div>
<label for=series>Řada:</label> <label for=series>Řada:</label>
<select name=series id=series> <select name=series id=series>
{{ range $prefix, $desc := .AllSeries }} {{- $preselect := or .NewSeries .Container.Series }}
{{- range $prefix, $desc := .AllSeries }}
<option value="{{ $prefix }}" <option value="{{ $prefix }}"
{{ if eq $prefix $.Container.Series }}selected{{ end }} {{ if eq $prefix $preselect }}selected{{ end -}}
>{{ $prefix }} &mdash; {{ $desc }}</option> >{{ $prefix }} &mdash; {{ $desc }}</option>
{{ end }} {{- end }}
</select> </select>
</div> </div>
<div> <div>
<label for=parent>Nadobal:</label> <label for=parent>Nadobal:</label>
<input type=text name=parent id=parent value="{{ .Container.Parent }}"> <input type=text name=parent id=parent
value="{{ or .NewParent .Container.Parent }}">
</div> </div>
<input type=submit value="Uložit"> <input type=submit value="Uložit">
</footer> </footer>
@@ -56,28 +64,33 @@
</section> </section>
<h2>Podobaly</h3> <h2>Podobaly</h3>
{{ else }} {{ else }}
<section> <section>
<header> <header>
<h2>Nový obal</h2> <h2>Nový obal</h2>
</header> </header>
<form method=post action="/"> <form method=post action="container">
<textarea name=description rows=5 {{- $description := or .NewDescription "" }}
placeholder="Popis obalu nebo jeho obsahu"></textarea> <textarea name=description rows="{{ max 5 (lines $description) }}"
placeholder="Popis obalu nebo jeho obsahu">
{{- $description -}}
</textarea>
<footer> <footer>
<div> <div>
<label for=series>Řada:</label> <label for=series>Řada:</label>
<select name=series id=series> <select name=series id=series>
{{ range $prefix, $desc := .AllSeries }} {{- $preselect := or .NewSeries "" }}
{{- range $prefix, $desc := .AllSeries }}
<option value="{{ $prefix }}" <option value="{{ $prefix }}"
{{ if eq $prefix $preselect }}selected{{ end -}}
>{{ $prefix }} &mdash; {{ $desc }}</option> >{{ $prefix }} &mdash; {{ $desc }}</option>
{{ end }} {{- end }}
</select> </select>
</div> </div>
<div> <div>
<label for=parent>Nadobal:</label> <label for=parent>Nadobal:</label>
<input type=text name=parent id=parent value=""> <input type=text name=parent id=parent
value="{{ or .NewParent "" }}">
</div> </div>
<input type=submit value="Uložit"> <input type=submit value="Uložit">
</footer> </footer>
@@ -90,23 +103,35 @@
{{ range .Children }} {{ range .Children }}
<section> <section>
<header> <header>
<h3><a href="/?id={{ .Id }}">{{ .Id }}</a></h3> <h3><a href="container?id={{ .Id }}">{{ .Id }}</a>
<form method=post action="/label?id={{ .Id }}"> {{- range .Path }}
<small>&laquo; <a href="container?id={{ . }}">{{ . }}</a></small>
{{- end }}
</h3>
<form method=post action="label?id={{ .Id }}" target=_blank>
{{- if $.Container }}
<input type=hidden name=context value="{{ $.Container.Id }}">
{{- end }}
<input type=submit value="Vytisknout štítek"> <input type=submit value="Vytisknout štítek">
</form> </form>
<form method=post action="/?id={{ .Id }}&amp;remove"> <form method=post action="container?id={{ .Id }}&amp;remove">
{{- if $.Container }}
<input type=hidden name=context value="{{ $.Container.Id }}">
{{- end }}
<input type=submit value="Odstranit"> <input type=submit value="Odstranit">
</form> </form>
</header> </header>
{{ if .Description }}
{{- if .Description }}
<p>{{ .Description }} <p>{{ .Description }}
{{ end }} {{- end }}
{{ if .Children }}
{{- if .Children }}
<p> <p>
{{ range .Children }} {{- range .Children }}
<a href="/?id={{ .Id }}">{{ .Id }}</a> <a href="container?id={{ .Id }}">{{ .Id }}</a>
{{ end }} {{- end }}
{{ end }} {{- end }}
</section> </section>
{{ else }} {{ else }}
<p>Obal je prázdný. <p>Obal je prázdný.

View File

@@ -163,6 +163,7 @@ var errNoSuchContainer = errors.New("no such container")
var errCannotChangeSeriesNotEmpty = errors.New( var errCannotChangeSeriesNotEmpty = errors.New(
"cannot change the series of a non-empty container") "cannot change the series of a non-empty container")
var errCannotChangeNumber = errors.New("cannot change the number") var errCannotChangeNumber = errors.New("cannot change the number")
var errWouldContainItself = errors.New("container would contain itself")
var errContainerInUse = errors.New("container is in use") var errContainerInUse = errors.New("container is in use")
// Find and filter out the container in O(n). // Find and filter out the container in O(n).
@@ -204,19 +205,34 @@ func dbContainerCreate(c *Container) error {
} }
func dbContainerUpdate(c *Container, updated Container) error { func dbContainerUpdate(c *Container, updated Container) error {
newId := updated.Id() if _, ok := indexSeries[updated.Series]; !ok {
return errNoSuchSeries
}
if updated.Parent != "" && indexContainer[updated.Parent] == nil {
return errNoSuchContainer
}
newID := updated.Id()
if updated.Series != c.Series && len(c.Children()) > 0 { if updated.Series != c.Series && len(c.Children()) > 0 {
return errCannotChangeSeriesNotEmpty return errCannotChangeSeriesNotEmpty
} }
if updated.Number != c.Number { if updated.Number != c.Number {
return errCannotChangeNumber return errCannotChangeNumber
} }
if _, ok := indexContainer[newId]; ok && newId != c.Id() { if _, ok := indexContainer[newID]; ok && newID != c.Id() {
return errContainerAlreadyExists return errContainerAlreadyExists
} }
if updated.Parent != c.Parent { if updated.Parent != c.Parent {
// Relying on the invariant that we can't change the ID
// of a non-empty container.
for pv := &updated; pv.Parent != ""; pv = indexContainer[pv.Parent] {
if pv.Parent == updated.Id() {
return errWouldContainItself
}
}
indexChildren[c.Parent] = filterContainer(indexChildren[c.Parent], c) indexChildren[c.Parent] = filterContainer(indexChildren[c.Parent], c)
indexChildren[newId] = append(indexChildren[newId], c) indexChildren[updated.Parent] = append(indexChildren[updated.Parent], c)
} }
*c = updated *c = updated
return dbCommit() return dbCommit()
@@ -282,6 +298,7 @@ func loadDatabase() error {
if err != nil { if err != nil {
return err return err
} }
defer dbFile.Close()
if err := json.NewDecoder(dbFile).Decode(&db); err != nil { if err := json.NewDecoder(dbFile).Decode(&db); err != nil {
return err return err
} }

View File

@@ -1,6 +1,7 @@
{{ define "Title" }}Tisk štítku{{ end }} {{ define "Title" }}Tisk štítku{{ end }}
{{ define "Content" }} {{ define "Content" }}
<h2>Tisk štítku pro {{ .Id }}</h2>
<h2>Tisk štítku pro <a href="container?id={{ .Id }}">{{ .Id }}</a></h2>
{{ if .UnknownId }} {{ if .UnknownId }}
<p>Neznámý obal. <p>Neznámý obal.

View File

@@ -1,7 +1,9 @@
package main package main
import ( import (
"context"
"errors" "errors"
"html"
"html/template" "html/template"
"io" "io"
"log" "log"
@@ -9,7 +11,13 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
"os/signal"
"path"
"path/filepath" "path/filepath"
"regexp"
"strings"
"sync"
"syscall"
"time" "time"
"janouch.name/sklad/imgutil" "janouch.name/sklad/imgutil"
@@ -25,24 +33,10 @@ func executeTemplate(name string, w io.Writer, data interface{}) {
} }
} }
func wrap(inner func(http.ResponseWriter, *http.Request)) func(
http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if r.Method == http.MethodGet {
w.Header().Set("Cache-Control", "no-store")
}
inner(w, r)
}
}
func handleLogin(w http.ResponseWriter, r *http.Request) { func handleLogin(w http.ResponseWriter, r *http.Request) {
redirect := r.FormValue("redirect") redirect := r.FormValue("redirect")
if redirect == "" { if redirect == "" {
redirect = "/" redirect = "container"
} }
session := sessionGet(w, r) session := sessionGet(w, r)
@@ -81,14 +75,14 @@ func handleLogout(w http.ResponseWriter, r *http.Request) {
session := r.Context().Value(sessionContextKey{}).(*Session) session := r.Context().Value(sessionContextKey{}).(*Session)
session.LoggedIn = false session.LoggedIn = false
http.Redirect(w, r, "/", http.StatusSeeOther) http.Redirect(w, r, "login", http.StatusSeeOther)
} }
func handleContainerPost(r *http.Request) error { func handleContainerPost(r *http.Request) error {
id := ContainerId(r.FormValue("id")) id := ContainerId(r.FormValue("id"))
description := r.FormValue("description") description := strings.TrimSpace(r.FormValue("description"))
series := r.FormValue("series") series := r.FormValue("series")
parent := ContainerId(r.FormValue("parent")) parent := ContainerId(strings.TrimSpace(r.FormValue("parent")))
_, remove := r.Form["remove"] _, remove := r.Form["remove"]
if container, ok := indexContainer[id]; ok { if container, ok := indexContainer[id]; ok {
@@ -98,6 +92,7 @@ func handleContainerPost(r *http.Request) error {
c := *container c := *container
c.Description = description c.Description = description
c.Series = series c.Series = series
c.Parent = parent
return dbContainerUpdate(container, c) return dbContainerUpdate(container, c)
} }
} else if remove { } else if remove {
@@ -112,11 +107,22 @@ func handleContainerPost(r *http.Request) error {
} }
func handleContainer(w http.ResponseWriter, r *http.Request) { func handleContainer(w http.ResponseWriter, r *http.Request) {
// When deleting, do not try to show the deleted entry but the context.
shownId := r.FormValue("context")
if shownId == "" {
shownId = r.FormValue("id")
}
var err error var err error
if r.Method == http.MethodPost { if r.Method == http.MethodPost {
err = handleContainerPost(r) if err = handleContainerPost(r); err == nil {
// XXX: This is rather ugly. When removing, we want to keep redirect := r.URL.EscapedPath()
// the context id, in addition to the id being changed. if shownId != "" {
redirect += "?id=" + url.QueryEscape(shownId)
}
http.Redirect(w, r, redirect, http.StatusSeeOther)
return
}
} else if r.Method != http.MethodGet { } else if r.Method != http.MethodGet {
w.WriteHeader(http.StatusMethodNotAllowed) w.WriteHeader(http.StatusMethodNotAllowed)
return return
@@ -127,14 +133,6 @@ func handleContainer(w http.ResponseWriter, r *http.Request) {
allSeries[s.Prefix] = s.Description allSeries[s.Prefix] = s.Description
} }
var container *Container
children := indexChildren[""]
if c, ok := indexContainer[ContainerId(r.FormValue("id"))]; ok {
children = c.Children()
container = c
}
params := struct { params := struct {
Error error Error error
ErrorNoSuchSeries bool ErrorNoSuchSeries bool
@@ -142,8 +140,12 @@ func handleContainer(w http.ResponseWriter, r *http.Request) {
ErrorNoSuchContainer bool ErrorNoSuchContainer bool
ErrorCannotChangeSeriesNotEmpty bool ErrorCannotChangeSeriesNotEmpty bool
ErrorCannotChangeNumber bool ErrorCannotChangeNumber bool
ErrorWouldContainItself bool
ErrorContainerInUse bool ErrorContainerInUse bool
Container *Container Container *Container
NewDescription *string
NewSeries string
NewParent *string
Children []*Container Children []*Container
AllSeries map[string]string AllSeries map[string]string
}{ }{
@@ -153,18 +155,33 @@ func handleContainer(w http.ResponseWriter, r *http.Request) {
ErrorNoSuchContainer: err == errNoSuchContainer, ErrorNoSuchContainer: err == errNoSuchContainer,
ErrorCannotChangeSeriesNotEmpty: err == errCannotChangeSeriesNotEmpty, ErrorCannotChangeSeriesNotEmpty: err == errCannotChangeSeriesNotEmpty,
ErrorCannotChangeNumber: err == errCannotChangeNumber, ErrorCannotChangeNumber: err == errCannotChangeNumber,
ErrorWouldContainItself: err == errWouldContainItself,
ErrorContainerInUse: err == errContainerInUse, ErrorContainerInUse: err == errContainerInUse,
Container: container, Children: indexChildren[""],
Children: children,
AllSeries: allSeries, AllSeries: allSeries,
} }
if c, ok := indexContainer[ContainerId(shownId)]; ok {
params.Children = c.Children()
params.Container = c
}
if description, ok := r.Form["description"]; ok {
params.NewDescription = &description[0]
}
if series, ok := r.Form["series"]; ok {
// It seems impossible to dereference strings in text/template so that
// `eq` can be used, and we don't actually need a null value here.
params.NewSeries = series[0]
}
if parent, ok := r.Form["parent"]; ok {
params.NewParent = &parent[0]
}
executeTemplate("container.tmpl", w, &params) executeTemplate("container.tmpl", w, &params)
} }
func handleSeriesPost(r *http.Request) error { func handleSeriesPost(r *http.Request) error {
prefix := r.FormValue("prefix") prefix := strings.TrimSpace(r.FormValue("prefix"))
description := r.FormValue("description") description := strings.TrimSpace(r.FormValue("description"))
_, remove := r.Form["remove"] _, remove := r.Form["remove"]
if series, ok := indexSeries[prefix]; ok { if series, ok := indexSeries[prefix]; ok {
@@ -188,7 +205,10 @@ func handleSeriesPost(r *http.Request) error {
func handleSeries(w http.ResponseWriter, r *http.Request) { func handleSeries(w http.ResponseWriter, r *http.Request) {
var err error var err error
if r.Method == http.MethodPost { if r.Method == http.MethodPost {
err = handleSeriesPost(r) if err = handleSeriesPost(r); err == nil {
http.Redirect(w, r, r.URL.EscapedPath(), http.StatusSeeOther)
return
}
// XXX: This is rather ugly. // XXX: This is rather ugly.
r.Form = url.Values{} r.Form = url.Values{}
} else if r.Method != http.MethodGet { } else if r.Method != http.MethodGet {
@@ -289,7 +309,7 @@ func printLabel(id string) error {
} }
return printer.Print(&imgutil.LeftRotate{Image: label.GenLabelForHeight( return printer.Print(&imgutil.LeftRotate{Image: label.GenLabelForHeight(
labelFont, id, mediaInfo.PrintAreaPins, db.BDFScale)}) labelFont, id, mediaInfo.PrintAreaPins, db.BDFScale)}, false)
} }
func handleLabel(w http.ResponseWriter, r *http.Request) { func handleLabel(w http.ResponseWriter, r *http.Request) {
@@ -315,6 +335,67 @@ func handleLabel(w http.ResponseWriter, r *http.Request) {
executeTemplate("label.tmpl", w, &params) executeTemplate("label.tmpl", w, &params)
} }
var mutex sync.Mutex
func handle(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if r.Method == http.MethodGet {
w.Header().Set("Cache-Control", "no-store")
}
mutex.Lock()
defer mutex.Unlock()
switch _, base := path.Split(r.URL.Path); base {
case "login":
handleLogin(w, r)
case "logout":
sessionWrap(handleLogout)(w, r)
case "container":
sessionWrap(handleContainer)(w, r)
case "series":
sessionWrap(handleSeries)(w, r)
case "search":
sessionWrap(handleSearch)(w, r)
case "label":
sessionWrap(handleLabel)(w, r)
case "":
http.Redirect(w, r, "container", http.StatusSeeOther)
default:
http.NotFound(w, r)
}
}
var funcMap = template.FuncMap{
"max": func(i, j int) int {
if i > j {
return i
}
return j
},
"lines": func(s string) int {
return strings.Count(s, "\n") + 1
},
"highlight": func(highlight, s string) template.HTML {
b, last := strings.Builder{}, 0
for _, m := range regexp.MustCompile(
`(?i:`+regexp.QuoteMeta(highlight)+`)`).FindAllStringIndex(s, -1) {
b.WriteString(html.EscapeString(s[last:m[0]]))
b.WriteString(`<mark>`)
b.WriteString(html.EscapeString(s[m[0]:m[1]]))
b.WriteString(`</mark>`)
last = m[1]
}
b.WriteString(html.EscapeString(s[last:]))
return template.HTML(b.String())
},
}
func main() { func main() {
// Randomize the RNG for session string generation. // Randomize the RNG for session string generation.
rand.Seed(time.Now().UnixNano()) rand.Seed(time.Now().UnixNano())
@@ -337,16 +418,27 @@ func main() {
log.Fatalln(err) log.Fatalln(err)
} }
for _, name := range m { for _, name := range m {
templates[name] = template.Must(template.ParseFiles("base.tmpl", name)) templates[name] = template.Must(template.New("base.tmpl").
Funcs(funcMap).ParseFiles("base.tmpl", name))
} }
http.HandleFunc("/login", wrap(handleLogin)) http.HandleFunc("/", handle)
http.HandleFunc("/logout", sessionWrap(wrap(handleLogout))) server := &http.Server{Addr: address}
http.HandleFunc("/", sessionWrap(wrap(handleContainer))) sigs := make(chan os.Signal, 1)
http.HandleFunc("/series", sessionWrap(wrap(handleSeries))) errs := make(chan error, 1)
http.HandleFunc("/search", sessionWrap(wrap(handleSearch))) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
http.HandleFunc("/label", sessionWrap(wrap(handleLabel))) go func() { errs <- server.ListenAndServe() }()
log.Fatalln(http.ListenAndServe(address, nil)) select {
case <-sigs:
case err := <-errs:
log.Println(err)
}
// Wait for all HTTP goroutines to finish so that not even the database
// log gets corrupted by an interrupted update.
if err := server.Shutdown(context.Background()); err != nil {
log.Fatalln(err)
}
} }

View File

@@ -1,15 +1,15 @@
{{ define "Title" }}&bdquo;{{ .Query }}&ldquo; &mdash; Vyhledávání{{ end }} {{ define "Title" }}&bdquo;{{ .Query }}&ldquo; &mdash; Vyhledávání{{ end }}
{{ define "Content" }} {{ define "Content" }}
<h2>Vyhledávání: &bdquo;{{ .Query }}&ldquo;<h2> <h2>Vyhledávání: &bdquo;{{ .Query }}&ldquo;</h2>
<h3>Řady</h3> <h3>Řady</h3>
{{ range .Series }} {{ range .Series }}
<section> <section>
<header> <header>
<h3><a href="/series?prefix={{ .Prefix }}">{{ .Prefix }}</a></h3> <h3><a href="series?prefix={{ .Prefix }}">{{ .Prefix }}</a></h3>
<p>{{ .Description }} <p>{{ .Description | highlight $.Query }}
</header> </header>
</section> </section>
{{ else }} {{ else }}
@@ -21,15 +21,15 @@
{{ range .Containers }} {{ range .Containers }}
<section> <section>
<header> <header>
<h3><a href="/?id={{ .Id }}">{{ .Id }}</a> <h3><a href="container?id={{ .Id }}">{{ .Id }}</a>
{{ range .Path }} {{- range .Path }}
<small>&laquo; <a href="/?id={{ . }}">{{ . }}</a></small> <small>&laquo; <a href="container?id={{ . }}">{{ . }}</a></small>
{{ end }} {{- end }}
</h3> </h3>
</header> </header>
{{ if .Description }} {{- if .Description }}
<p>{{ .Description }} <p>{{ .Description | highlight $.Query }}
{{ end }} {{- end }}
</section> </section>
{{ else }} {{ else }}
<p>Neodpovídají žádné obaly. <p>Neodpovídají žádné obaly.

View File

@@ -22,15 +22,13 @@
<p>{{ .Description }} <p>{{ .Description }}
{{ end }} {{ end }}
{{ else }} {{ else }}
<section> <section>
<form method=post action="/series"> <form method=post action="series">
<header> <header>
<h3>Nová řada</h3> <h3>Nová řada</h3>
<input type=text name=prefix placeholder="Prefix řady"> <input type=text name=prefix placeholder="Prefix řady">
<input type=text name=description placeholder="Popis řady" <input type=text name=description placeholder="Popis řady"
><input type=submit value="Uložit"> ><input type=submit value="Uložit">
</form>
</header> </header>
</form> </form>
</section> </section>
@@ -38,21 +36,21 @@
{{ range .AllSeries }} {{ range .AllSeries }}
<section> <section>
<header> <header>
<h3><a href="/series?prefix={{ .Prefix }}">{{ .Prefix }}</a></h3> <h3><a href="series?prefix={{ .Prefix }}">{{ .Prefix }}</a></h3>
{{ with $count := len .Containers }} {{- with $count := len .Containers }}
{{ if eq $count 1 }} {{- if eq $count 1 }}
<p>{{ $count }} obal <p>{{ $count }} obal
{{ else if and (ge $count 2) (le $count 4) }} {{- else if and (ge $count 2) (le $count 4) }}
<p>{{ $count }} obaly <p>{{ $count }} obaly
{{ else if gt $count 0 }} {{- else if gt $count 0 }}
<p>{{ $count }} obalů <p>{{ $count }} obalů
{{ end }} {{- end }}
{{ end }} {{- end }}
<form method=post action="/series?prefix={{ .Prefix }}"> <form method=post action="series?prefix={{ .Prefix }}">
<input type=text name=description value="{{ .Description }}" <input type=text name=description value="{{ .Description }}"
><input type=submit value="Uložit"> ><input type=submit value="Uložit">
</form> </form>
<form method=post action="/series?prefix={{ .Prefix }}&amp;remove"> <form method=post action="series?prefix={{ .Prefix }}&amp;remove">
<input type=submit value="Odstranit"> <input type=submit value="Odstranit">
</form> </form>
</header> </header>
@@ -60,7 +58,6 @@
{{ else }} {{ else }}
<p>Nejsou žádné řady. <p>Nejsou žádné řady.
{{ end }} {{ end }}
{{ end }} {{ end }}
{{ end }} {{ end }}

View File

@@ -50,7 +50,7 @@ func sessionWrap(inner func(http.ResponseWriter, *http.Request)) func(
// though I don't expect any substantial improvements of anything. // though I don't expect any substantial improvements of anything.
w.Header().Set("Cache-Control", "no-store") w.Header().Set("Cache-Control", "no-store")
redirect := "/login" redirect := "login"
if r.RequestURI != "/" && r.Method == http.MethodGet { if r.RequestURI != "/" && r.Method == http.MethodGet {
redirect += "?redirect=" + url.QueryEscape(r.RequestURI) redirect += "?redirect=" + url.QueryEscape(r.RequestURI)
} }

27
go.mod
View File

@@ -1,28 +1,5 @@
module janouch.name/sklad module janouch.name/sklad
go 1.12 go 1.17
require ( require github.com/boombuler/barcode v1.0.1
github.com/boombuler/barcode v1.0.0
github.com/cosiner/argv v0.0.1 // indirect
github.com/cpuguy83/go-md2man v1.0.10 // indirect
github.com/go-delve/delve v1.2.0 // indirect
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
github.com/mattn/go-colorable v0.1.1 // indirect
github.com/mattn/go-isatty v0.0.7 // indirect
github.com/mattn/go-runewidth v0.0.4 // indirect
github.com/peterh/liner v1.1.0 // indirect
github.com/pkg/profile v1.3.0 // indirect
github.com/russross/blackfriday v2.0.0+incompatible // indirect
github.com/sirupsen/logrus v1.4.1 // indirect
github.com/spf13/cobra v0.0.3 // indirect
github.com/spf13/pflag v1.0.3 // indirect
github.com/stretchr/objx v0.2.0 // indirect
golang.org/x/arch v0.0.0-20190312162104-788fe5ffcd8c // indirect
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a // indirect
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 // indirect
golang.org/x/sys v0.0.0-20190411185658-b44545bcd369 // indirect
golang.org/x/tools v0.0.0-20190411180116-681f9ce8ac52 // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
gopkg.in/yaml.v2 v2.2.2 // indirect
)

72
go.sum
View File

@@ -1,70 +1,2 @@
github.com/boombuler/barcode v1.0.0 h1:s1TvRnXwL2xJRaccrdcBQMZxq6X7DvsMogtmJeHDdrc= github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyXcs=
github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/cosiner/argv v0.0.0-20170225145430-13bacc38a0a5/go.mod h1:p/NrK5tF6ICIly4qwEDsf6VDirFiWWz0FenfYBwJaKQ=
github.com/cosiner/argv v0.0.1 h1:2iAFN+sWPktbZ4tvxm33Ei8VY66FPCxdOxpncUGpAXE=
github.com/cosiner/argv v0.0.1/go.mod h1:p/NrK5tF6ICIly4qwEDsf6VDirFiWWz0FenfYBwJaKQ=
github.com/cpuguy83/go-md2man v1.0.8/go.mod h1:N6JayAiVKtlHSnuTCeuLSQVs75hb8q+dYQLjr7cDsKY=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-delve/delve v1.2.0 h1:uwGyfYO0WsWqbnDWvxCBKOr2qFLpii3tLxwM+fTJs70=
github.com/go-delve/delve v1.2.0/go.mod h1:yP+LD36s/ud5nm4lsQY0TwNhYu2PAwk6xItz+442j74=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/mattn/go-colorable v0.0.0-20170327083344-ded68f7a9561/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.7 h1:UvyT9uN+3r7yLEYSlJsbQGdsaB/a0DlgWP3pql6iwOc=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/peterh/liner v0.0.0-20170317030525-88609521dc4b/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc=
github.com/peterh/liner v1.1.0 h1:f+aAedNJA6uk7+6rXsYBnhdo4Xux7ESLe+kcuVUF5os=
github.com/peterh/liner v1.1.0/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0=
github.com/pkg/profile v0.0.0-20170413231811-06b906832ed0/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
github.com/pkg/profile v1.3.0/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday v0.0.0-20180428102519-11635eb403ff/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday v2.0.0+incompatible/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/sirupsen/logrus v0.0.0-20180523074243-ea8897e79973/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/spf13/cobra v0.0.0-20170417170307-b6cb39589372/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/pflag v0.0.0-20170417173400-9e4c21054fa1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
golang.org/x/arch v0.0.0-20171004143515-077ac972c2e4/go.mod h1:cYlCBUl1MsqxdiKgmc4uh7TxZfWSFLOGSRR090WDxt8=
golang.org/x/arch v0.0.0-20190312162104-788fe5ffcd8c h1:Rx/HTKi09myZ25t1SOlDHmHOy/mKxNAcu0hP1oPX9qM=
golang.org/x/arch v0.0.0-20190312162104-788fe5ffcd8c/go.mod h1:flIaEI6LNU6xOCD5PaJvn9wGP0agmIOqjrtsKGRguv4=
golang.org/x/crypto v0.0.0-20180614174826-fd5f17ee7299/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20180614134839-8883426083c0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190411185658-b44545bcd369 h1:aBlRBZoCuZNRDClvfkDoklQqdLzBaA3uViASg2z2p24=
golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20181120060634-fc4f04983f62/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190411180116-681f9ce8ac52/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.0.0-20170407172122-cd8b52f8269e/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

108
label/label.go Normal file
View File

@@ -0,0 +1,108 @@
package label
import (
"image"
"image/color"
"image/draw"
"strings"
"janouch.name/sklad/bdf"
"janouch.name/sklad/imgutil"
"github.com/boombuler/barcode"
"github.com/boombuler/barcode/qr"
)
// TODO: Rename to GenQRLabelForHeight.
func GenLabelForHeight(font *bdf.Font,
text string, height, scale int) image.Image {
// Create a scaled bitmap of the text label.
textRect, _ := font.BoundString(text)
textImg := image.NewRGBA(textRect)
draw.Draw(textImg, textRect, image.White, image.ZP, draw.Src)
font.DrawString(textImg, image.ZP, color.Black, text)
scaledTextImg := imgutil.Scale{Image: textImg, Scale: scale}
scaledTextRect := scaledTextImg.Bounds()
remains := height - scaledTextRect.Dy() - 20
width := scaledTextRect.Dx()
if remains > width {
width = remains
}
// Create a scaled bitmap of the QR code.
qrImg, _ := qr.Encode(text, qr.H, qr.Auto)
qrImg, _ = barcode.Scale(qrImg, remains, remains)
qrRect := qrImg.Bounds()
// Combine.
combinedRect := image.Rect(0, 0, width, height)
combinedImg := image.NewRGBA(combinedRect)
draw.Draw(combinedImg, combinedRect, image.White, image.ZP, draw.Src)
draw.Draw(combinedImg,
combinedRect.Add(image.Point{X: (width - qrRect.Dx()) / 2, Y: 0}),
qrImg, image.ZP, draw.Src)
target := image.Rect(
(width-scaledTextRect.Dx())/2, qrRect.Dy()+20,
combinedRect.Max.X, combinedRect.Max.Y)
draw.Draw(combinedImg, target, &scaledTextImg, scaledTextRect.Min, draw.Src)
return combinedImg
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
func GenLabelForWidth(font *bdf.Font,
text string, width, scale int) image.Image {
var lines []string
for _, line := range strings.Split(text, "\n") {
lines = append(lines, strings.TrimSuffix(line, "\r"))
}
// Respect font ascent and descent so that there are gaps between lines.
rects := make([]image.Rectangle, len(lines))
jumps := make([]int, len(lines))
for i, line := range lines {
r, _ := font.BoundString(line)
rects[i] = r
if i > 0 {
deficitD := font.Descent - rects[i-1].Max.Y
jumps[i] += max(0, deficitD)
deficitA := font.Ascent - (-r.Min.Y)
jumps[i] += max(0, deficitA)
}
}
height := 0
for i := range lines {
height += jumps[i] + rects[i].Dy()
}
imgRect := image.Rect(0, 0, width, height*scale)
img := image.NewRGBA(imgRect)
draw.Draw(img, imgRect, image.White, image.ZP, draw.Src)
y := 0
for i, line := range lines {
textImg := image.NewRGBA(rects[i])
draw.Draw(textImg, rects[i], image.White, image.ZP, draw.Src)
font.DrawString(textImg, image.ZP, color.Black, line)
scaledImg := imgutil.Scale{Image: textImg, Scale: scale}
scaledRect := scaledImg.Bounds()
y += jumps[i]
target := image.Rect(0, y*scale, imgRect.Max.X, imgRect.Max.Y)
draw.Draw(img, target, &scaledImg, scaledRect.Min, draw.Src)
y += rects[i].Dy()
}
return img
}

104
ql/ql.go
View File

@@ -120,9 +120,67 @@ const (
printPins = printBytes * 8 printPins = printBytes * 8
) )
// pack packs a bool array into a byte array for the printer to print out.
func pack(data [printPins]bool, out *[]byte) {
for i := 0; i < printBytes; i++ {
var b byte
for j := 0; j < 8; j++ {
b <<= 1
if data[i*8+j] {
b |= 1
}
}
*out = append(*out, b)
}
}
// makeBitmapDataRB converts an image to the printer's red-black raster format.
func makeBitmapDataRB(src image.Image, margin, length int) []byte {
data, bounds := []byte{}, src.Bounds()
if bounds.Dy() > length {
bounds.Max.Y = bounds.Min.Y + length
}
if bounds.Dx() > printPins-margin {
bounds.Max.X = bounds.Min.X + printPins
}
redcells, blackcells := [printPins]bool{}, [printPins]bool{}
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
length--
// The graphics needs to be inverted horizontally, iterating backwards.
offset := margin
for x := bounds.Max.X - 1; x >= bounds.Min.X; x-- {
r, g, b, a := src.At(x, y).RGBA()
redcells[offset] = r >= 0xc000 && g < 0x4000 && b < 0x4000 &&
a >= 0x8000
blackcells[offset] = r < 0x4000 && g < 0x4000 && b < 0x4000 &&
a >= 0x8000
offset++
}
data = append(data, 'w', 0x01, printBytes)
pack(blackcells, &data)
data = append(data, 'w', 0x02, printBytes)
pack(redcells, &data)
}
for ; length > 0; length-- {
data = append(data, 'w', 0x01, printBytes)
data = append(data, make([]byte, printBytes)...)
data = append(data, 'w', 0x02, printBytes)
data = append(data, make([]byte, printBytes)...)
}
return data
}
// makeBitmapData converts an image to the printer's raster format. // makeBitmapData converts an image to the printer's raster format.
func makeBitmapData(src image.Image, margin, length int) (data []byte) { func makeBitmapData(src image.Image, rb bool, margin, length int) []byte {
bounds := src.Bounds() // It's a necessary nuisance, so just copy and paste.
if rb {
return makeBitmapDataRB(src, margin, length)
}
data, bounds := []byte{}, src.Bounds()
if bounds.Dy() > length { if bounds.Dy() > length {
bounds.Max.Y = bounds.Min.Y + length bounds.Max.Y = bounds.Min.Y + length
} }
@@ -138,30 +196,24 @@ func makeBitmapData(src image.Image, margin, length int) (data []byte) {
offset := margin offset := margin
for x := bounds.Max.X - 1; x >= bounds.Min.X; x-- { for x := bounds.Max.X - 1; x >= bounds.Min.X; x-- {
r, g, b, a := src.At(x, y).RGBA() r, g, b, a := src.At(x, y).RGBA()
pixels[offset] = r == 0 && g == 0 && b == 0 && a >= 0x8000 pixels[offset] = r < 0x4000 && g < 0x4000 && b < 0x4000 &&
a >= 0x8000
offset++ offset++
} }
data = append(data, 'g', 0x00, printBytes) data = append(data, 'g', 0x00, printBytes)
for i := 0; i < printBytes; i++ { pack(pixels, &data)
var b byte
for j := 0; j < 8; j++ {
b <<= 1
if pixels[i*8+j] {
b |= 1
}
}
data = append(data, b)
}
} }
for ; length > 0; length-- { for ; length > 0; length-- {
data = append(data, 'g', 0x00, printBytes) data = append(data, 'g', 0x00, printBytes)
data = append(data, make([]byte, printBytes)...) data = append(data, make([]byte, printBytes)...)
} }
return return data
} }
func makePrintData(status *Status, image image.Image) (data []byte) { // XXX: It would be preferrable to know for certain if this is a red-black tape,
// because the printer refuses to print on a mismatch.
func makePrintData(status *Status, image image.Image, rb bool) (data []byte) {
mediaInfo := GetMediaInfo( mediaInfo := GetMediaInfo(
status.MediaWidthMM(), status.MediaWidthMM(),
status.MediaLengthMM(), status.MediaLengthMM(),
@@ -188,8 +240,17 @@ func makePrintData(status *Status, image image.Image) (data []byte) {
mediaType = byte(0x0b) mediaType = byte(0x0b)
} }
data = append(data, 0x1b, 0x69, 0x7a, 0x02|0x04|0x40|0x80, mediaType, const (
byte(status.MediaWidthMM()), byte(status.MediaLengthMM()), flagValidMediaType = 0x02
flagValidMediaWidth = 0x04
flagValidMediaLength = 0x08
flagPriorityToQuality = 0x40
flagRecoveryAlwaysOn = 0x80
)
data = append(data, 0x1b, 0x69, 0x7a, flagValidMediaType|
flagValidMediaWidth|flagValidMediaLength|
flagPriorityToQuality|flagRecoveryAlwaysOn,
mediaType, byte(status.MediaWidthMM()), byte(status.MediaLengthMM()),
byte(dy), byte(dy>>8), byte(dy>>16), byte(dy>>24), 0, 0x00) byte(dy), byte(dy>>8), byte(dy>>16), byte(dy>>24), 0, 0x00)
// Auto cut, each 1 label. // Auto cut, each 1 label.
@@ -198,9 +259,13 @@ func makePrintData(status *Status, image image.Image) (data []byte) {
// Cut at end (though it's the default). // Cut at end (though it's the default).
// Not sure what it means, doesn't seem to have any effect to turn it off. // Not sure what it means, doesn't seem to have any effect to turn it off.
if rb {
data = append(data, 0x1b, 0x69, 0x4b, 0x08|0x01)
} else {
data = append(data, 0x1b, 0x69, 0x4b, 0x08) data = append(data, 0x1b, 0x69, 0x4b, 0x08)
}
if status.MediaLengthMM() != 0 { if status.MediaLengthMM() == 0 {
// 3mm margins along the direction of feed. 0x23 = 35 dots, the minimum. // 3mm margins along the direction of feed. 0x23 = 35 dots, the minimum.
data = append(data, 0x1b, 0x69, 0x64, 0x23, 0x00) data = append(data, 0x1b, 0x69, 0x64, 0x23, 0x00)
} else { } else {
@@ -213,7 +278,8 @@ func makePrintData(status *Status, image image.Image) (data []byte) {
data = append(data, 0x4d, 0x00) data = append(data, 0x4d, 0x00)
// The graphics data itself. // The graphics data itself.
data = append(data, makeBitmapData(image, mediaInfo.SideMarginPins, dy)...) bitmapData := makeBitmapData(image, rb, mediaInfo.SideMarginPins, dy)
data = append(data, bitmapData...)
// Print command with feeding. // Print command with feeding.
return append(data, 0x1a) return append(data, 0x1a)

View File

@@ -172,8 +172,8 @@ var errErrorOccurred = errors.New("error occurred")
var errUnexpectedStatus = errors.New("unexpected status") var errUnexpectedStatus = errors.New("unexpected status")
var errUnknownMedia = errors.New("unknown media") var errUnknownMedia = errors.New("unknown media")
func (p *Printer) Print(image image.Image) error { func (p *Printer) Print(image image.Image, rb bool) error {
data := makePrintData(p.LastStatus, image) data := makePrintData(p.LastStatus, image, rb)
if data == nil { if data == nil {
return errUnknownMedia return errUnknownMedia
} }

BIN
sklad.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB