From 287707a752b855cf10902815cd2a8cf59d2a6745 Mon Sep 17 00:00:00 2001 From: rex <1073853456@qq.com> Date: Thu, 17 Nov 2022 10:16:23 +0800 Subject: [PATCH] reorganize file structure --- src/QRCoders.jl | 457 +--------------------- src/encode.jl | 12 + src/export.jl | 282 +++++++++++++ src/{errorcorrection.jl => polynomial.jl} | 0 src/{ => preprocess}/kanji.jl | 0 src/{ => preprocess}/matrix.jl | 0 src/{ => preprocess}/tables.jl | 3 +- src/styles/locate.jl | 88 +++++ src/{style.jl => styles/plotimage.jl} | 166 +------- src/styles/style.jl | 23 ++ src/styles/unicodeplot.jl | 58 +++ src/types.jl | 159 ++++++++ 12 files changed, 640 insertions(+), 608 deletions(-) create mode 100644 src/export.jl rename src/{errorcorrection.jl => polynomial.jl} (100%) rename src/{ => preprocess}/kanji.jl (100%) rename src/{ => preprocess}/matrix.jl (100%) rename src/{ => preprocess}/tables.jl (99%) create mode 100644 src/styles/locate.jl rename src/{style.jl => styles/plotimage.jl} (54%) create mode 100644 src/styles/style.jl create mode 100644 src/styles/unicodeplot.jl create mode 100644 src/types.jl diff --git a/src/QRCoders.jl b/src/QRCoders.jl index 38c49c5..2c7efa8 100644 --- a/src/QRCoders.jl +++ b/src/QRCoders.jl @@ -27,455 +27,26 @@ export imagebyerrcor, animatebyerrcor, generator_matrix -""" -Invalid step in encoding process. -""" -struct EncodeError <: Exception - st::AbstractString -end +# Data types in QRCoders +include("types.jl") -# Encoding mode of the QR code -""" -Abstract type that groups the five supported encoding modes `Numeric`, -`Alphanumeric`, `Byte`, `Kanji` and `UTF8`. -""" -abstract type Mode end -""" -Encoding mode for messages composed of digits only. -""" -struct Numeric <: Mode end -""" -Encoding mode for messages composed of digits, characters `A`-`Z` (capital only) -, space and `%` `*` `+` `-` `.` `/` `:` `\$`. -""" -struct Alphanumeric <: Mode end -""" -Encoding mode for messages composed of one-byte characters(unicode range from -0x00 to 0xff, including ISO-8859-1 and undefined characters) -""" -struct Byte <: Mode end -""" -Encoding mode for messages composed of Shift JIS(Shift Japanese Industrial Standards) characters. -""" -struct Kanji <: Mode end -""" -Encoding mode for messages composed of utf-8 characters. -""" -struct UTF8 <: Mode end +# Data tables from the specificatioms +include("preprocess/tables.jl") -# relationships between the encoding modes -import Base: issubset -""" - issubset(mode1::Mode, mode2::Mode) - ⊆(mode1::Mode, mode2::Mode) - -Returns `true` if the character set of `mode1` is a subset of the character set of `mode2`. -""" -issubset(::Mode, ::UTF8) = true -issubset(mode::Mode, ::Numeric) = mode == Numeric() -issubset(mode::Mode, ::Alphanumeric) = (mode == Alphanumeric() || mode == Numeric()) -issubset(mode::Mode, ::Byte) = (mode != UTF8() && mode != Kanji()) -issubset(mode::Mode, ::Kanji) = mode == Kanji() - -# Error correction level of the QR code -""" -Abstract type that groups the four error correction levels `Low`, `Medium`, -`Quartile` and `High`. -""" -abstract type ErrCorrLevel end -""" -Error correction level that can restore up to 7% of missing codewords. -""" -struct Low <: ErrCorrLevel end -""" -Error correction level that can restore up to 15% of missing codewords. -""" -struct Medium <: ErrCorrLevel end -""" -Error correction level that can restore up to 25% of missing codewords. -""" -struct Quartile <: ErrCorrLevel end -""" -Error correction level that can restore up to 30% of missing codewords. -""" -struct High <: ErrCorrLevel end - -""" - QRCode - -A type that represents a QR code. - -# Fields -- `version::Int`: version of the QR code -- `mode::Mode`: encoding mode of the QR code -- `eclevel::ErrCorrLevel`: error correction level of the QR code -- `mask::Int`: mask pattern of the QR code -- `message::String`: message to be encoded -- `border::Int`: width of the white border -""" -mutable struct QRCode - version::Int # version of the QR code - mode::Mode # encoding mode - eclevel::ErrCorrLevel - mask::Int # mask pattern - message::String # message to be encoded - border::Int # width of the white border -end - -""" - copy(code::QRCode) - -Returns a copy of `code`. -""" -function Base.copy(code::QRCode) - QRCode(code.version, code.mode, code.eclevel, code.mask, code.message, code.border) -end - -""" - QRCode(message::AbstractString) - -Create a QR code with the default settings. -""" -function QRCode( message::AbstractString - ; mask::Int = -1 - , eclevel::ErrCorrLevel = Medium() - , mode::Mode = Numeric() - , version::Int = 0 - , width::Int = 1) - minmode = getmode(message) - mode = issubset(minmode, mode) ? mode : minmode - minversion = getversion(message, mode, eclevel) - version = version ≥ minversion ? version : minversion - # valid mask pattern (0-7) - 0 ≤ mask ≤ 7 && return QRCode(version, mode, eclevel, mask, message, width) - - # find the best mask - data = encodemessage(message, mode, eclevel, version) - matrix = emptymatrix(version) - masks = makemasks(matrix) - matrix = placedata!(matrix, data) # fill in data bits - addversion!(matrix, version) # fill in version bits - # Apply mask and add format information - maskedmats = [addformat!(xor.(matrix, mat), i-1, eclevel) - for (i, mat) in enumerate(masks)] - # Pick the best mask - mask = argmin(penalty.(maskedmats)) - 1 - QRCode(version, mode, eclevel, mask, message, width) -end - -""" - qrwidth(code::QRCode) - -Return the width of the QR code. -""" -qrwidth(code::QRCode) = 4 * code.version + 17 + 2 * code.border - -""" - show(io::IO, code::QRCode) - -Show the QR code in REPL using `unicodeplotbychar`. -""" -Base.show(io::IO, code::QRCode) = print(io, unicodeplotbychar(qrcode(code))) - -include("tables.jl") -include("errorcorrection.jl") -include("matrix.jl") +# Polynomial arithmetic +include("polynomial.jl") using .Polynomial -include("encode.jl") -""" - function addborder(matrix::AbstractMatrix, width::Int) +# Manipulations for creating the QR code matrix +include("preprocess/matrix.jl") -Add a white border of width `width` to the QR code. -""" -function addborder(matrix::AbstractMatrix, width::Int) - width == 0 && return matrix - background = falses(size(matrix) .+ (width*2, width*2)) - background[width+1:end-width, width+1:end-width] = matrix - return background -end - -""" - qrcode( message::AbstractString - ; eclevel::ErrCorrLevel = Medium() - , version::Int = 0 - , mode::Mode = Numeric() - , mask::Int = -1 - , width::Int=0) - -Create a `BitArray{2}` with the encoded `message`, with `true` (`1`) for the black -areas and `false` (`0`) as the white ones. - -The error correction level `eclevel` can be picked from four values: `Low()` -(7% of missing data can be restored), `Medium()` (15%), `Quartile()` (25%) or -`High()` (30%). Higher levels make denser QR codes. - -The version of the QR code can be picked from 1 to 40. If the assigned version is -too small to contain the message, the first available version is used. - -The encoding mode `mode` can be picked from five values: `Numeric()`, `Alphanumeric()`, -`Byte()`, `Kanji()` or `UTF8()`. If the assigned `mode` is `nothing` or failed to contain the message, -the mode is automatically picked. - -The mask pattern `mask` can be picked from 0 to 7. If the assigned `mask` is `nothing`, -the mask pattern will picked by the penalty rules. -""" -function qrcode( message::AbstractString - ; eclevel::ErrCorrLevel = Medium() - , version::Int = 0 - , mode::Mode = Numeric() - , mask::Int = -1 - , compact::Bool = false - , width::Int = 0) - isempty(message) && @warn( - "Most QR code scanners don't support empty message!") - # Determining mode and version of the QR code - bestmode = getmode(message) - mode = bestmode ⊆ mode ? mode : bestmode - - version > 40 && throw(EncodeError("Version $version should be no larger than 40")) - minversion = getversion(message, mode, eclevel) - if version < minversion # the specified version is too small - version = minversion - end - - # encode message - data = encodemessage(message, mode, eclevel, version) - - # Generate qr code matrix - matrix = emptymatrix(version) - masks = makemasks(matrix) # 8 masks - matrix = placedata!(matrix, data) # fill in data bits - addversion!(matrix, version) # fill in version bits - - # Apply mask and add format information - maskedmats = [addformat!(xor.(matrix, mat), i-1, eclevel) - for (i, mat) in enumerate(masks)] - - # Pick the best mask - if !(0 ≤ mask ≤ 7) # invalid mask - mask = argmin(penalty.(maskedmats)) - 1 - end - matrix = maskedmats[mask + 1] - - # white border - (compact || width == 0) && return matrix # keyword compact will be removed in the future - return addborder(matrix, width) -end - -""" - _resize(matrix::AbstractMatrix, widthpixels::Int) - -Resize the width of the QR code to `widthpixels` pixels(approximately). - -Note: the size of the resulting matrix is an integer multiple of the size of the original one. -""" -function _resize(matrix::AbstractMatrix, widthpixels::Int = 160) - scale = ceil(Int, widthpixels / size(matrix, 1)) - kron(matrix, trues(scale, scale)) -end - -""" - exportbitmat(matrix::BitMatrix, path::AbstractString; pixels::Int = 160) - -Export the `BitMatrix` `matrix` to an image with file path `path`. -""" -function exportbitmat( matrix::BitMatrix - , path::AbstractString - ; targetsize::Int=0 - , pixels::Int=160) - # check whether the image format is supported - supportexts = ["png", "jpg", "gif"] - if !endswith(path, r"\.\w+") - path *= ".png" - else - ext = last(split(path, '.')) - ext ∈ supportexts || throw(EncodeError( - "Unsupported file extension: $ext\n Supported extensions: $supportexts")) - end - # resize the matrix - if targetsize > 0 # original keyword -- will be removed in the future - n = size(matrix, 1) - pixels = ceil(Int, 72 * targetsize / 2.45 / n) * n - end - save(path, .! _resize(matrix, pixels)) -end - -""" - exportqrcode( message::AbstractString - , path::AbstractString = "qrcode.png" - ; eclevel::ErrCorrLevel = Medium() - , version::Int = 0 - , mode::Mode = nothing - , width::int = 4 - , pixels::Int = 160) - -Create an image with the encoded `message` of approximate size `pixels x pixels`. - -The error correction level `eclevel` can be picked from four values: `Low()` -(7% of missing data can be restored), `Medium()` (15%), `Quartile()` (25%) or -`High()` (30%). Higher levels make denser QR codes. - -The version of the QR code can be picked from 1 to 40. If the assigned version is -too small to contain the message, the first available version is used. - -The encoding mode `mode` can be picked from four values: `Numeric()`, `Alphanumeric()`, -`Byte()`, `Kanji()` or `UTF8()`. If the assigned `mode` is `nothing` or failed to contain the message, -the mode is automatically picked. - -The mask pattern `mask` can be picked from 0 to 7. If the assigned `mask` is `nothing`, -the mask pattern will picked by the penalty rules. -""" -function exportqrcode( message::AbstractString - , path::AbstractString = "qrcode.png" - ; eclevel::ErrCorrLevel = Medium() - , version::Int = 0 - , mode::Mode = Numeric() - , mask::Int = -1 - , width::Int = 4 - , compact::Bool = false - , targetsize::Int = 0 - , pixels::Int = 160) - # encode data - matrix = qrcode(message; eclevel=eclevel, - version=version, - mode=mode, - mask=mask, - compact=compact, - width=width) - exportbitmat(matrix, path; targetsize=targetsize, pixels=pixels) -end - -# new API for qrcode and exportqrcode -""" - qrcode(code::QRCode) - -Create a QR code matrix by the `QRCode` object. - -Note: It would raise an error if failed to use the specified `mode`` or `version`. -""" -function qrcode(code::QRCode) - # raise error if failed to use the specified mode or version - mode, eclevel, version, mask = code.mode, code.eclevel, code.version, code.mask - message, width = code.message, code.border - getmode(message) ⊆ mode || throw(EncodeError("Mode $mode can not encode the message")) - getversion(message, mode, eclevel) ≤ version ≤ 40 || throw(EncodeError("The version $version is too small")) - - # encode message - data = encodemessage(message, mode, eclevel, version) - - # Generate qr code matrix - matrix = emptymatrix(version) - maskmat = makemask(matrix, mask) - matrix = placedata!(matrix, data) # fill in data bits - addversion!(matrix, version) # fill in version bits - matrix = addformat!(xor.(matrix, maskmat), mask, eclevel) - - # white border - addborder(matrix, width) -end - -""" - exportqrcode( code::QRCode - , path::AbstractString = "qrcode.png" - ; pixels::Int = 160) - -Create an image with the encoded `message` of approximate size `targetsize`. -""" -function exportqrcode( code::QRCode - , path::AbstractString = "qrcode.png" - ; targetsize::Int = 0 - , pixels::Int = 160) - matrix = qrcode(code) - exportbitmat(matrix, path; targetsize=targetsize, pixels=pixels) -end - -""" - exportqrcode( codes::AbstractVector{QRCode} - , path::AbstractString = "qrcode.gif" - ; pixels::Int = 160 - , fps::Int = 2) - -Create an animated gif with `codes` of approximate size `targetsize`. - -The frame rate `fps` is the number of frames per second. - -Note: The `codes` should have the same size while the other properties can be different. -""" -function exportqrcode( codes::AbstractVector{QRCode} - , path::AbstractString = "qrcode.gif" - ; targetsize::Int = 0 - , pixels::Int = 160 - , fps::Int = 2) - matwidth = qrwidth(first(codes)) - all(==(matwidth), qrwidth.(codes)) || throw(EncodeError("The codes should have the same size")) - # check whether the image format is supported - if !endswith(path, r"\.\w+") - path *= ".gif" - else - ext = last(split(path, '.')) - ext == "gif" || throw(EncodeError( - "$ext\n is not a valid format for animated images")) - end - # generate frames - if targetsize > 0 # original keyword -- will be removed in the future - Base.depwarn("keyword `targetsize` will be removed in the future, use `pixels` instead", :exportbitmat) - pixels = ceil(Int, 72 * targetsize / 2.45 / matwidth) * matwidth - else - pixels = ceil(Int, pixels / matwidth) * matwidth - end - code = Array{Bool}(undef, pixels, pixels, length(codes)) - for (i, c) in enumerate(codes) - code[:,:,i] = _resize(qrcode(c), pixels) - end - save(path, .! code, fps=fps) -end - -""" - exportqrcode( msgs::AbstractVector{<:AbstractString} - , path::AbstractString = "qrcode.gif" - ; eclevel::ErrCorrLevel = Medium() - , version::Int = 0 - , mode::Mode = Numeric() - , mask::Int = -1 - , width::Int = 4 - , targetsize::Int = 5 - , pixels::Int = 160 - , fps::Int = 2) - -Create an animated gif with `msgs` of approximate size `pixels x pixels`. +# Encoding process +include("encode.jl") -The frame rate `fps` is the number of frames per second. -""" -function exportqrcode( msgs::AbstractVector{<:AbstractString} - , path::AbstractString = "qrcode.gif" - ; eclevel::ErrCorrLevel = Medium() - , version::Int = 0 - , mode::Mode = Numeric() - , mask::Int = -1 - , width::Int = 4 - , targetsize::Int = 0 - , pixels::Int = 160 - , fps::Int = 2) - codes = QRCode.( msgs - ; eclevel=eclevel - , version=version - , mode=mode - , mask=mask - , width=width) - # the image should have the same size - ## method 1: edit version - version = maximum(getproperty.(codes, :version)) - setproperty!.(codes, :version, version) - ## method 2: enlarge width of the small ones - # maxwidth = first(maximum(qrwidth.(codes))) - # for code in codes - # code.border += (maxwidth - qrwidth(code)) ÷ 2 - # end - # setproperty!.(codes, :width, width) - exportqrcode(codes, path; targetsize=targetsize, pixels=pixels, fps=fps) -end +# Special QR codes +include("styles/style.jl") -include("style.jl") +# Generate and export QR code +include("export.jl") end # module diff --git a/src/encode.jl b/src/encode.jl index ca9e5b1..ea3cf0c 100644 --- a/src/encode.jl +++ b/src/encode.jl @@ -292,3 +292,15 @@ function encodemessage(msg::AbstractString, mode::Mode, eclevel::ErrCorrLevel, v return msgbits end + +""" + function addborder(matrix::AbstractMatrix, width::Int) + +Add a white border of width `width` to the QR code. +""" +function addborder(matrix::AbstractMatrix, width::Int) + width == 0 && return matrix + background = falses(size(matrix) .+ (width*2, width*2)) + background[width+1:end-width, width+1:end-width] = matrix + return background +end \ No newline at end of file diff --git a/src/export.jl b/src/export.jl new file mode 100644 index 0000000..a4af11a --- /dev/null +++ b/src/export.jl @@ -0,0 +1,282 @@ +# Generate and export QR codes + +""" + qrcode( message::AbstractString + ; eclevel::ErrCorrLevel = Medium() + , version::Int = 0 + , mode::Mode = Numeric() + , mask::Int = -1 + , width::Int=0) + +Create a `BitArray{2}` with the encoded `message`, with `true` (`1`) for the black +areas and `false` (`0`) as the white ones. + +The error correction level `eclevel` can be picked from four values: `Low()` +(7% of missing data can be restored), `Medium()` (15%), `Quartile()` (25%) or +`High()` (30%). Higher levels make denser QR codes. + +The version of the QR code can be picked from 1 to 40. If the assigned version is +too small to contain the message, the first available version is used. + +The encoding mode `mode` can be picked from five values: `Numeric()`, `Alphanumeric()`, +`Byte()`, `Kanji()` or `UTF8()`. If the assigned `mode` is `nothing` or failed to contain the message, +the mode is automatically picked. + +The mask pattern `mask` can be picked from 0 to 7. If the assigned `mask` is `nothing`, +the mask pattern will picked by the penalty rules. +""" +function qrcode( message::AbstractString + ; eclevel::ErrCorrLevel = Medium() + , version::Int = 0 + , mode::Mode = Numeric() + , mask::Int = -1 + , compact::Bool = false + , width::Int = 0) + isempty(message) && @warn( + "Most QR code scanners don't support empty message!") + # Determining mode and version of the QR code + bestmode = getmode(message) + mode = bestmode ⊆ mode ? mode : bestmode + + version > 40 && throw(EncodeError("Version $version should be no larger than 40")) + minversion = getversion(message, mode, eclevel) + if version < minversion # the specified version is too small + version = minversion + end + + # encode message + data = encodemessage(message, mode, eclevel, version) + + # Generate qr code matrix + matrix = emptymatrix(version) + masks = makemasks(matrix) # 8 masks + matrix = placedata!(matrix, data) # fill in data bits + addversion!(matrix, version) # fill in version bits + + # Apply mask and add format information + maskedmats = [addformat!(xor.(matrix, mat), i-1, eclevel) + for (i, mat) in enumerate(masks)] + + # Pick the best mask + if !(0 ≤ mask ≤ 7) # invalid mask + mask = argmin(penalty.(maskedmats)) - 1 + end + matrix = maskedmats[mask + 1] + + # white border + (compact || width == 0) && return matrix # keyword compact will be removed in the future + return addborder(matrix, width) +end + +""" + _resize(matrix::AbstractMatrix, widthpixels::Int) + +Resize the width of the QR code to `widthpixels` pixels(approximately). + +Note: the size of the resulting matrix is an integer multiple of the size of the original one. +""" +function _resize(matrix::AbstractMatrix, widthpixels::Int = 160) + scale = ceil(Int, widthpixels / size(matrix, 1)) + kron(matrix, trues(scale, scale)) +end + +""" + exportbitmat(matrix::BitMatrix, path::AbstractString; pixels::Int = 160) + +Export the `BitMatrix` `matrix` to an image with file path `path`. +""" +function exportbitmat( matrix::BitMatrix + , path::AbstractString + ; targetsize::Int=0 + , pixels::Int=160) + # check whether the image format is supported + supportexts = ["png", "jpg", "gif"] + if !endswith(path, r"\.\w+") + path *= ".png" + else + ext = last(split(path, '.')) + ext ∈ supportexts || throw(EncodeError( + "Unsupported file extension: $ext\n Supported extensions: $supportexts")) + end + # resize the matrix + if targetsize > 0 # original keyword -- will be removed in the future + n = size(matrix, 1) + pixels = ceil(Int, 72 * targetsize / 2.45 / n) * n + end + save(path, .! _resize(matrix, pixels)) +end + +""" + exportqrcode( message::AbstractString + , path::AbstractString = "qrcode.png" + ; eclevel::ErrCorrLevel = Medium() + , version::Int = 0 + , mode::Mode = nothing + , width::int = 4 + , pixels::Int = 160) + +Create an image with the encoded `message` of approximate size `pixels x pixels`. + +The error correction level `eclevel` can be picked from four values: `Low()` +(7% of missing data can be restored), `Medium()` (15%), `Quartile()` (25%) or +`High()` (30%). Higher levels make denser QR codes. + +The version of the QR code can be picked from 1 to 40. If the assigned version is +too small to contain the message, the first available version is used. + +The encoding mode `mode` can be picked from four values: `Numeric()`, `Alphanumeric()`, +`Byte()`, `Kanji()` or `UTF8()`. If the assigned `mode` is `nothing` or failed to contain the message, +the mode is automatically picked. + +The mask pattern `mask` can be picked from 0 to 7. If the assigned `mask` is `nothing`, +the mask pattern will picked by the penalty rules. +""" +function exportqrcode( message::AbstractString + , path::AbstractString = "qrcode.png" + ; eclevel::ErrCorrLevel = Medium() + , version::Int = 0 + , mode::Mode = Numeric() + , mask::Int = -1 + , width::Int = 4 + , compact::Bool = false + , targetsize::Int = 0 + , pixels::Int = 160) + # encode data + matrix = qrcode(message; eclevel=eclevel, + version=version, + mode=mode, + mask=mask, + compact=compact, + width=width) + exportbitmat(matrix, path; targetsize=targetsize, pixels=pixels) +end + +# new API for qrcode and exportqrcode +""" + qrcode(code::QRCode) + +Create a QR code matrix by the `QRCode` object. + +Note: It would raise an error if failed to use the specified `mode`` or `version`. +""" +function qrcode(code::QRCode) + # raise error if failed to use the specified mode or version + mode, eclevel, version, mask = code.mode, code.eclevel, code.version, code.mask + message, width = code.message, code.border + getmode(message) ⊆ mode || throw(EncodeError("Mode $mode can not encode the message")) + getversion(message, mode, eclevel) ≤ version ≤ 40 || throw(EncodeError("The version $version is too small")) + + # encode message + data = encodemessage(message, mode, eclevel, version) + + # Generate qr code matrix + matrix = emptymatrix(version) + maskmat = makemask(matrix, mask) + matrix = placedata!(matrix, data) # fill in data bits + addversion!(matrix, version) # fill in version bits + matrix = addformat!(xor.(matrix, maskmat), mask, eclevel) + + # white border + addborder(matrix, width) +end + +""" + exportqrcode( code::QRCode + , path::AbstractString = "qrcode.png" + ; pixels::Int = 160) + +Create an image with the encoded `message` of approximate size `targetsize`. +""" +function exportqrcode( code::QRCode + , path::AbstractString = "qrcode.png" + ; targetsize::Int = 0 + , pixels::Int = 160) + matrix = qrcode(code) + exportbitmat(matrix, path; targetsize=targetsize, pixels=pixels) +end + +""" + exportqrcode( codes::AbstractVector{QRCode} + , path::AbstractString = "qrcode.gif" + ; pixels::Int = 160 + , fps::Int = 2) + +Create an animated gif with `codes` of approximate size `targetsize`. + +The frame rate `fps` is the number of frames per second. + +Note: The `codes` should have the same size while the other properties can be different. +""" +function exportqrcode( codes::AbstractVector{QRCode} + , path::AbstractString = "qrcode.gif" + ; targetsize::Int = 0 + , pixels::Int = 160 + , fps::Int = 2) + matwidth = qrwidth(first(codes)) + all(==(matwidth), qrwidth.(codes)) || throw(EncodeError("The codes should have the same size")) + # check whether the image format is supported + if !endswith(path, r"\.\w+") + path *= ".gif" + else + ext = last(split(path, '.')) + ext == "gif" || throw(EncodeError( + "$ext\n is not a valid format for animated images")) + end + # generate frames + if targetsize > 0 # original keyword -- will be removed in the future + Base.depwarn("keyword `targetsize` will be removed in the future, use `pixels` instead", :exportbitmat) + pixels = ceil(Int, 72 * targetsize / 2.45 / matwidth) * matwidth + else + pixels = ceil(Int, pixels / matwidth) * matwidth + end + code = Array{Bool}(undef, pixels, pixels, length(codes)) + for (i, c) in enumerate(codes) + code[:,:,i] = _resize(qrcode(c), pixels) + end + save(path, .! code, fps=fps) +end + +""" + exportqrcode( msgs::AbstractVector{<:AbstractString} + , path::AbstractString = "qrcode.gif" + ; eclevel::ErrCorrLevel = Medium() + , version::Int = 0 + , mode::Mode = Numeric() + , mask::Int = -1 + , width::Int = 4 + , targetsize::Int = 5 + , pixels::Int = 160 + , fps::Int = 2) + +Create an animated gif with `msgs` of approximate size `pixels x pixels`. + +The frame rate `fps` is the number of frames per second. +""" +function exportqrcode( msgs::AbstractVector{<:AbstractString} + , path::AbstractString = "qrcode.gif" + ; eclevel::ErrCorrLevel = Medium() + , version::Int = 0 + , mode::Mode = Numeric() + , mask::Int = -1 + , width::Int = 4 + , targetsize::Int = 0 + , pixels::Int = 160 + , fps::Int = 2) + codes = QRCode.( msgs + ; eclevel=eclevel + , version=version + , mode=mode + , mask=mask + , width=width) + # the image should have the same size + ## method 1: edit version + version = maximum(getproperty.(codes, :version)) + setproperty!.(codes, :version, version) + ## method 2: enlarge width of the small ones + # maxwidth = first(maximum(qrwidth.(codes))) + # for code in codes + # code.border += (maxwidth - qrwidth(code)) ÷ 2 + # end + # setproperty!.(codes, :width, width) + exportqrcode(codes, path; targetsize=targetsize, pixels=pixels, fps=fps) +end \ No newline at end of file diff --git a/src/errorcorrection.jl b/src/polynomial.jl similarity index 100% rename from src/errorcorrection.jl rename to src/polynomial.jl diff --git a/src/kanji.jl b/src/preprocess/kanji.jl similarity index 100% rename from src/kanji.jl rename to src/preprocess/kanji.jl diff --git a/src/matrix.jl b/src/preprocess/matrix.jl similarity index 100% rename from src/matrix.jl rename to src/preprocess/matrix.jl diff --git a/src/tables.jl b/src/preprocess/tables.jl similarity index 99% rename from src/tables.jl rename to src/preprocess/tables.jl index 012ac5f..df626d1 100644 --- a/src/tables.jl +++ b/src/preprocess/tables.jl @@ -1,5 +1,7 @@ # Data tables from the specificatioms +include("kanji.jl") + """ Allowed characters for `Alphanumeric()` mode and their number. """ @@ -8,7 +10,6 @@ const alphanumeric = Dict{AbstractChar, Int}( const antialphanumeric = Dict{Int, AbstractChar}(val => key for (key, val) in alphanumeric) -include("kanji.jl") const antikanji = Dict{Int, AbstractChar}(val => key for (key, val) in kanji) """ diff --git a/src/styles/locate.jl b/src/styles/locate.jl new file mode 100644 index 0000000..6a78d86 --- /dev/null +++ b/src/styles/locate.jl @@ -0,0 +1,88 @@ +# 2. Locate message bits +## 2.1 extract indexes of message bits + +""" + getindexes(v::Int) + +Extract indexes of message bits from the QR code of version `v`. + +The procedure is similar to `placedata!` in `matrix.jl`. +""" +function getindexes(v::Int) + mat, n = emptymatrix(v), 17 + 4 * v + inds = Vector{Int}(undef, msgbitslen[v]) + col, row, ind = n, n + 1, 1 + while col > 0 + # Skip the column with the timing pattern + if col == 7 + col -= 1 + continue + end + # path goes up and down + row, δrow = row > n ? (n, -1) : (1, 1) + # recode index if the matrix element is nothing + for _ in 1:n + if isnothing(mat[row, col]) + inds[ind] = (col - 1) * n + row + ind += 1 + end + if isnothing(mat[row, col - 1]) + inds[ind] = (col - 2) * n + row + ind += 1 + end + row += δrow + end + # move to the next column + col -= 2 + end + ind == msgbitslen[v] + 1 || throw(ArgumentError( + "The number of indexes is not correct.")) + return inds +end + +## 2.2 split indexes into several segments(de-interleave) + +""" + getsegments(v::Int, mode::Mode, eclevel::ErrCorrLevel) + +Get indexes segments of the corresponding settings. +Each of the segments has atmost 8 * 255 elements. + +The procedure is similar to `deinterleave` in `QRDecoders.jl`. +""" +function getsegments(v::Int, eclevel::ErrCorrLevel) + # initialize + ## get information about error correction + necwords, nb1, nc1, nb2, nc2 = ecblockinfo[eclevel][v, :] + ## initialize blocks + expand(x) = (8 * x - 7):8 * x + segments = vcat([Vector{Int}(undef, 8 * nc1) for _ in 1:nb1], + [Vector{Int}(undef, 8 * nc2) for _ in 1:nb2]) + ecsegments = [Vector{Int}(undef, 8 * necwords) for _ in 1:nb1 + nb2] + # get segments from the QR code + ## indexes of message bits + inds = getindexes(v) + ## discard remainder bits + inds = @view inds[1:end-remainderbits[v]] + length(inds) & 7 == 0 || throw(ArgumentError( + "The number of indexes is not correct.")) + + ## get blocks + ind = length(inds) >> 3 # number of bytes + ### error correction bytes + @inbounds for i in necwords:-1:1, j in (nb1 + nb2):-1:1 + ecsegments[j][expand(i)] = @view(inds[expand(ind)]) + ind -= 1 + end + ### message bytes + @inbounds for i in nc2:-1:(1 + nc1), j in (nb1 + nb2):-1:(nb1 + 1) + segments[j][expand(i)] = @view(inds[expand(ind)]) + ind -= 1 + end + @inbounds for i in nc1:-1:1, j in (nb1 + nb2):-1:1 + segments[j][expand(i)] = @view(inds[expand(ind)]) + ind -= 1 + end + ind != 0 && throw(ArgumentError("getsegments: not all data is recorded")) + return segments, ecsegments +end \ No newline at end of file diff --git a/src/style.jl b/src/styles/plotimage.jl similarity index 54% rename from src/style.jl rename to src/styles/plotimage.jl index 3f5c3e8..d94ff6a 100644 --- a/src/style.jl +++ b/src/styles/plotimage.jl @@ -1,167 +1,5 @@ -#= -# special QR codes -supported list: - 1. Unicode plot - I. Unicode plot by UnicodePlots.jl - II. Unicode plot by Unicode characters - 2. locate message bits - 2.1 extract indexes of message bits - 2.2 split indexes into several segments(de-interleave) - 3. plot image inside QR code - I. use error correction - II. use pad bits - III. use pad bits and error correction -=# - -# 1. Unicode plot -""" - unicodeplot(mat::AbstractMatrix{Bool}; border=:none) - -Uses UnicodePlots.jl to draw the matrix. - -Note: In UnicodePlots.jl, matrix index start from the left-down corner. -""" -function unicodeplot(mat::AbstractMatrix{Bool}; border=:none) - width, height = size(mat) - return heatmap(@view(mat[end:-1:1,:]); - labels=false, - border=border, - colormap=:gray, - width=width, - height=height) -end - -""" - unicodeplot(message::AbstractString - ; border=:none) - -Uses UnicodePlots.jl to draw the QR code of `message`. -""" -function unicodeplot(message::AbstractString; border=:none) - unicodeplot(qrcode(message;eclevel=Low(), width=2); border=border) -end - -## idea by @notinaboat -const pixelchars = ['█', '▀', '▄', ' '] -pixelchar(block::AbstractVector) = pixelchars[2 * block[1] + block[2] + 1] -pixelchar(bit::Bool) = bit ? pixelchars[4] : pixelchars[2] - -""" - unicodeplotbychar(mat::AbstractMatrix) - -Plot of the QR code using Unicode characters. - -The value `1(true)` represents a dark space and `0(false)` -a white square. It is the same convention as QR code and -is the opposite of general image settings. -""" -function unicodeplotbychar(mat::AbstractMatrix) - m = size(mat, 1) - txt = @views join((join(pixelchar.(eachcol(mat[i:i+1, :]))) for i in 1:2:m & 1 ⊻ m), '\n') - isodd(m) || return txt - return @views txt * '\n' * join(pixelchar.(mat[m, :])) -end - -""" - unicodeplotbychar(message::AbstractString) - -Plot of the QR code using Unicode characters. -""" -function unicodeplotbychar(message::AbstractString) - unicodeplotbychar(qrcode(message; eclevel=Low(), width=2)) -end - -# 2. locate message bits -## 2.1 extract indexes of message bits - -""" - getindexes(v::Int) - -Extract indexes of message bits from the QR code of version `v`. - -The procedure is similar to `placedata!` in `matrix.jl`. -""" -function getindexes(v::Int) - mat, n = emptymatrix(v), 17 + 4 * v - inds = Vector{Int}(undef, msgbitslen[v]) - col, row, ind = n, n + 1, 1 - while col > 0 - # Skip the column with the timing pattern - if col == 7 - col -= 1 - continue - end - # path goes up and down - row, δrow = row > n ? (n, -1) : (1, 1) - # recode index if the matrix element is nothing - for _ in 1:n - if isnothing(mat[row, col]) - inds[ind] = (col - 1) * n + row - ind += 1 - end - if isnothing(mat[row, col - 1]) - inds[ind] = (col - 2) * n + row - ind += 1 - end - row += δrow - end - # move to the next column - col -= 2 - end - ind == msgbitslen[v] + 1 || throw(ArgumentError( - "The number of indexes is not correct.")) - return inds -end - -## 2.2 split indexes into several segments(de-interleave) - -""" - getsegments(v::Int, mode::Mode, eclevel::ErrCorrLevel) - -Get indexes segments of the corresponding settings. -Each of the segments has atmost 8 * 255 elements. - -The procedure is similar to `deinterleave` in `QRDecoders.jl`. -""" -function getsegments(v::Int, eclevel::ErrCorrLevel) - # initialize - ## get information about error correction - necwords, nb1, nc1, nb2, nc2 = ecblockinfo[eclevel][v, :] - ## initialize blocks - expand(x) = (8 * x - 7):8 * x - segments = vcat([Vector{Int}(undef, 8 * nc1) for _ in 1:nb1], - [Vector{Int}(undef, 8 * nc2) for _ in 1:nb2]) - ecsegments = [Vector{Int}(undef, 8 * necwords) for _ in 1:nb1 + nb2] - # get segments from the QR code - ## indexes of message bits - inds = getindexes(v) - ## discard remainder bits - inds = @view inds[1:end-remainderbits[v]] - length(inds) & 7 == 0 || throw(ArgumentError( - "The number of indexes is not correct.")) - - ## get blocks - ind = length(inds) >> 3 # number of bytes - ### error correction bytes - @inbounds for i in necwords:-1:1, j in (nb1 + nb2):-1:1 - ecsegments[j][expand(i)] = @view(inds[expand(ind)]) - ind -= 1 - end - ### message bytes - @inbounds for i in nc2:-1:(1 + nc1), j in (nb1 + nb2):-1:(nb1 + 1) - segments[j][expand(i)] = @view(inds[expand(ind)]) - ind -= 1 - end - @inbounds for i in nc1:-1:1, j in (nb1 + nb2):-1:1 - segments[j][expand(i)] = @view(inds[expand(ind)]) - ind -= 1 - end - ind != 0 && throw(ArgumentError("getsegments: not all data is recorded")) - return segments, ecsegments -end - -## 3. plot image inside QR code -### I. use error correction +# 3. Plot image inside QR code +## 3.1 use error correction """ pickindexes(errinds::AbstractVector{<:Integer}, blockinds{<:Integer}, modify::Int) diff --git a/src/styles/style.jl b/src/styles/style.jl new file mode 100644 index 0000000..d7db513 --- /dev/null +++ b/src/styles/style.jl @@ -0,0 +1,23 @@ +#= +# special QR codes +supported list: + 1. Unicode plot + 1.1 Unicode plot by UnicodePlots.jl + 1.2 Unicode plot by Unicode characters + 2. locate message bits + 2.1 extract indexes of message bits + 2.2 split indexes into several segments(de-interleave) + 3. plot image inside QR code + 3.1 use error correction + 3.2 use pad bits + 3.3 use pad bits and error correction +=# + +# Unicode plot +include("unicodeplot.jl") + +# locate message bits +include("locate.jl") + +# Plot image inside QR code +include("plotimage.jl") diff --git a/src/styles/unicodeplot.jl b/src/styles/unicodeplot.jl new file mode 100644 index 0000000..e886b63 --- /dev/null +++ b/src/styles/unicodeplot.jl @@ -0,0 +1,58 @@ +# 1. Unicode plot +## 1.1 by UnicodePlots.jl +""" + unicodeplot(mat::AbstractMatrix{Bool}; border=:none) + +Uses UnicodePlots.jl to draw the matrix. + +Note: In UnicodePlots.jl, matrix index start from the left-down corner. +""" +function unicodeplot(mat::AbstractMatrix{Bool}; border=:none) + width, height = size(mat) + return heatmap(@view(mat[end:-1:1,:]); + labels=false, + border=border, + colormap=:gray, + width=width, + height=height) +end + +""" + unicodeplot(message::AbstractString + ; border=:none) + +Uses UnicodePlots.jl to draw the QR code of `message`. +""" +function unicodeplot(message::AbstractString; border=:none) + unicodeplot(qrcode(message;eclevel=Low(), width=2); border=border) +end + +## 1.2 Idea by @notinaboat +const pixelchars = ['█', '▀', '▄', ' '] +pixelchar(block::AbstractVector) = pixelchars[2 * block[1] + block[2] + 1] +pixelchar(bit::Bool) = bit ? pixelchars[4] : pixelchars[2] + +""" + unicodeplotbychar(mat::AbstractMatrix) + +Plot of the QR code using Unicode characters. + +The value `1(true)` represents a dark space and `0(false)` +a white square. It is the same convention as QR code and +is the opposite of general image settings. +""" +function unicodeplotbychar(mat::AbstractMatrix) + m = size(mat, 1) + txt = @views join((join(pixelchar.(eachcol(mat[i:i+1, :]))) for i in 1:2:m & 1 ⊻ m), '\n') + isodd(m) || return txt + return @views txt * '\n' * join(pixelchar.(mat[m, :])) +end + +""" + unicodeplotbychar(message::AbstractString) + +Plot of the QR code using Unicode characters. +""" +function unicodeplotbychar(message::AbstractString) + unicodeplotbychar(qrcode(message; eclevel=Low(), width=2)) +end \ No newline at end of file diff --git a/src/types.jl b/src/types.jl new file mode 100644 index 0000000..c95b185 --- /dev/null +++ b/src/types.jl @@ -0,0 +1,159 @@ +# Data types in QRCoders + +""" +Invalid step in encoding process. +""" +struct EncodeError <: Exception + st::AbstractString +end + +# Encoding mode of the QR code +""" +Abstract type that groups the five supported encoding modes `Numeric`, +`Alphanumeric`, `Byte`, `Kanji` and `UTF8`. +""" +abstract type Mode end +""" +Encoding mode for messages composed of digits only. +""" +struct Numeric <: Mode end +""" +Encoding mode for messages composed of digits, characters `A`-`Z` (capital only) +, space and `%` `*` `+` `-` `.` `/` `:` `\$`. +""" +struct Alphanumeric <: Mode end +""" +Encoding mode for messages composed of one-byte characters(unicode range from +0x00 to 0xff, including ISO-8859-1 and undefined characters) +""" +struct Byte <: Mode end +""" +Encoding mode for messages composed of Shift JIS(Shift Japanese Industrial Standards) characters. +""" +struct Kanji <: Mode end +""" +Encoding mode for messages composed of utf-8 characters. +""" +struct UTF8 <: Mode end + +# relationships between the encoding modes +import Base: issubset +""" + issubset(mode1::Mode, mode2::Mode) + ⊆(mode1::Mode, mode2::Mode) + +Returns `true` if the character set of `mode1` is a subset of the character set of `mode2`. +""" +issubset(::Mode, ::UTF8) = true +issubset(mode::Mode, ::Numeric) = mode == Numeric() +issubset(mode::Mode, ::Alphanumeric) = (mode == Alphanumeric() || mode == Numeric()) +issubset(mode::Mode, ::Byte) = (mode != UTF8() && mode != Kanji()) +issubset(mode::Mode, ::Kanji) = mode == Kanji() + +# Error correction level of the QR code +""" +Abstract type that groups the four error correction levels `Low`, `Medium`, +`Quartile` and `High`. +""" +abstract type ErrCorrLevel end +""" +Error correction level that can restore up to 7% of missing codewords. +""" +struct Low <: ErrCorrLevel end +""" +Error correction level that can restore up to 15% of missing codewords. +""" +struct Medium <: ErrCorrLevel end +""" +Error correction level that can restore up to 25% of missing codewords. +""" +struct Quartile <: ErrCorrLevel end +""" +Error correction level that can restore up to 30% of missing codewords. +""" +struct High <: ErrCorrLevel end + +# structure type of the QR code +""" + QRCode + +A type that represents a QR code. + +# Fields +- `version::Int`: version of the QR code +- `mode::Mode`: encoding mode of the QR code +- `eclevel::ErrCorrLevel`: error correction level of the QR code +- `mask::Int`: mask pattern of the QR code +- `message::String`: message to be encoded +- `border::Int`: width of the white border +""" +mutable struct QRCode + version::Int # version of the QR code + mode::Mode # encoding mode + eclevel::ErrCorrLevel + mask::Int # mask pattern + message::String # message to be encoded + border::Int # width of the white border +end + +""" + copy(code::QRCode) + +Returns a copy of `code`. +""" +function Base.copy(code::QRCode) + QRCode(code.version, code.mode, code.eclevel, + code.mask, code.message, code.border) +end + + +""" + QRCode( message::AbstractString + ; mask::Int = -1 + , eclevel::ErrCorrLevel = Medium() + , mode::Mode = Numeric() + , version::Int = 0 + , width::Int = 1) + +Create a QR code with the default settings. +""" +function QRCode( message::AbstractString + ; mask::Int = -1 + , eclevel::ErrCorrLevel = Medium() + , mode::Mode = Numeric() + , version::Int = 0 + , width::Int = 1) + minmode = getmode(message) + mode = issubset(minmode, mode) ? mode : minmode + minversion = getversion(message, mode, eclevel) + version = version ≥ minversion ? version : minversion + # valid mask pattern (0-7) + 0 ≤ mask ≤ 7 && return QRCode(version, mode, eclevel, mask, message, width) + + # find the best mask + data = encodemessage(message, mode, eclevel, version) + matrix = emptymatrix(version) + masks = makemasks(matrix) + matrix = placedata!(matrix, data) # fill in data bits + addversion!(matrix, version) # fill in version bits + # Apply mask and add format information + maskedmats = [addformat!(xor.(matrix, mat), i-1, eclevel) + for (i, mat) in enumerate(masks)] + # Pick the best mask + mask = argmin(penalty.(maskedmats)) - 1 + QRCode(version, mode, eclevel, mask, message, width) +end + +""" + qrwidth(code::QRCode) + +Return the width of the QR code. +""" +qrwidth(code::QRCode) = 4 * code.version + 17 + 2 * code.border + +""" + show(io::IO, code::QRCode) + +Show the QR code in REPL using `unicodeplotbychar`. +""" +Base.show(io::IO, code::QRCode) = print(io, unicodeplotbychar(qrcode(code))) \ No newline at end of file