texlive[73916] trunk: extractbb, now also ebb

commits+karl at tug.org commits+karl at tug.org
Wed Feb 12 15:58:30 CET 2025


Revision: 73916
          https://tug.org/svn/texlive?view=revision&revision=73916
Author:   karl
Date:     2025-02-12 15:58:30 +0100 (Wed, 12 Feb 2025)
Log Message:
-----------
extractbb, now also ebb

Modified Paths:
--------------
    trunk/Build/source/texk/texlive/linked_scripts/Makefile.am
    trunk/Build/source/texk/texlive/linked_scripts/Makefile.in
    trunk/Build/source/texk/texlive/linked_scripts/extractbb/extractbb.lua
    trunk/Master/texmf-dist/doc/man/man1/extractbb.1
    trunk/Master/texmf-dist/doc/man/man1/extractbb.man1.pdf
    trunk/Master/texmf-dist/doc/support/extractbb/README.md
    trunk/Master/texmf-dist/doc/support/extractbb/extractbb.man1.pdf
    trunk/Master/texmf-dist/scripts/extractbb/extractbb.lua
    trunk/Master/tlpkg/libexec/ctan2tds

Added Paths:
-----------
    trunk/Master/bin/aarch64-linux/ebb
    trunk/Master/bin/amd64-freebsd/ebb
    trunk/Master/bin/amd64-netbsd/ebb
    trunk/Master/bin/armhf-linux/ebb
    trunk/Master/bin/i386-freebsd/ebb
    trunk/Master/bin/i386-linux/ebb
    trunk/Master/bin/i386-netbsd/ebb
    trunk/Master/bin/i386-solaris/ebb
    trunk/Master/bin/universal-darwin/ebb
    trunk/Master/bin/x86_64-cygwin/ebb
    trunk/Master/bin/x86_64-darwinlegacy/ebb
    trunk/Master/bin/x86_64-linux/ebb
    trunk/Master/bin/x86_64-linuxmusl/ebb
    trunk/Master/bin/x86_64-solaris/ebb

Removed Paths:
-------------
    trunk/Master/bin/aarch64-linux/ebb
    trunk/Master/bin/amd64-freebsd/ebb
    trunk/Master/bin/amd64-netbsd/ebb
    trunk/Master/bin/armhf-linux/ebb
    trunk/Master/bin/i386-freebsd/ebb
    trunk/Master/bin/i386-linux/ebb
    trunk/Master/bin/i386-netbsd/ebb
    trunk/Master/bin/i386-solaris/ebb
    trunk/Master/bin/universal-darwin/ebb
    trunk/Master/bin/windows/ebb.exe
    trunk/Master/bin/x86_64-cygwin/ebb
    trunk/Master/bin/x86_64-darwinlegacy/ebb
    trunk/Master/bin/x86_64-linux/ebb
    trunk/Master/bin/x86_64-linuxmusl/ebb
    trunk/Master/bin/x86_64-solaris/ebb
    trunk/Master/texmf-dist/scripts/extractbb/extractbb-scratch.lua
    trunk/Master/texmf-dist/scripts/extractbb/extractbb-wrapper.lua

Modified: trunk/Build/source/texk/texlive/linked_scripts/Makefile.am
===================================================================
--- trunk/Build/source/texk/texlive/linked_scripts/Makefile.am	2025-02-12 14:09:34 UTC (rev 73915)
+++ trunk/Build/source/texk/texlive/linked_scripts/Makefile.am	2025-02-12 14:58:30 UTC (rev 73916)
@@ -284,6 +284,7 @@
 	cluttex:clxelatex \
 	cluttex:cllualatex \
 	epstopdf:repstopdf \
+	extractbb:ebb \
 	fmtutil:mktexfmt \
 	kpsetool:kpsexpand \
 	kpsetool:kpsepath \

Modified: trunk/Build/source/texk/texlive/linked_scripts/Makefile.in
===================================================================
--- trunk/Build/source/texk/texlive/linked_scripts/Makefile.in	2025-02-12 14:09:34 UTC (rev 73915)
+++ trunk/Build/source/texk/texlive/linked_scripts/Makefile.in	2025-02-12 14:58:30 UTC (rev 73916)
@@ -503,6 +503,7 @@
 	cluttex:clxelatex \
 	cluttex:cllualatex \
 	epstopdf:repstopdf \
+	extractbb:ebb \
 	fmtutil:mktexfmt \
 	kpsetool:kpsexpand \
 	kpsetool:kpsepath \

Modified: trunk/Build/source/texk/texlive/linked_scripts/extractbb/extractbb.lua
===================================================================
--- trunk/Build/source/texk/texlive/linked_scripts/extractbb/extractbb.lua	2025-02-12 14:09:34 UTC (rev 73915)
+++ trunk/Build/source/texk/texlive/linked_scripts/extractbb/extractbb.lua	2025-02-12 14:58:30 UTC (rev 73916)
@@ -2,86 +2,664 @@
 -- extractbb-lua
 -- https://github.com/gucci-on-fleek/extractbb
 -- SPDX-License-Identifier: MPL-2.0+
--- SPDX-FileCopyrightText: 2024 Max Chernoff
+-- SPDX-FileCopyrightText: 2024--2025 Max Chernoff
 --
--- A wrapper script to allow you to choose which implementation of extractbb to
--- use. Should hopefully be replaced with the ``scratch'' file in TeX Live 2025.
+-- Inclusion Methods
+-- =================
 --
--- v1.0.6 (2024-11-21) %%version %%dashdate
+-- This script can use two different methods to extract bounding boxes from
+-- images: the "img" module and the "pdfe" module. The "img" module will be
+-- automatically selected in most cases and supports all image types that are
+-- supported by the original "extractbb" program. If and only if the "img"
+-- module fails to load, the "pdfe" module will be used as a fallback. However,
+-- the "pdfe" module only supports PDF files. Both modules are built in to the
+-- LuaTeX binaries, however due to some technical issues, the "img" module may
+-- fail to load on some more exotic platforms.
+--
+--
+-- Compatibility
+-- =============
+--
+-- Based off of my testing, this Lua script is 100% compatible with the original
+-- C-based "extractbb" program, with the following exceptions:
+--
+--   * When running in "img" mode, the PDF version is always reported as "1.5".
+--
+--   * When running in "img" mode, if the requested bounding box is not found,
+--     the script will fallback to the Crop box or the Media box, instead of
+--     following the original fallback order. (In practice, almost all PDFs set
+--     all their bounding boxes equal to each other, and even if the boxes are
+--     set to different values, the script will still return the requested box,
+--     provided that it is set in the PDF.)
+--
+--   * When running in "pdfe" mode, only PDF files are supported.
+--
+-- All of these issues are very unlikely to affect any real-world documents.
+--
+--
+-- Security
+-- ========
+--
+-- This script is designed to be safely ran from restricted shell escape. A few
+-- security features:
+--
+--   * The majority of this script runs inside a sandboxed Lua environment,
+--     which only exposes a very restricted set of functions.
+--
+--   * All file-related functions available inside the sandbox first check with
+--     kpathsea to ensure that the file is allowed to be opened.
+--
+--   * In the event of any errors, the script immediately exits.
+--
+--   * This script does not run (fork/exec) any external programs.
+--
+--   * This script is written entirely in Lua, so overflow/use-after-free
+--     vulnerabilities are not possible.
+--
+-- Some potential security concerns:
+--
+--   * This script has not been audited or reviewed by anyone other than myself.
+--
+--   * The underlying LuaTeX modules may themselves have security
+--     vulnerabilities, which would be inherited by this script.
 
----------------------
---- Configuration ---
----------------------
--- Choose which implementation of extractbb to use.
-local DEFAULT = "wrapper"
 
+----------------------
+--- Initialization ---
+----------------------
 
------------------
---- Execution ---
------------------
+-- Pre-sandbox variables/constants
+local show_errors = true
+local SOURCE_DATE_EPOCH = tonumber(os.getenv("SOURCE_DATE_EPOCH"))
+local version = "extractbb.lua v1.1.0 (2025-02-11)" --%%version %%dashdate
 
--- Send the error messages to stderr.
-local function error(...)
-    -- Header
-    io.stderr:write("! extractbb ERROR: ")
+-- Required for any kpathsea calls to work.
+kpse.set_program_name("texlua", "extractbb")
 
-    -- Message
-    for i = 1, select("#", ...) do
-        io.stderr:write(tostring(select(i, ...)), " ")
+-- Required to use the "img" module from texlua, but only works for LuaTeX
+-- versions >= 1.21.0.
+if not (status.development_id >= 7661) then
+    error("LuaTeX version is too old, cannot proceed.")
+end
+texconfig.texlua_img = true
+
+-- We need to set \outputmode to PDF to be able to use most of the "img" module
+-- functions, but to set \outputmode, we need to initialize the TeX interpreter.
+tex.initialize()
+_G.tex = package.loaded.tex
+tex.enableprimitives("", tex.extraprimitives())
+tex.outputmode = 1
+tex.interactionmode = 0
+
+-- "pdf" module
+_G.pdf = package.loaded.pdf
+pdf.setignoreunknownimages(1)
+pdf.setmajorversion(2)
+pdf.setminorversion(0)
+
+
+------------------
+--- Sandboxing ---
+------------------
+
+-- Prepare the sandbox for the rest of the script.
+local env = {
+    arg      = arg,
+    io       = { stdout = io.stdout, },
+    ipairs   = ipairs,
+    math     = math,
+    os       = { date = os.date, exit = os.exit, },
+    pairs    = pairs,
+    pdfe     = pdfe,
+    print    = print,
+    select   = select,
+    table    = table,
+    tonumber = tonumber,
+    type     = type,
+}
+
+do
+    -- Saved global functions
+    local debug_traceback  = debug.traceback
+    local find_file        = kpse.find_file
+    local img_scan         = img.scan
+    local io_open          = io.open
+    local io_stderr        = io.stderr
+    local kpse_in_name_ok  = kpse.in_name_ok
+    local kpse_out_name_ok = kpse.out_name_ok
+    local kpse_var_value   = kpse.var_value
+    local lfs_attributes   = lfs.attributes
+    local os_exit          = os.exit
+    local os_setenv        = os.setenv
+    local pdfe_open        = pdfe.open
+    local select           = select
+    local tostring         = tostring
+
+    -- Error messages
+    local function error(...)
+        if show_errors then
+            -- Header
+            io_stderr:write("! extractbb ERROR: ")
+
+            -- Message
+            for i = 1, select("#", ...) do
+                io_stderr:write(tostring(select(i, ...)), " ")
+            end
+
+            -- Traceback
+            io_stderr:write("\n", "\n")
+            io_stderr:write(debug_traceback(nil, 2), "\n")
+        end
+
+        -- Flush and exit
+        io_stderr:flush()
+        os_exit(1)
     end
 
-    -- Flush and exit
-    io.stderr:write("\n")
-    io.stderr:flush()
-    os.exit(1)
+    env.error = error
+
+    -- Make sure that "openin_any" is at least "restricted", and that
+    -- "openout_any" is at least "paranoid".
+    local initial_openin  = kpse_var_value("openin_any")
+    local initial_openout = kpse_var_value("openout_any")
+
+    if (initial_openin ~= "r") or (initial_openout ~= "p") then
+        os_setenv("openin_any",  "r")
+    end
+
+    if (initial_openout ~= "p") then
+        os_setenv("openout_any", "p")
+    end
+
+    -- Check the input paths.
+    local function resolve_input_name(file_name)
+        local file_path = find_file(file_name, "graphic/figure", true)
+        if not file_path then
+            error("Cannot find input file:", file_name)
+        end
+
+        local allowed = kpse_in_name_ok(file_path)
+        if not allowed then
+            error("Input file is not allowed:", file_path)
+        end
+
+        local mode = lfs_attributes(file_path, "mode")
+        if mode ~= "file" then
+            error("Input file is not a regular file:", file_path)
+        end
+
+        return file_path
+    end
+
+    -- Check the output paths.
+    local function resolve_output_name(file_name)
+        local allowed = kpse_out_name_ok(file_name)
+        if not allowed then
+            error("Output file is not allowed:", file_name)
+        end
+
+        local name, extension = file_name:match("(.+)%.([^.]-)$")
+
+        if (not name) or (not extension) or
+           (name == "") or (extension == "")
+        then
+            error("Output file has no extension:", file_name)
+        end
+
+        if (extension ~= "xbb") and (extension ~= "bb") then
+            error("Output file has an invalid extension:", file_name)
+        end
+
+        -- We shouldn't allow files with weird characters in their names.
+        if name:match("[%c%%\t\r\n><*|]") then
+            error("Output file has an invalid name:", file_name)
+        end
+
+        return file_name
+    end
+
+    -- Opens a file.
+    function env.open_file(file_name, read_write, binary_text)
+        local file_path, mode
+        if read_write == "read" then
+            file_path = resolve_input_name(file_name)
+            mode = "r"
+        elseif read_write == "write" then
+            file_path = resolve_output_name(file_name)
+            mode = "w"
+        else
+            error("Invalid read/write mode:", read_write)
+        end
+
+        if binary_text == "binary" then
+            mode = mode .. "b"
+        elseif binary_text == "text" then
+            mode = mode .. ""
+        else
+            error("Invalid binary/text mode:", binary_text)
+        end
+
+        local file, message = io_open(file_path, mode)
+
+        if not file then
+            error("Cannot open file:", file_path, message)
+        end
+
+        return file
+    end
+
+    -- Open an PDF file.
+    function env.pdfe.open(file_name)
+        local file_path = resolve_input_name(file_name)
+        return pdfe_open(file_path)
+    end
+
+    -- Open an image file.
+    function env.open_image(file_name, page, box)
+        local file_path = resolve_input_name(file_name)
+        return img_scan {
+            filename = file_path,
+            filepath = file_path,
+            page     = page,
+            pagebox  = box,
+        }
+    end
+
+    if not img_scan then
+        env.open_image = false
+    end
 end
 
--- Get the value of the environment variable that decides which version to run.
-local env_choice = os.env["TEXLIVE_EXTRACTBB"]
+-- Prevent trying to change the environment.
+local function bad_index(...)
+    env.error("Attempt to access an undefined index:", select(2, ...))
+end
 
--- If the environment variable is set to a file path, run that directly.
-local env_mode = lfs.attributes(env_choice or "", "mode")
-if (env_mode == "file") or (env_mode == "link") then
-    arg[0] = env_choice
-    table.insert(arg, 1, env_choice)
-    arg[-1] = nil
-    return os.exec(arg)
+setmetatable(env, {
+    __index     = bad_index,
+    __metatable = false,
+    __newindex  = bad_index,
+})
+
+-- Set the environment.
+_ENV = env
+
+
+-----------------------------------
+--- Post-Sandbox Initialization ---
+-----------------------------------
+
+-- Constants
+local BP_TO_SP    = 65781.76
+local IN_TO_BP    = 72
+local DATE_FORMAT = "%a %b %d %H:%M:%S %Y" -- "%c"
+
+-- Save often-used globals for a slight speed boost.
+local floor            = math.floor
+local insert           = table.insert
+local remove           = table.remove
+local script_arguments = arg
+local unpack           = table.unpack
+
+-- General-purpose functions
+local function round(number)
+    return floor(number +0.5)
 end
 
--- Find the subscripts
-kpse.set_program_name("texlua", "extractbb")
 
-local function find_script(name)
-    -- Find the script, searching **only** in the scripts directories.
-    local path = kpse.lookup(
-        name,
-        { path = kpse.var_value("TEXMFSCRIPTS"), format = "lua" }
-    )
+-------------------------
+--- Argument Handling ---
+-------------------------
 
-    -- Make sure that the script is not writable.
-    if kpse.out_name_ok_silent_extended(path) then
-        if os.env["TEXLIVE_EXTRACTBB_UNSAFE"] == "unsafe" then
-            -- If we're running in development mode, then we can allow this.
-        else
-            error("Refusing to run a writable script.")
+-- Define the argument handling functions.
+local process_arguments = {}
+
+-- > Specify a PDF pagebox for bounding box
+-- > pagebox=cropbox, mediabox, artbox, trimbox, bleedbox
+local bbox_option = "auto"
+function process_arguments.B(script_arguments)
+    bbox_option = remove(script_arguments, 1)
+end
+
+-- > Show this help message and exit
+function process_arguments.h(script_arguments)
+    print [[
+Usage: extractbb [-B pagebox] [-p page] [-q|-v] [-O] [-m|-x] FILE...
+       extractbb --help|--version
+Extract bounding box from PDF, PNG, JPEG, JP2, or BMP file; default output below.
+
+Options:
+  -B pagebox    Specify a PDF pagebox for bounding box
+                pagebox=cropbox, mediabox, artbox, trimbox, bleedbox
+  -h | --help   Show this help message and exit
+  --version     Output version information and exit
+  -p page       Specify a PDF page to extract bounding box
+  -q            Be quiet
+  -v            Be verbose
+  -O            Write output to stdout
+  -m            Output .bb  file used in DVIPDFM (default)
+  -x            Output .xbb file used in DVIPDFMx
+]]
+    os.exit(0)
+end
+
+process_arguments["-help"] = process_arguments.h
+
+-- > Output version information and exit
+function process_arguments.V(script_arguments)
+    print(version)
+    os.exit(0)
+end
+
+process_arguments["-version"] = process_arguments.V
+
+-- > Specify a PDF page to extract bounding box
+local page_number = 1
+function process_arguments.p(script_arguments)
+    page_number = tonumber(remove(script_arguments, 1))
+end
+
+-- > Be quiet
+function process_arguments.q(script_arguments)
+    show_errors = false
+end
+
+-- > Be verbose
+function process_arguments.v(script_arguments)
+    show_errors = true
+end
+
+-- > Write output to stdout
+local output_file
+function process_arguments.O(script_arguments)
+    output_file = io.stdout
+end
+
+-- Output format
+local output_format = "xbb"
+
+if script_arguments[0]:match("ebb") then
+    output_format = "bb"
+end
+
+-- > Output .bb  file used in DVIPDFM (default)
+function process_arguments.m(script_arguments)
+    output_format = "bb"
+end
+
+-- > Output .xbb file used in DVIPDFMx
+function process_arguments.x(script_arguments)
+    output_format = "xbb"
+end
+
+-- Get the input file name.
+local input_name
+function process_arguments.i(script_arguments)
+    input_name = remove(script_arguments, 1)
+end
+
+process_arguments["-input-name"] = process_arguments.i
+
+-- Clear the interpreter and script names.
+script_arguments[-1] = nil
+script_arguments[0]  = nil
+
+-- Process the arguments.
+while script_arguments[1] do
+    -- Get the next argument.
+    local arg = remove(script_arguments, 1)
+    local cmd = arg:match("^%-(.*)$")
+
+    -- Default to "--input-name" if no command is given.
+    if not cmd then
+        insert(script_arguments, 1, arg)
+        cmd = "-input-name"
+    end
+
+    -- Handle multi-character arguments.
+    if (cmd:len() >= 2) and (not cmd:match("^%-")) then
+        local i = 0
+        for char in cmd:gmatch(".") do
+            i = i + 1
+            insert(script_arguments, i, "-" .. char)
         end
+
+        goto continue
     end
 
-    return path
+    -- Get the function to process the argument and run it.
+    local func = process_arguments[cmd]
+
+    if not func then
+        error("Invalid argument:", arg)
+    end
+
+    func(script_arguments)
+
+    ::continue::
 end
 
--- Map the choice names to file names.
-local choice_mapping = {
-    wrapper = find_script("extractbb-wrapper.lua"),
-    scratch = find_script("extractbb-scratch.lua"),
+-- Validate the arguments.
+if not type(page_number) == "number" then
+    error("Invalid page number:", page_number)
+end
+
+if not input_name then
+    error("No input file specified.")
+end
+
+-- Validate the bounding box type. We need this rather crazy fallback scheme
+-- to match the behaviour of "extractbb".
+local bbox_orders = {}
+bbox_orders.mediabox = {
+    { img = "media", pdfe = "MediaBox" },
 }
+bbox_orders.cropbox = {
+    { img = "crop", pdfe = "CropBox" }, unpack(bbox_orders.mediabox)
+}
+bbox_orders.artbox = {
+    { img = "art", pdfe = "ArtBox" }, unpack(bbox_orders.cropbox)
+}
+bbox_orders.trimbox = {
+    { img = "trim", pdfe = "TrimBox" }, unpack(bbox_orders.artbox)
+}
+bbox_orders.bleedbox = {
+    { img = "bleed", pdfe = "BleedBox" }, unpack(bbox_orders.trimbox)
+}
+bbox_orders.auto = {
+    bbox_orders.cropbox[1], bbox_orders.artbox[1], bbox_orders.trimbox[1],
+    bbox_orders.bleedbox[1], bbox_orders.mediabox[1],
+}
 
--- Choose the implementation to run.
-local choice = choice_mapping[env_choice] or choice_mapping[DEFAULT]
+local bbox_order = bbox_orders[bbox_option]
 
-if not choice then
-    error("No implementation of extractbb found.")
+if not bbox_order then
+    error("Invalid PDF box type:", bbox_option)
 end
 
--- And run it.
-dofile(choice)
+-- Set the default pixel resolution.
+local default_dpi
+if output_format == "xbb" then
+    default_dpi = 72
+elseif output_format == "bb" then
+    default_dpi = 100
+else
+    error("Invalid output format:", output_format)
+end
+
+-- Open the output file.
+if not output_file then
+    local base_name   = input_name:match("(.+)%.([^.]-)$") or input_name
+    local output_name = base_name .. "." .. output_format
+    output_file = open_file(output_name, "write", "text")
+end
+
+
+------------------------
+--- Image Processing ---
+------------------------
+
+local x_min, y_min, x_max, y_max
+local num_pages, image_type
+local pdf_major_version, pdf_minor_version
+
+if open_image then
+    -- Check the number of pages.
+    local image = open_image(input_name)
+    num_pages = image.pages
+
+    if page_number > num_pages then
+        error("Invalid page number:", page_number)
+    end
+
+    -- Open the image to the specified page and bounding box. If the requested
+    -- bounding box is not available, LuaTeX will fall back to the crop box
+    -- or the media box.
+    image = open_image(input_name, page_number, bbox_order[1].img)
+
+    if not image then
+        error("Cannot open image:", input_name)
+    end
+
+    -- Get the image metadata.
+    image_type   = image.imagetype
+    local bounding_box = image.bbox
+
+    if not bounding_box then
+        error("Cannot get bounding box:", page_number)
+    end
+
+    local x_resolution = image.xres
+    local y_resolution = image.yres
+
+    if (x_resolution or 0) == 0 then
+        x_resolution = default_dpi
+    end
+
+    if (y_resolution or 0) == 0 then
+        y_resolution = default_dpi
+    end
+
+    -- Convert the bounding box to PostScript points.
+    for i, dimen in ipairs(bounding_box) do
+        if image_type == "pdf" then
+            dimen = dimen / BP_TO_SP
+        else
+            if i % 2 == 1 then
+                dimen = dimen / x_resolution * IN_TO_BP
+            else
+                dimen = dimen / y_resolution * IN_TO_BP
+            end
+        end
+
+        bounding_box[i] = dimen
+    end
+
+    -- Save the bounding box.
+    x_min, y_min, x_max, y_max = unpack(bounding_box)
+
+    -- We can't get the PDF version with the "img" library, so we'll just
+    -- pretend that it's v1.5 (which supports most features).
+    pdf_major_version = 1
+    pdf_minor_version = 5
+else
+    -- Fallback to PDFs only.
+    image_type = "pdf"
+    local document = pdfe.open(input_name)
+
+    if pdfe.getstatus(document) ~= 0 then
+        error("Cannot open PDF file:", input_name)
+    end
+
+    -- Check the number of pages.
+    num_pages = pdfe.getnofpages(document)
+
+    if type(num_pages) ~= "number" then
+        error("Invalid number of pages:", num_pages)
+    end
+
+    if page_number > num_pages then
+        error("Invalid page number:", page_number)
+    end
+
+    -- Get the page.
+    local page = pdfe.getpage(document, page_number)
+
+    if not page then
+        error("Cannot get page:", page_number)
+    end
+
+    -- Get the bounding box. Here, we check the boxes in the exact same order
+    -- that "extractbb" does.
+    local bounding_box
+    for _, bbox in ipairs(bbox_order) do
+        bounding_box = pdfe.getbox(page, bbox.pdfe)
+
+        if bounding_box then
+            break
+        end
+    end
+
+    if not bounding_box then
+        error("Cannot get bounding box:", page_number)
+    end
+
+    -- Save the bounding box.
+    x_min, y_min, x_max, y_max = unpack(bounding_box)
+
+    -- Get the PDF version.
+    pdf_major_version, pdf_minor_version = pdfe.getversion(document)
+end
+
+-- Validate the bounding box.
+for _, dimen in ipairs { x_min, y_min, x_max, y_max } do
+    if type(dimen) ~= "number" then
+        error("Invalid bounding box:", x_min, y_min, x_max, y_max)
+    end
+end
+
+
+--------------
+--- Output ---
+--------------
+
+-- Get the output fields and values.
+local lines = {}
+
+insert(lines, ("Title: %s"):format(input_name))
+insert(lines, ("Creator: %s"):format(version))
+insert(lines,
+       ("BoundingBox: %d %d %d %d")
+       :format(round(x_min), round(y_min), round(x_max), round(y_max)))
+
+if output_format == "xbb" then
+    insert(lines,
+           ("HiResBoundingBox: %0.6f %0.6f %0.6f %0.6f")
+           :format(x_min, y_min, x_max, y_max))
+
+    if image_type == "pdf" then
+        insert(lines,
+               ("PDFVersion: %d.%d")
+               :format(pdf_major_version, pdf_minor_version))
+
+        insert(lines, ("Pages: %d"):format(num_pages))
+    end
+
+end
+
+insert(lines, ("CreationDate: %s"):format(os.date(DATE_FORMAT, SOURCE_DATE_EPOCH)))
+
+-- Create the output text.
+local begin_line = "%%"
+local end_line   = "\n"
+
+local text = begin_line ..
+             table.concat(lines, end_line .. begin_line) ..
+             end_line .. end_line
+
+-- Write the output text.
+output_file:write(text)
+output_file:close()
+
+-- Everything is done, so now we can exit.
+os.exit(0)

Deleted: trunk/Master/bin/aarch64-linux/ebb
===================================================================
--- trunk/Master/bin/aarch64-linux/ebb	2025-02-12 14:09:34 UTC (rev 73915)
+++ trunk/Master/bin/aarch64-linux/ebb	2025-02-12 14:58:30 UTC (rev 73916)
@@ -1 +0,0 @@
-link xdvipdfmx
\ No newline at end of file

Added: trunk/Master/bin/aarch64-linux/ebb
===================================================================
--- trunk/Master/bin/aarch64-linux/ebb	                        (rev 0)
+++ trunk/Master/bin/aarch64-linux/ebb	2025-02-12 14:58:30 UTC (rev 73916)
@@ -0,0 +1 @@
+link extractbb
\ No newline at end of file


Property changes on: trunk/Master/bin/aarch64-linux/ebb
___________________________________________________________________
Added: svn:special
## -0,0 +1 ##
+*
\ No newline at end of property
Deleted: trunk/Master/bin/amd64-freebsd/ebb
===================================================================
--- trunk/Master/bin/amd64-freebsd/ebb	2025-02-12 14:09:34 UTC (rev 73915)
+++ trunk/Master/bin/amd64-freebsd/ebb	2025-02-12 14:58:30 UTC (rev 73916)
@@ -1 +0,0 @@
-link xdvipdfmx
\ No newline at end of file

Added: trunk/Master/bin/amd64-freebsd/ebb
===================================================================
--- trunk/Master/bin/amd64-freebsd/ebb	                        (rev 0)
+++ trunk/Master/bin/amd64-freebsd/ebb	2025-02-12 14:58:30 UTC (rev 73916)
@@ -0,0 +1 @@
+link extractbb
\ No newline at end of file


Property changes on: trunk/Master/bin/amd64-freebsd/ebb
___________________________________________________________________
Added: svn:special
## -0,0 +1 ##
+*
\ No newline at end of property
Deleted: trunk/Master/bin/amd64-netbsd/ebb
===================================================================
--- trunk/Master/bin/amd64-netbsd/ebb	2025-02-12 14:09:34 UTC (rev 73915)
+++ trunk/Master/bin/amd64-netbsd/ebb	2025-02-12 14:58:30 UTC (rev 73916)
@@ -1 +0,0 @@
-link xdvipdfmx
\ No newline at end of file

Added: trunk/Master/bin/amd64-netbsd/ebb
===================================================================
--- trunk/Master/bin/amd64-netbsd/ebb	                        (rev 0)
+++ trunk/Master/bin/amd64-netbsd/ebb	2025-02-12 14:58:30 UTC (rev 73916)
@@ -0,0 +1 @@
+link extractbb
\ No newline at end of file


Property changes on: trunk/Master/bin/amd64-netbsd/ebb
___________________________________________________________________
Added: svn:special
## -0,0 +1 ##
+*
\ No newline at end of property
Deleted: trunk/Master/bin/armhf-linux/ebb
===================================================================
--- trunk/Master/bin/armhf-linux/ebb	2025-02-12 14:09:34 UTC (rev 73915)
+++ trunk/Master/bin/armhf-linux/ebb	2025-02-12 14:58:30 UTC (rev 73916)
@@ -1 +0,0 @@
-link xdvipdfmx
\ No newline at end of file

Added: trunk/Master/bin/armhf-linux/ebb
===================================================================
--- trunk/Master/bin/armhf-linux/ebb	                        (rev 0)
+++ trunk/Master/bin/armhf-linux/ebb	2025-02-12 14:58:30 UTC (rev 73916)
@@ -0,0 +1 @@
+link extractbb
\ No newline at end of file


Property changes on: trunk/Master/bin/armhf-linux/ebb
___________________________________________________________________
Added: svn:special
## -0,0 +1 ##
+*
\ No newline at end of property
Deleted: trunk/Master/bin/i386-freebsd/ebb
===================================================================
--- trunk/Master/bin/i386-freebsd/ebb	2025-02-12 14:09:34 UTC (rev 73915)
+++ trunk/Master/bin/i386-freebsd/ebb	2025-02-12 14:58:30 UTC (rev 73916)
@@ -1 +0,0 @@
-link xdvipdfmx
\ No newline at end of file

Added: trunk/Master/bin/i386-freebsd/ebb
===================================================================
--- trunk/Master/bin/i386-freebsd/ebb	                        (rev 0)
+++ trunk/Master/bin/i386-freebsd/ebb	2025-02-12 14:58:30 UTC (rev 73916)
@@ -0,0 +1 @@
+link extractbb
\ No newline at end of file


Property changes on: trunk/Master/bin/i386-freebsd/ebb
___________________________________________________________________
Added: svn:special
## -0,0 +1 ##
+*
\ No newline at end of property
Deleted: trunk/Master/bin/i386-linux/ebb
===================================================================
--- trunk/Master/bin/i386-linux/ebb	2025-02-12 14:09:34 UTC (rev 73915)
+++ trunk/Master/bin/i386-linux/ebb	2025-02-12 14:58:30 UTC (rev 73916)
@@ -1 +0,0 @@
-link xdvipdfmx
\ No newline at end of file

Added: trunk/Master/bin/i386-linux/ebb
===================================================================
--- trunk/Master/bin/i386-linux/ebb	                        (rev 0)
+++ trunk/Master/bin/i386-linux/ebb	2025-02-12 14:58:30 UTC (rev 73916)
@@ -0,0 +1 @@
+link extractbb
\ No newline at end of file


Property changes on: trunk/Master/bin/i386-linux/ebb
___________________________________________________________________
Added: svn:special
## -0,0 +1 ##
+*
\ No newline at end of property
Deleted: trunk/Master/bin/i386-netbsd/ebb
===================================================================
--- trunk/Master/bin/i386-netbsd/ebb	2025-02-12 14:09:34 UTC (rev 73915)
+++ trunk/Master/bin/i386-netbsd/ebb	2025-02-12 14:58:30 UTC (rev 73916)
@@ -1 +0,0 @@
-link xdvipdfmx
\ No newline at end of file

Added: trunk/Master/bin/i386-netbsd/ebb
===================================================================
--- trunk/Master/bin/i386-netbsd/ebb	                        (rev 0)
+++ trunk/Master/bin/i386-netbsd/ebb	2025-02-12 14:58:30 UTC (rev 73916)
@@ -0,0 +1 @@
+link extractbb
\ No newline at end of file


Property changes on: trunk/Master/bin/i386-netbsd/ebb
___________________________________________________________________
Added: svn:special
## -0,0 +1 ##
+*
\ No newline at end of property
Deleted: trunk/Master/bin/i386-solaris/ebb
===================================================================
--- trunk/Master/bin/i386-solaris/ebb	2025-02-12 14:09:34 UTC (rev 73915)
+++ trunk/Master/bin/i386-solaris/ebb	2025-02-12 14:58:30 UTC (rev 73916)
@@ -1 +0,0 @@
-link xdvipdfmx
\ No newline at end of file

Added: trunk/Master/bin/i386-solaris/ebb
===================================================================
--- trunk/Master/bin/i386-solaris/ebb	                        (rev 0)
+++ trunk/Master/bin/i386-solaris/ebb	2025-02-12 14:58:30 UTC (rev 73916)
@@ -0,0 +1 @@
+link extractbb
\ No newline at end of file


Property changes on: trunk/Master/bin/i386-solaris/ebb
___________________________________________________________________
Added: svn:special
## -0,0 +1 ##
+*
\ No newline at end of property
Deleted: trunk/Master/bin/universal-darwin/ebb
===================================================================
--- trunk/Master/bin/universal-darwin/ebb	2025-02-12 14:09:34 UTC (rev 73915)
+++ trunk/Master/bin/universal-darwin/ebb	2025-02-12 14:58:30 UTC (rev 73916)
@@ -1 +0,0 @@
-link xdvipdfmx
\ No newline at end of file

Added: trunk/Master/bin/universal-darwin/ebb
===================================================================
--- trunk/Master/bin/universal-darwin/ebb	                        (rev 0)
+++ trunk/Master/bin/universal-darwin/ebb	2025-02-12 14:58:30 UTC (rev 73916)
@@ -0,0 +1 @@
+link extractbb
\ No newline at end of file


Property changes on: trunk/Master/bin/universal-darwin/ebb
___________________________________________________________________
Added: svn:special
## -0,0 +1 ##
+*
\ No newline at end of property
Deleted: trunk/Master/bin/windows/ebb.exe
===================================================================
(Binary files differ)

Deleted: trunk/Master/bin/x86_64-cygwin/ebb
===================================================================
--- trunk/Master/bin/x86_64-cygwin/ebb	2025-02-12 14:09:34 UTC (rev 73915)
+++ trunk/Master/bin/x86_64-cygwin/ebb	2025-02-12 14:58:30 UTC (rev 73916)
@@ -1 +0,0 @@
-link xdvipdfmx.exe
\ No newline at end of file

Added: trunk/Master/bin/x86_64-cygwin/ebb
===================================================================
--- trunk/Master/bin/x86_64-cygwin/ebb	                        (rev 0)
+++ trunk/Master/bin/x86_64-cygwin/ebb	2025-02-12 14:58:30 UTC (rev 73916)
@@ -0,0 +1 @@
+link extractbb
\ No newline at end of file


Property changes on: trunk/Master/bin/x86_64-cygwin/ebb
___________________________________________________________________
Added: svn:special
## -0,0 +1 ##
+*
\ No newline at end of property
Deleted: trunk/Master/bin/x86_64-darwinlegacy/ebb
===================================================================
--- trunk/Master/bin/x86_64-darwinlegacy/ebb	2025-02-12 14:09:34 UTC (rev 73915)
+++ trunk/Master/bin/x86_64-darwinlegacy/ebb	2025-02-12 14:58:30 UTC (rev 73916)
@@ -1 +0,0 @@
-link xdvipdfmx
\ No newline at end of file

Added: trunk/Master/bin/x86_64-darwinlegacy/ebb
===================================================================
--- trunk/Master/bin/x86_64-darwinlegacy/ebb	                        (rev 0)
+++ trunk/Master/bin/x86_64-darwinlegacy/ebb	2025-02-12 14:58:30 UTC (rev 73916)
@@ -0,0 +1 @@
+link extractbb
\ No newline at end of file


Property changes on: trunk/Master/bin/x86_64-darwinlegacy/ebb
___________________________________________________________________
Added: svn:special
## -0,0 +1 ##
+*
\ No newline at end of property
Deleted: trunk/Master/bin/x86_64-linux/ebb
===================================================================
--- trunk/Master/bin/x86_64-linux/ebb	2025-02-12 14:09:34 UTC (rev 73915)
+++ trunk/Master/bin/x86_64-linux/ebb	2025-02-12 14:58:30 UTC (rev 73916)
@@ -1 +0,0 @@
-link xdvipdfmx
\ No newline at end of file

Added: trunk/Master/bin/x86_64-linux/ebb
===================================================================
--- trunk/Master/bin/x86_64-linux/ebb	                        (rev 0)
+++ trunk/Master/bin/x86_64-linux/ebb	2025-02-12 14:58:30 UTC (rev 73916)
@@ -0,0 +1 @@
+link extractbb
\ No newline at end of file


Property changes on: trunk/Master/bin/x86_64-linux/ebb
___________________________________________________________________
Added: svn:special
## -0,0 +1 ##
+*
\ No newline at end of property
Deleted: trunk/Master/bin/x86_64-linuxmusl/ebb
===================================================================
--- trunk/Master/bin/x86_64-linuxmusl/ebb	2025-02-12 14:09:34 UTC (rev 73915)
+++ trunk/Master/bin/x86_64-linuxmusl/ebb	2025-02-12 14:58:30 UTC (rev 73916)
@@ -1 +0,0 @@
-link xdvipdfmx
\ No newline at end of file

Added: trunk/Master/bin/x86_64-linuxmusl/ebb
===================================================================
--- trunk/Master/bin/x86_64-linuxmusl/ebb	                        (rev 0)
+++ trunk/Master/bin/x86_64-linuxmusl/ebb	2025-02-12 14:58:30 UTC (rev 73916)
@@ -0,0 +1 @@
+link extractbb
\ No newline at end of file


Property changes on: trunk/Master/bin/x86_64-linuxmusl/ebb
___________________________________________________________________
Added: svn:special
## -0,0 +1 ##
+*
\ No newline at end of property
Deleted: trunk/Master/bin/x86_64-solaris/ebb
===================================================================
--- trunk/Master/bin/x86_64-solaris/ebb	2025-02-12 14:09:34 UTC (rev 73915)
+++ trunk/Master/bin/x86_64-solaris/ebb	2025-02-12 14:58:30 UTC (rev 73916)
@@ -1 +0,0 @@
-link xdvipdfmx
\ No newline at end of file

Added: trunk/Master/bin/x86_64-solaris/ebb
===================================================================
--- trunk/Master/bin/x86_64-solaris/ebb	                        (rev 0)
+++ trunk/Master/bin/x86_64-solaris/ebb	2025-02-12 14:58:30 UTC (rev 73916)
@@ -0,0 +1 @@
+link extractbb
\ No newline at end of file


Property changes on: trunk/Master/bin/x86_64-solaris/ebb
___________________________________________________________________
Added: svn:special
## -0,0 +1 ##
+*
\ No newline at end of property
Modified: trunk/Master/texmf-dist/doc/man/man1/extractbb.1
===================================================================
--- trunk/Master/texmf-dist/doc/man/man1/extractbb.1	2025-02-12 14:09:34 UTC (rev 73915)
+++ trunk/Master/texmf-dist/doc/man/man1/extractbb.1	2025-02-12 14:58:30 UTC (rev 73916)
@@ -39,7 +39,7 @@
 as used by
 .BR dvipdfm .
 .B Xbb
-may be defined as a synomym for
+may be defined as a synonym for
 .B extractbb
 on your system.
 .PP

Modified: trunk/Master/texmf-dist/doc/man/man1/extractbb.man1.pdf
===================================================================
(Binary files differ)

Modified: trunk/Master/texmf-dist/doc/support/extractbb/README.md
===================================================================
--- trunk/Master/texmf-dist/doc/support/extractbb/README.md	2025-02-12 14:09:34 UTC (rev 73915)
+++ trunk/Master/texmf-dist/doc/support/extractbb/README.md	2025-02-12 14:58:30 UTC (rev 73916)
@@ -1,7 +1,7 @@
 <!-- extractbb-lua
      https://github.com/gucci-on-fleek/extractbb
      SPDX-License-Identifier: MPL-2.0+ OR CC-BY-SA-4.0+
-     SPDX-FileCopyrightText: 2024 Max Chernoff
+     SPDX-FileCopyrightText: 2024--2025 Max Chernoff
 -->
 
 `extractbb-lua`
@@ -11,36 +11,6 @@
 [`extractbb`](https://texdoc.org/serve/extractbb/0), written in Lua.
 
 
-Variants
---------
-
-There are two variants of `extractbb-lua`:
-
-- **`wrapper`**: A wrapper script around the original `xdvipdmfx`-based
-  `extractbb` that is used to fix a security vulernability in
-  `xdvipdfmx`.
-
-- **`scratch`**: A standalone implementation of `extractbb`, written in
-  Lua from scratch, with no dependencies on `xdvipdfmx`.
-
-Currently, the script `extractbb` defaults to the `wrapper` variant, but
-you can manually select any specific variant by setting the
-`TEXLIVE_EXTRACTBB` environment variable to either `wrapper` or
-`scratch`.
-
-> [!WARNING]
-> The `scratch` variant is still in development and may be buggy or
-> insecure.
-
-
-### Secret Developer Options
-
-If you set `TEXLIVE_EXTRACTBB` to the full path of an executable, it
-will run that directly. And if you set
-`TEXLIVE_EXTRACTBB_UNSAFE=unsafe`, then it will ignore some of the
-security checks.
-
-
 Support
 -------
 
@@ -78,4 +48,4 @@
 licence file.)
 
 ---
-_v1.0.6 (2024-11-21)_ <!--%%version %%dashdate-->
+_v1.1.0 (2025-02-11)_ <!--%%version %%dashdate-->

Modified: trunk/Master/texmf-dist/doc/support/extractbb/extractbb.man1.pdf
===================================================================
(Binary files differ)

Deleted: trunk/Master/texmf-dist/scripts/extractbb/extractbb-scratch.lua
===================================================================
--- trunk/Master/texmf-dist/scripts/extractbb/extractbb-scratch.lua	2025-02-12 14:09:34 UTC (rev 73915)
+++ trunk/Master/texmf-dist/scripts/extractbb/extractbb-scratch.lua	2025-02-12 14:58:30 UTC (rev 73916)
@@ -1,706 +0,0 @@
-#!/usr/bin/env texlua
--- extractbb-lua
--- https://github.com/gucci-on-fleek/extractbb
--- SPDX-License-Identifier: MPL-2.0+
--- SPDX-FileCopyrightText: 2024 Max Chernoff
---
--- Inclusion Methods
--- =================
---
--- This script can use two different methods to extract bounding boxes from
--- images: the "img" module and the "pdfe" module. The "img" module will be
--- automatically selected in most cases and supports all image types that are
--- supported by the original "extractbb" program. If and only if the "img"
--- module fails to load, the "pdfe" module will be used as a fallback. However,
--- the "pdfe" module only supports PDF files. Both modules are built in to the
--- LuaTeX binaries, however due to some technical issues, the "img" module may
--- fail to load on some more exotic platforms.
---
---
--- Compatibility
--- =============
---
--- Based off of my testing, this Lua script is 100% compatible with the original
--- C-based "extractbb" program, with the following exceptions:
---
---   * When running in "img" mode, the PDF version is always reported as "1.5".
---
---   * When running in "img" mode, if the requested bounding box is not found,
---     the script will fallback to the Crop box or the Media box, instead of
---     following the original fallback order. (In practice, almost all PDFs set
---     all their bounding boxes equal to each other, and even if the boxes are
---     set to different values, the script will still return the requested box,
---     provided that it is set in the PDF.)
---
---   * When running in "pdfe" mode, only PDF files are supported.
---
--- All of these issues are very unlikely to affect any real-world documents.
---
---
--- Security
--- ========
---
--- This script is designed to be safely ran from restricted shell escape. A few
--- security features:
---
---   * The majority of this script runs inside a sandboxed Lua environment,
---     which only exposes a very restricted set of functions.
---
---   * All file-related functions available inside the sandbox first check with
---     kpathsea to ensure that the file is allowed to be opened.
---
---   * In the event of any errors, the script immediately exits.
---
---   * This script does not run (fork/exec) any external programs.
---
---   * This script is written entirely in Lua, so overflow/use-after-free
---     vulnerabilities are not possible.
---
--- Some potential security concerns:
---
---   * This script has not been audited or reviewed by anyone other than myself.
---
---   * Using the "ffi" module to load the "img" library is technically undefined
---     behaviour, and as such may potentially lead to unforeseen security
---     issues.
---
---   * The underlying LuaTeX modules may themselves have security
---     vulnerabilities, which would be inherited by this script.
-
-
-----------------------
---- Initialization ---
-----------------------
-
--- Pre-sandbox variables/constants
-local show_errors = true
-local SOURCE_DATE_EPOCH = tonumber(os.getenv("SOURCE_DATE_EPOCH"))
-local version = "extractbb.lua v1.0.6 (2024-11-21)" --%%version %%dashdate
-
--- Required for any kpathsea calls to work.
-kpse.set_program_name("texlua", "extractbb")
-
-
---------------------------
---- Questionable Hacks ---
---------------------------
-
--- LuaTeX doesn't load the "img" library in "texlua" mode, so we need this
--- questionable hack to load it manually. We do it inside of "pcall" since there
--- are some exotic platforms where the "ffi" module is unsupported.
-pcall(function()
-    local ffi
-
-    if img then
-        -- Ok, we're running under a recent LuaTeX that enables the "img"
-        -- library in "texlua" mode, so we can skip the FFI hack.
-        ffi = false
-    else
-        ffi = package.loaded.ffi
-
-        ffi.cdef[[
-            typedef struct lua_State lua_State;
-            typedef int (*lua_CFunction) (lua_State *L);
-
-            lua_State *Luas;
-            void luaL_requiref(lua_State *L, const char *modname,
-                                lua_CFunction openf, int glb);
-            int luaopen_img(lua_State * L);
-
-            int lua_only;
-        ]]
-
-        -- Basic initialization
-        ffi.C.lua_only = 0
-    end
-    tex.initialize()
-
-    -- "tex" module
-    _G.tex = package.loaded.tex
-    tex.enableprimitives("", tex.extraprimitives())
-    tex.outputmode = 1
-    tex.interactionmode = 0
-
-    -- "pdf" module
-    _G.pdf = package.loaded.pdf
-    pdf.setignoreunknownimages(1)
-    pdf.setmajorversion(2)
-    pdf.setminorversion(0)
-
-    -- "img" module
-    if ffi then
-        ffi.C.luaL_requiref(ffi.C.Luas, "img", ffi.C.luaopen_img, 1)
-    end
-end)
-
--- In case of failure, define an empty "img" table.
-if not img then
-    _G.img = {}
-end
-
-
-------------------
---- Sandboxing ---
-------------------
-
--- Prepare the sandbox for the rest of the script.
-local env = {
-    arg      = arg,
-    io       = { stdout = io.stdout, },
-    ipairs   = ipairs,
-    math     = math,
-    os       = { date = os.date, exit = os.exit, },
-    pairs    = pairs,
-    pdfe     = pdfe,
-    print    = print,
-    select   = select,
-    table    = table,
-    tonumber = tonumber,
-    type     = type,
-}
-
-do
-    -- Saved global functions
-    local debug_traceback  = debug.traceback
-    local find_file        = kpse.find_file
-    local img_scan         = img.scan
-    local io_open          = io.open
-    local io_stderr        = io.stderr
-    local kpse_in_name_ok  = kpse.in_name_ok
-    local kpse_out_name_ok = kpse.out_name_ok
-    local kpse_var_value   = kpse.var_value
-    local lfs_attributes   = lfs.attributes
-    local os_exit          = os.exit
-    local os_setenv        = os.setenv
-    local pdfe_open        = pdfe.open
-    local select           = select
-    local tostring         = tostring
-
-    -- Error messages
-    local function error(...)
-        if show_errors then
-            -- Header
-            io_stderr:write("! extractbb ERROR: ")
-
-            -- Message
-            for i = 1, select("#", ...) do
-                io_stderr:write(tostring(select(i, ...)), " ")
-            end
-
-            -- Traceback
-            io_stderr:write("\n", "\n")
-            io_stderr:write(debug_traceback(nil, 2), "\n")
-        end
-
-        -- Flush and exit
-        io_stderr:flush()
-        os_exit(1)
-    end
-
-    env.error = error
-
-    -- Make sure that "openin_any" is at least "restricted", and that
-    -- "openout_any" is at least "paranoid".
-    local initial_openin  = kpse_var_value("openin_any")
-    local initial_openout = kpse_var_value("openout_any")
-
-    if (initial_openin ~= "r") or (initial_openout ~= "p") then
-        os_setenv("openin_any",  "r")
-    end
-
-    if (initial_openout ~= "p") then
-        os_setenv("openout_any", "p")
-    end
-
-    -- Check the input paths.
-    local function resolve_input_name(file_name)
-        local file_path = find_file(file_name, "graphic/figure", true)
-        if not file_path then
-            error("Cannot find input file:", file_name)
-        end
-
-        local allowed = kpse_in_name_ok(file_path)
-        if not allowed then
-            error("Input file is not allowed:", file_path)
-        end
-
-        local mode = lfs_attributes(file_path, "mode")
-        if mode ~= "file" then
-            error("Input file is not a regular file:", file_path)
-        end
-
-        return file_path
-    end
-
-    -- Check the output paths.
-    local function resolve_output_name(file_name)
-        local allowed = kpse_out_name_ok(file_name)
-        if not allowed then
-            error("Output file is not allowed:", file_name)
-        end
-
-        local name, extension = file_name:match("(.+)%.([^.]-)$")
-
-        if (not name) or (not extension) or
-           (name == "") or (extension == "")
-        then
-            error("Output file has no extension:", file_name)
-        end
-
-        if (extension ~= "xbb") and (extension ~= "bb") then
-            error("Output file has an invalid extension:", file_name)
-        end
-
-        -- We shouldn't allow files with weird characters in their names.
-        if name:match("[%c%%\t\r\n><*|]") then
-            error("Output file has an invalid name:", file_name)
-        end
-
-        return file_name
-    end
-
-    -- Opens a file.
-    function env.open_file(file_name, read_write, binary_text)
-        local file_path, mode
-        if read_write == "read" then
-            file_path = resolve_input_name(file_name)
-            mode = "r"
-        elseif read_write == "write" then
-            file_path = resolve_output_name(file_name)
-            mode = "w"
-        else
-            error("Invalid read/write mode:", read_write)
-        end
-
-        if binary_text == "binary" then
-            mode = mode .. "b"
-        elseif binary_text == "text" then
-            mode = mode .. ""
-        else
-            error("Invalid binary/text mode:", binary_text)
-        end
-
-        local file, message = io_open(file_path, mode)
-
-        if not file then
-            error("Cannot open file:", file_path, message)
-        end
-
-        return file
-    end
-
-    -- Open an PDF file.
-    function env.pdfe.open(file_name)
-        local file_path = resolve_input_name(file_name)
-        return pdfe_open(file_path)
-    end
-
-    -- Open an image file.
-    function env.open_image(file_name, page, box)
-        local file_path = resolve_input_name(file_name)
-        return img_scan {
-            filename = file_path,
-            filepath = file_path,
-            page     = page,
-            pagebox  = box,
-        }
-    end
-
-    if not img_scan then
-        env.open_image = false
-    end
-end
-
--- Prevent trying to change the environment.
-local function bad_index(...)
-    env.error("Attempt to access an undefined index:", select(2, ...))
-end
-
-setmetatable(env, {
-    __index     = bad_index,
-    __metatable = false,
-    __newindex  = bad_index,
-})
-
--- Set the environment.
-_ENV = env
-
-
------------------------------------
---- Post-Sandbox Initialization ---
------------------------------------
-
--- Constants
-local BP_TO_SP    = 65781.76
-local IN_TO_BP    = 72
-local DATE_FORMAT = "%a %b %d %H:%M:%S %Y" -- "%c"
-
--- Save often-used globals for a slight speed boost.
-local floor            = math.floor
-local insert           = table.insert
-local remove           = table.remove
-local script_arguments = arg
-local unpack           = table.unpack
-
--- General-purpose functions
-local function round(number)
-    return floor(number +0.5)
-end
-
-
--------------------------
---- Argument Handling ---
--------------------------
-
--- Define the argument handling functions.
-local process_arguments = {}
-
--- > Specify a PDF pagebox for bounding box
--- > pagebox=cropbox, mediabox, artbox, trimbox, bleedbox
-local bbox_option = "auto"
-function process_arguments.B(script_arguments)
-    bbox_option = remove(script_arguments, 1)
-end
-
--- > Show this help message and exit
-function process_arguments.h(script_arguments)
-    print [[
-Usage: extractbb [-B pagebox] [-p page] [-q|-v] [-O] [-m|-x] FILE...
-       extractbb --help|--version
-Extract bounding box from PDF, PNG, JPEG, JP2, or BMP file; default output below.
-
-Options:
-  -B pagebox    Specify a PDF pagebox for bounding box
-                pagebox=cropbox, mediabox, artbox, trimbox, bleedbox
-  -h | --help   Show this help message and exit
-  --version     Output version information and exit
-  -p page       Specify a PDF page to extract bounding box
-  -q            Be quiet
-  -v            Be verbose
-  -O            Write output to stdout
-  -m            Output .bb  file used in DVIPDFM (default)
-  -x            Output .xbb file used in DVIPDFMx
-]]
-    os.exit(0)
-end
-
-process_arguments["-help"] = process_arguments.h
-
--- > Output version information and exit
-function process_arguments.V(script_arguments)
-    print(version)
-    os.exit(0)
-end
-
-process_arguments["-version"] = process_arguments.V
-
--- > Specify a PDF page to extract bounding box
-local page_number = 1
-function process_arguments.p(script_arguments)
-    page_number = tonumber(remove(script_arguments, 1))
-end
-
--- > Be quiet
-function process_arguments.q(script_arguments)
-    show_errors = false
-end
-
--- > Be verbose
-function process_arguments.v(script_arguments)
-    show_errors = true
-end
-
--- > Write output to stdout
-local output_file
-function process_arguments.O(script_arguments)
-    output_file = io.stdout
-end
-
--- Output format
-local output_format = "xbb"
-
-if script_arguments[0]:match("ebb") then
-    output_format = "bb"
-end
-
--- > Output .bb  file used in DVIPDFM (default)
-function process_arguments.m(script_arguments)
-    output_format = "bb"
-end
-
--- > Output .xbb file used in DVIPDFMx
-function process_arguments.x(script_arguments)
-    output_format = "xbb"
-end
-
--- Get the input file name.
-local input_name
-function process_arguments.i(script_arguments)
-    input_name = remove(script_arguments, 1)
-end
-
-process_arguments["-input-name"] = process_arguments.i
-
--- Clear the interpreter and script names.
-script_arguments[-1] = nil
-script_arguments[0]  = nil
-
--- Process the arguments.
-while script_arguments[1] do
-    -- Get the next argument.
-    local arg = remove(script_arguments, 1)
-    local cmd = arg:match("^%-(.*)$")
-
-    -- Default to "--input-name" if no command is given.
-    if not cmd then
-        insert(script_arguments, 1, arg)
-        cmd = "-input-name"
-    end
-
-    -- Handle multi-character arguments.
-    if (cmd:len() >= 2) and (not cmd:match("^%-")) then
-        local i = 0
-        for char in cmd:gmatch(".") do
-            i = i + 1
-            insert(script_arguments, i, "-" .. char)
-        end
-
-        goto continue
-    end
-
-    -- Get the function to process the argument and run it.
-    local func = process_arguments[cmd]
-
-    if not func then
-        error("Invalid argument:", arg)
-    end
-
-    func(script_arguments)
-
-    ::continue::
-end
-
--- Validate the arguments.
-if not type(page_number) == "number" then
-    error("Invalid page number:", page_number)
-end
-
-if not input_name then
-    error("No input file specified.")
-end
-
--- Validate the bounding box type. We need this rather crazy fallback scheme
--- to match the behaviour of "extractbb".
-local bbox_orders = {}
-bbox_orders.mediabox = {
-    { img = "media", pdfe = "MediaBox" },
-}
-bbox_orders.cropbox = {
-    { img = "crop", pdfe = "CropBox" }, unpack(bbox_orders.mediabox)
-}
-bbox_orders.artbox = {
-    { img = "art", pdfe = "ArtBox" }, unpack(bbox_orders.cropbox)
-}
-bbox_orders.trimbox = {
-    { img = "trim", pdfe = "TrimBox" }, unpack(bbox_orders.artbox)
-}
-bbox_orders.bleedbox = {
-    { img = "bleed", pdfe = "BleedBox" }, unpack(bbox_orders.trimbox)
-}
-bbox_orders.auto = {
-    bbox_orders.cropbox[1], bbox_orders.artbox[1], bbox_orders.trimbox[1],
-    bbox_orders.bleedbox[1], bbox_orders.mediabox[1],
-}
-
-local bbox_order = bbox_orders[bbox_option]
-
-if not bbox_order then
-    error("Invalid PDF box type:", bbox_option)
-end
-
--- Set the default pixel resolution.
-local default_dpi
-if output_format == "xbb" then
-    default_dpi = 72
-elseif output_format == "bb" then
-    default_dpi = 100
-else
-    error("Invalid output format:", output_format)
-end
-
--- Open the output file.
-if not output_file then
-    local base_name   = input_name:match("(.+)%.([^.]-)$") or input_name
-    local output_name = base_name .. "." .. output_format
-    output_file = open_file(output_name, "write", "text")
-end
-
-
-------------------------
---- Image Processing ---
-------------------------
-
-local x_min, y_min, x_max, y_max
-local num_pages, image_type
-local pdf_major_version, pdf_minor_version
-
-if open_image then
-    -- Check the number of pages.
-    local image = open_image(input_name)
-    num_pages = image.pages
-
-    if page_number > num_pages then
-        error("Invalid page number:", page_number)
-    end
-
-    -- Open the image to the specified page and bounding box. If the requested
-    -- bounding box is not available, LuaTeX will fall back to the crop box
-    -- or the media box.
-    image = open_image(input_name, page_number, bbox_order[1].img)
-
-    if not image then
-        error("Cannot open image:", input_name)
-    end
-
-    -- Get the image metadata.
-    image_type   = image.imagetype
-    local bounding_box = image.bbox
-
-    if not bounding_box then
-        error("Cannot get bounding box:", page_number)
-    end
-
-    local x_resolution = image.xres
-    local y_resolution = image.yres
-
-    if (x_resolution or 0) == 0 then
-        x_resolution = default_dpi
-    end
-
-    if (y_resolution or 0) == 0 then
-        y_resolution = default_dpi
-    end
-
-    -- Convert the bounding box to PostScript points.
-    for i, dimen in ipairs(bounding_box) do
-        if image_type == "pdf" then
-            dimen = dimen / BP_TO_SP
-        else
-            if i % 2 == 1 then
-                dimen = dimen / x_resolution * IN_TO_BP
-            else
-                dimen = dimen / y_resolution * IN_TO_BP
-            end
-        end
-
-        bounding_box[i] = dimen
-    end
-
-    -- Save the bounding box.
-    x_min, y_min, x_max, y_max = unpack(bounding_box)
-
-    -- We can't get the PDF version with the "img" library, so we'll just
-    -- pretend that it's v1.5 (which supports most features).
-    pdf_major_version = 1
-    pdf_minor_version = 5
-else
-    -- Fallback to PDFs only.
-    image_type = "pdf"
-    local document = pdfe.open(input_name)
-
-    if pdfe.getstatus(document) ~= 0 then
-        error("Cannot open PDF file:", input_name)
-    end
-
-    -- Check the number of pages.
-    num_pages = pdfe.getnofpages(document)
-
-    if type(num_pages) ~= "number" then
-        error("Invalid number of pages:", num_pages)
-    end
-
-    if page_number > num_pages then
-        error("Invalid page number:", page_number)
-    end
-
-    -- Get the page.
-    local page = pdfe.getpage(document, page_number)
-
-    if not page then
-        error("Cannot get page:", page_number)
-    end
-
-    -- Get the bounding box. Here, we check the boxes in the exact same order
-    -- that "extractbb" does.
-    local bounding_box
-    for _, bbox in ipairs(bbox_order) do
-        bounding_box = pdfe.getbox(page, bbox.pdfe)
-
-        if bounding_box then
-            break
-        end
-    end
-
-    if not bounding_box then
-        error("Cannot get bounding box:", page_number)
-    end
-
-    -- Save the bounding box.
-    x_min, y_min, x_max, y_max = unpack(bounding_box)
-
-    -- Get the PDF version.
-    pdf_major_version, pdf_minor_version = pdfe.getversion(document)
-end
-
--- Validate the bounding box.
-for _, dimen in ipairs { x_min, y_min, x_max, y_max } do
-    if type(dimen) ~= "number" then
-        error("Invalid bounding box:", x_min, y_min, x_max, y_max)
-    end
-end
-
-
---------------
---- Output ---
---------------
-
--- Get the output fields and values.
-local lines = {}
-
-insert(lines, ("Title: %s"):format(input_name))
-insert(lines, ("Creator: %s"):format(version))
-insert(lines,
-       ("BoundingBox: %d %d %d %d")
-       :format(round(x_min), round(y_min), round(x_max), round(y_max)))
-
-if output_format == "xbb" then
-    insert(lines,
-           ("HiResBoundingBox: %0.6f %0.6f %0.6f %0.6f")
-           :format(x_min, y_min, x_max, y_max))
-
-    if image_type == "pdf" then
-        insert(lines,
-               ("PDFVersion: %d.%d")
-               :format(pdf_major_version, pdf_minor_version))
-
-        insert(lines, ("Pages: %d"):format(num_pages))
-    end
-
-end
-
-insert(lines, ("CreationDate: %s"):format(os.date(DATE_FORMAT, SOURCE_DATE_EPOCH)))
-
--- Create the output text.
-local begin_line = "%%"
-local end_line   = "\n"
-
-local text = begin_line ..
-             table.concat(lines, end_line .. begin_line) ..
-             end_line .. end_line
-
--- Write the output text.
-output_file:write(text)
-output_file:close()
-
--- Everything is done, so now we can exit.
-os.exit(0)

Deleted: trunk/Master/texmf-dist/scripts/extractbb/extractbb-wrapper.lua
===================================================================
--- trunk/Master/texmf-dist/scripts/extractbb/extractbb-wrapper.lua	2025-02-12 14:09:34 UTC (rev 73915)
+++ trunk/Master/texmf-dist/scripts/extractbb/extractbb-wrapper.lua	2025-02-12 14:58:30 UTC (rev 73916)
@@ -1,270 +0,0 @@
-#!/usr/bin/env texlua
--- extractbb-lua
--- https://github.com/gucci-on-fleek/extractbb
--- SPDX-License-Identifier: MPL-2.0+
--- SPDX-FileCopyrightText: 2024 Max Chernoff
---
--- A generic wrapper to make commands safe to run with restricted shell escape.
---
--- Originally created for extractbb, which is listed in shell_escape_commands,
--- but can be run as dvipdfm(x), which in turn can run arbitrary commands
--- using its -D option.
---
--- The idea is to exec "ebb --ebb <other args>", since only argv[1] is
--- used by dvipdfmx to determine its behavior.
---
--- Note: This script can only adjust the paths and arguments of the target
--- executable; it *CANNOT* make an arbitrary program safe to run with
--- restricted shell escape.
-
--- A shorter, less paranoid version.
--- (Prepend a hyphen to the line below to enable).
---[=[
-arg[0] = arg[0]:gsub("extractbb", "ebb")
-table.insert(arg, 1, "ebb")
-table.insert(arg, 2, "--extractbb")
-os.exec(arg)
-os.exit(1)
---]=]
-
----------------------
---- Configuration ---
----------------------
-
--- The base name of this script. (Example: ``extractbb'')
-local SCRIPT_NAME = "extractbb"
-
--- The base name of the path to the target program. (Example: ``xdvipdfmx'')
-local TARGET_PATH_NAME = "xdvipdfmx"
-
--- The name to use when calling the target program. Equivalent to ``argv[0]''
--- in C. (Example: ``extractbb'')
-local TARGET_EXEC_NAME = "ebb"
-
--- Any extra arguments to be prepended to the target program, before any
--- user-supplied arguments. Equivalent to ``argv[1], ...'' in C.
--- (Example: ``--extractbb'')
-local TARGET_PREPEND_ARGS = { "--extractbb" }
-
--- Any extra arguments to be appended to the target program, after any
--- user-supplied arguments. Equivalent to ``..., argv[argc]'' in C.
-local TARGET_APPEND_ARGS = {}
-
--- Sets the value of ``openin_any'' to this value. If ``nil'', then the value
--- will be left unchanged. (Example: ``r'')
-local READ_PERMS = "r"
-
--- Sets the value of ``openout_any'' to this value. If ``nil'', then the value
--- will be left unchanged. (Example: ``p'')
-local WRITE_PERMS = "p"
-
--- The name of the Lua interpreter. (Example: ``texlua'')
-local INTERPRETER_NAME = "texlua"
-
--- The extension of the interpreter. Extensionless-names are also permitted.
--- (Example: ``exe'')
-local INTERPRETER_EXT = "exe"
-
-
-----------------------
---- Initialization ---
-----------------------
-
--- Save often-used globals for a slight speed boost.
-local insert = table.insert
-
--- Set the kpathsea program name
-kpse.set_program_name(INTERPRETER_NAME, SCRIPT_NAME)
-
--- Rename the input arguments so we don't get confused
-local script_args = arg
-
-
-----------------------------
---- Function Definitions ---
-----------------------------
-
--- Error messages
-local function error(title, details)
-    -- Header
-    io.stderr:write("! extractbb ERROR: ")
-    io.stderr:write(title)
-    io.stderr:write(".\n\nTechnical Details:\n")
-
-    -- Messages
-    for key, value in pairs(details) do
-        io.stderr:write(tostring(key), ": ")
-        io.stderr:write("(", type(value), ") ")
-        io.stderr:write(tostring(value), "\n")
-    end
-
-    -- Traceback
-    io.stderr:write("\n")
-    io.stderr:write(debug.traceback(nil, 2), "\n")
-
-    -- Flush and exit
-    io.stderr:flush()
-    os.exit(1)
-end
-
--- Get the directory, name, and extension from a full path. We'll split on
--- either a forward or backward slash---Windows can use either, and we don't
--- need to support Unix systems with TL installed to a directory with
--- backslashes in its name.
-local split_dir_pattern = "^(.*)[/\\]([^/\\]-)$"
-local split_ext_pattern = "(.*)%.([^.]-)$"
-
-local function split_path(path)
-    -- Make sure that we were given a string
-    if type(path) ~= "string" then
-        return nil, nil, nil
-    end
-
-    -- Split the (directory) from the (name and extension)
-    local dir, name_ext = path:match(split_dir_pattern)
-
-    -- No directory
-    if not dir then
-        dir      = nil
-        name_ext = path
-
-    -- A bare directory (with a trailing slash)
-    elseif name_ext == "" then
-        return dir, nil, nil
-    end
-
-    -- Split the (name) from the (extension)
-    local name, ext = name_ext:match(split_ext_pattern)
-
-    -- No extension (or a dotfile)
-    if (not name) or (name == "") then
-        name = name_ext
-        ext  = nil
-    end
-
-    return dir, name, ext
-end
-
--- See if a file exists
-local function file_exists(path)
-    local mode = lfs.attributes(path, "mode")
-    return (mode == "file") or (mode == "link")
-end
-
-
----------------------
---- Safety Checks ---
----------------------
-
--- Make sure that we're running unrestricted.
-if status.shell_escape ~= 1 then
-    error("Shell escape has been disabled", {
-        shell_escape = status.shell_escape,
-    })
-end
-
-if status.safer_option ~= 0 then
-    error("The ``safer'' option has been enabled", {
-        safer_option = status.safer_option,
-    })
-end
-
--- Set the file permissions.
-if READ_PERMS then
-    os.setenv("openin_any", READ_PERMS)
-end
-
-if WRITE_PERMS then
-    os.setenv("openout_any", WRITE_PERMS)
-end
-
--- Get the location of the interpreter
-local interpreter_dir = os.selfdir or kpse.var_value("SELFAUTOLOC")
-local _, interpreter_name, interpreter_ext = split_path(script_args[-1])
-
-if os.type == "windows" then
-    interpreter_ext = INTERPRETER_EXT
-end
-
--- Error details
-local error_details = {
-    interpreter_dir     = interpreter_dir  or "<nil>",
-    interpreter_name    = interpreter_name or "<nil>",
-    interpreter_ext     = interpreter_ext  or "<nil>",
-    os_type             = os.type          or "<nil>",
-    os_name             = os.name          or "<nil>",
-}
-
--- Get the path to the target program
-local target_ext  = interpreter_ext and ("." .. interpreter_ext) or ""
-local target_path = interpreter_dir .. "/" .. TARGET_PATH_NAME .. target_ext
-
-error_details.target_path = target_path or "<nil>"
-error_details.target_ext  = target_ext  or "<nil>"
-
--- Make sure that the target program exists
-if not file_exists(target_path) then
-    error("The target program does not exist", error_details)
-end
-
-
-----------------------
---- Run the target ---
-----------------------
-
--- Generate the target arguments
-local target_args = {
-    [0] = target_path,      -- Path to the executable
-    [1] = TARGET_EXEC_NAME, -- argv[0]
-}
-
--- argv[2] through argv[n]
-for _, arg in ipairs(TARGET_PREPEND_ARGS) do
-    insert(target_args, arg)
-end
-
-for i = 1, #script_args do
-    -- We use a numeric iterator here to avoid ``arg[-1]'' and ``arg[0]''.
-    local this_arg = script_args[i]
-    insert(target_args, this_arg)
-
-    -- Show version information
-    if this_arg:match("%-version") then
-        print("[Wrapped by extractbb.lua v1.0.6 (2024-11-21)]") --%%version %%dashdate
-    end
-end
-
-for _, arg in ipairs(TARGET_APPEND_ARGS) do
-    insert(target_args, arg)
-end
-
--- For Unixish OSs, argv is passed as an array into a spawned process, so no
--- further tokenization is done by the C runtime, therefore arguments containing
--- special characters won't cause any issues. For Windows, argv is concatenated
--- into a single string when spawning a new process, then retokenized by the C
--- runtime in the new program, so we need to be careful with special characters
--- here since they might be interpreted as token separators.
-if os.type == "windows" then
-    for i, arg in ipairs(target_args) do
-        -- Hmm, which characters do we need to protect against?
-        if arg:match('"') then
-            -- Already contains a quote, so let's hope that this means that
-            -- someone already quoted it correctly.
-        elseif arg:match("[^-_%l%u%d]") then
-            -- Contains a special character, so let's quote it.
-            target_args[i] = '"' .. arg .. '"'
-        end
-        -- Any other cases and things should be fine. Probably.
-    end
-end
-
--- Run the target program, replacing the current process
-local _, err = os.exec(target_args)
-
--- Unreachable except in the case of a failed exec
-for key, value in ipairs(target_args) do
-    error_details["target_args[" .. key .. "]"] = value
-end
-
-error_details.exec_message = err or "<nil>"
-error("The target program failed to run", error_details)

Modified: trunk/Master/texmf-dist/scripts/extractbb/extractbb.lua
===================================================================
--- trunk/Master/texmf-dist/scripts/extractbb/extractbb.lua	2025-02-12 14:09:34 UTC (rev 73915)
+++ trunk/Master/texmf-dist/scripts/extractbb/extractbb.lua	2025-02-12 14:58:30 UTC (rev 73916)
@@ -2,86 +2,664 @@
 -- extractbb-lua
 -- https://github.com/gucci-on-fleek/extractbb
 -- SPDX-License-Identifier: MPL-2.0+
--- SPDX-FileCopyrightText: 2024 Max Chernoff
+-- SPDX-FileCopyrightText: 2024--2025 Max Chernoff
 --
--- A wrapper script to allow you to choose which implementation of extractbb to
--- use. Should hopefully be replaced with the ``scratch'' file in TeX Live 2025.
+-- Inclusion Methods
+-- =================
 --
--- v1.0.6 (2024-11-21) %%version %%dashdate
+-- This script can use two different methods to extract bounding boxes from
+-- images: the "img" module and the "pdfe" module. The "img" module will be
+-- automatically selected in most cases and supports all image types that are
+-- supported by the original "extractbb" program. If and only if the "img"
+-- module fails to load, the "pdfe" module will be used as a fallback. However,
+-- the "pdfe" module only supports PDF files. Both modules are built in to the
+-- LuaTeX binaries, however due to some technical issues, the "img" module may
+-- fail to load on some more exotic platforms.
+--
+--
+-- Compatibility
+-- =============
+--
+-- Based off of my testing, this Lua script is 100% compatible with the original
+-- C-based "extractbb" program, with the following exceptions:
+--
+--   * When running in "img" mode, the PDF version is always reported as "1.5".
+--
+--   * When running in "img" mode, if the requested bounding box is not found,
+--     the script will fallback to the Crop box or the Media box, instead of
+--     following the original fallback order. (In practice, almost all PDFs set
+--     all their bounding boxes equal to each other, and even if the boxes are
+--     set to different values, the script will still return the requested box,
+--     provided that it is set in the PDF.)
+--
+--   * When running in "pdfe" mode, only PDF files are supported.
+--
+-- All of these issues are very unlikely to affect any real-world documents.
+--
+--
+-- Security
+-- ========
+--
+-- This script is designed to be safely ran from restricted shell escape. A few
+-- security features:
+--
+--   * The majority of this script runs inside a sandboxed Lua environment,
+--     which only exposes a very restricted set of functions.
+--
+--   * All file-related functions available inside the sandbox first check with
+--     kpathsea to ensure that the file is allowed to be opened.
+--
+--   * In the event of any errors, the script immediately exits.
+--
+--   * This script does not run (fork/exec) any external programs.
+--
+--   * This script is written entirely in Lua, so overflow/use-after-free
+--     vulnerabilities are not possible.
+--
+-- Some potential security concerns:
+--
+--   * This script has not been audited or reviewed by anyone other than myself.
+--
+--   * The underlying LuaTeX modules may themselves have security
+--     vulnerabilities, which would be inherited by this script.
 
----------------------
---- Configuration ---
----------------------
--- Choose which implementation of extractbb to use.
-local DEFAULT = "wrapper"
 
+----------------------
+--- Initialization ---
+----------------------
 
------------------
---- Execution ---
------------------
+-- Pre-sandbox variables/constants
+local show_errors = true
+local SOURCE_DATE_EPOCH = tonumber(os.getenv("SOURCE_DATE_EPOCH"))
+local version = "extractbb.lua v1.1.0 (2025-02-11)" --%%version %%dashdate
 
--- Send the error messages to stderr.
-local function error(...)
-    -- Header
-    io.stderr:write("! extractbb ERROR: ")
+-- Required for any kpathsea calls to work.
+kpse.set_program_name("texlua", "extractbb")
 
-    -- Message
-    for i = 1, select("#", ...) do
-        io.stderr:write(tostring(select(i, ...)), " ")
+-- Required to use the "img" module from texlua, but only works for LuaTeX
+-- versions >= 1.21.0.
+if not (status.development_id >= 7661) then
+    error("LuaTeX version is too old, cannot proceed.")
+end
+texconfig.texlua_img = true
+
+-- We need to set \outputmode to PDF to be able to use most of the "img" module
+-- functions, but to set \outputmode, we need to initialize the TeX interpreter.
+tex.initialize()
+_G.tex = package.loaded.tex
+tex.enableprimitives("", tex.extraprimitives())
+tex.outputmode = 1
+tex.interactionmode = 0
+
+-- "pdf" module
+_G.pdf = package.loaded.pdf
+pdf.setignoreunknownimages(1)
+pdf.setmajorversion(2)
+pdf.setminorversion(0)
+
+
+------------------
+--- Sandboxing ---
+------------------
+
+-- Prepare the sandbox for the rest of the script.
+local env = {
+    arg      = arg,
+    io       = { stdout = io.stdout, },
+    ipairs   = ipairs,
+    math     = math,
+    os       = { date = os.date, exit = os.exit, },
+    pairs    = pairs,
+    pdfe     = pdfe,
+    print    = print,
+    select   = select,
+    table    = table,
+    tonumber = tonumber,
+    type     = type,
+}
+
+do
+    -- Saved global functions
+    local debug_traceback  = debug.traceback
+    local find_file        = kpse.find_file
+    local img_scan         = img.scan
+    local io_open          = io.open
+    local io_stderr        = io.stderr
+    local kpse_in_name_ok  = kpse.in_name_ok
+    local kpse_out_name_ok = kpse.out_name_ok
+    local kpse_var_value   = kpse.var_value
+    local lfs_attributes   = lfs.attributes
+    local os_exit          = os.exit
+    local os_setenv        = os.setenv
+    local pdfe_open        = pdfe.open
+    local select           = select
+    local tostring         = tostring
+
+    -- Error messages
+    local function error(...)
+        if show_errors then
+            -- Header
+            io_stderr:write("! extractbb ERROR: ")
+
+            -- Message
+            for i = 1, select("#", ...) do
+                io_stderr:write(tostring(select(i, ...)), " ")
+            end
+
+            -- Traceback
+            io_stderr:write("\n", "\n")
+            io_stderr:write(debug_traceback(nil, 2), "\n")
+        end
+
+        -- Flush and exit
+        io_stderr:flush()
+        os_exit(1)
     end
 
-    -- Flush and exit
-    io.stderr:write("\n")
-    io.stderr:flush()
-    os.exit(1)
+    env.error = error
+
+    -- Make sure that "openin_any" is at least "restricted", and that
+    -- "openout_any" is at least "paranoid".
+    local initial_openin  = kpse_var_value("openin_any")
+    local initial_openout = kpse_var_value("openout_any")
+
+    if (initial_openin ~= "r") or (initial_openout ~= "p") then
+        os_setenv("openin_any",  "r")
+    end
+
+    if (initial_openout ~= "p") then
+        os_setenv("openout_any", "p")
+    end
+
+    -- Check the input paths.
+    local function resolve_input_name(file_name)
+        local file_path = find_file(file_name, "graphic/figure", true)
+        if not file_path then
+            error("Cannot find input file:", file_name)
+        end
+
+        local allowed = kpse_in_name_ok(file_path)
+        if not allowed then
+            error("Input file is not allowed:", file_path)
+        end
+
+        local mode = lfs_attributes(file_path, "mode")
+        if mode ~= "file" then
+            error("Input file is not a regular file:", file_path)
+        end
+
+        return file_path
+    end
+
+    -- Check the output paths.
+    local function resolve_output_name(file_name)
+        local allowed = kpse_out_name_ok(file_name)
+        if not allowed then
+            error("Output file is not allowed:", file_name)
+        end
+
+        local name, extension = file_name:match("(.+)%.([^.]-)$")
+
+        if (not name) or (not extension) or
+           (name == "") or (extension == "")
+        then
+            error("Output file has no extension:", file_name)
+        end
+
+        if (extension ~= "xbb") and (extension ~= "bb") then
+            error("Output file has an invalid extension:", file_name)
+        end
+
+        -- We shouldn't allow files with weird characters in their names.
+        if name:match("[%c%%\t\r\n><*|]") then
+            error("Output file has an invalid name:", file_name)
+        end
+
+        return file_name
+    end
+
+    -- Opens a file.
+    function env.open_file(file_name, read_write, binary_text)
+        local file_path, mode
+        if read_write == "read" then
+            file_path = resolve_input_name(file_name)
+            mode = "r"
+        elseif read_write == "write" then
+            file_path = resolve_output_name(file_name)
+            mode = "w"
+        else
+            error("Invalid read/write mode:", read_write)
+        end
+
+        if binary_text == "binary" then
+            mode = mode .. "b"
+        elseif binary_text == "text" then
+            mode = mode .. ""
+        else
+            error("Invalid binary/text mode:", binary_text)
+        end
+
+        local file, message = io_open(file_path, mode)
+
+        if not file then
+            error("Cannot open file:", file_path, message)
+        end
+
+        return file
+    end
+
+    -- Open an PDF file.
+    function env.pdfe.open(file_name)
+        local file_path = resolve_input_name(file_name)
+        return pdfe_open(file_path)
+    end
+
+    -- Open an image file.
+    function env.open_image(file_name, page, box)
+        local file_path = resolve_input_name(file_name)
+        return img_scan {
+            filename = file_path,
+            filepath = file_path,
+            page     = page,
+            pagebox  = box,
+        }
+    end
+
+    if not img_scan then
+        env.open_image = false
+    end
 end
 
--- Get the value of the environment variable that decides which version to run.
-local env_choice = os.env["TEXLIVE_EXTRACTBB"]
+-- Prevent trying to change the environment.
+local function bad_index(...)
+    env.error("Attempt to access an undefined index:", select(2, ...))
+end
 
--- If the environment variable is set to a file path, run that directly.
-local env_mode = lfs.attributes(env_choice or "", "mode")
-if (env_mode == "file") or (env_mode == "link") then
-    arg[0] = env_choice
-    table.insert(arg, 1, env_choice)
-    arg[-1] = nil
-    return os.exec(arg)
+setmetatable(env, {
+    __index     = bad_index,
+    __metatable = false,
+    __newindex  = bad_index,
+})
+
+-- Set the environment.
+_ENV = env
+
+
+-----------------------------------
+--- Post-Sandbox Initialization ---
+-----------------------------------
+
+-- Constants
+local BP_TO_SP    = 65781.76
+local IN_TO_BP    = 72
+local DATE_FORMAT = "%a %b %d %H:%M:%S %Y" -- "%c"
+
+-- Save often-used globals for a slight speed boost.
+local floor            = math.floor
+local insert           = table.insert
+local remove           = table.remove
+local script_arguments = arg
+local unpack           = table.unpack
+
+-- General-purpose functions
+local function round(number)
+    return floor(number +0.5)
 end
 
--- Find the subscripts
-kpse.set_program_name("texlua", "extractbb")
 
-local function find_script(name)
-    -- Find the script, searching **only** in the scripts directories.
-    local path = kpse.lookup(
-        name,
-        { path = kpse.var_value("TEXMFSCRIPTS"), format = "lua" }
-    )
+-------------------------
+--- Argument Handling ---
+-------------------------
 
-    -- Make sure that the script is not writable.
-    if kpse.out_name_ok_silent_extended(path) then
-        if os.env["TEXLIVE_EXTRACTBB_UNSAFE"] == "unsafe" then
-            -- If we're running in development mode, then we can allow this.
-        else
-            error("Refusing to run a writable script.")
+-- Define the argument handling functions.
+local process_arguments = {}
+
+-- > Specify a PDF pagebox for bounding box
+-- > pagebox=cropbox, mediabox, artbox, trimbox, bleedbox
+local bbox_option = "auto"
+function process_arguments.B(script_arguments)
+    bbox_option = remove(script_arguments, 1)
+end
+
+-- > Show this help message and exit
+function process_arguments.h(script_arguments)
+    print [[
+Usage: extractbb [-B pagebox] [-p page] [-q|-v] [-O] [-m|-x] FILE...
+       extractbb --help|--version
+Extract bounding box from PDF, PNG, JPEG, JP2, or BMP file; default output below.
+
+Options:
+  -B pagebox    Specify a PDF pagebox for bounding box
+                pagebox=cropbox, mediabox, artbox, trimbox, bleedbox
+  -h | --help   Show this help message and exit
+  --version     Output version information and exit
+  -p page       Specify a PDF page to extract bounding box
+  -q            Be quiet
+  -v            Be verbose
+  -O            Write output to stdout
+  -m            Output .bb  file used in DVIPDFM (default)
+  -x            Output .xbb file used in DVIPDFMx
+]]
+    os.exit(0)
+end
+
+process_arguments["-help"] = process_arguments.h
+
+-- > Output version information and exit
+function process_arguments.V(script_arguments)
+    print(version)
+    os.exit(0)
+end
+
+process_arguments["-version"] = process_arguments.V
+
+-- > Specify a PDF page to extract bounding box
+local page_number = 1
+function process_arguments.p(script_arguments)
+    page_number = tonumber(remove(script_arguments, 1))
+end
+
+-- > Be quiet
+function process_arguments.q(script_arguments)
+    show_errors = false
+end
+
+-- > Be verbose
+function process_arguments.v(script_arguments)
+    show_errors = true
+end
+
+-- > Write output to stdout
+local output_file
+function process_arguments.O(script_arguments)
+    output_file = io.stdout
+end
+
+-- Output format
+local output_format = "xbb"
+
+if script_arguments[0]:match("ebb") then
+    output_format = "bb"
+end
+
+-- > Output .bb  file used in DVIPDFM (default)
+function process_arguments.m(script_arguments)
+    output_format = "bb"
+end
+
+-- > Output .xbb file used in DVIPDFMx
+function process_arguments.x(script_arguments)
+    output_format = "xbb"
+end
+
+-- Get the input file name.
+local input_name
+function process_arguments.i(script_arguments)
+    input_name = remove(script_arguments, 1)
+end
+
+process_arguments["-input-name"] = process_arguments.i
+
+-- Clear the interpreter and script names.
+script_arguments[-1] = nil
+script_arguments[0]  = nil
+
+-- Process the arguments.
+while script_arguments[1] do
+    -- Get the next argument.
+    local arg = remove(script_arguments, 1)
+    local cmd = arg:match("^%-(.*)$")
+
+    -- Default to "--input-name" if no command is given.
+    if not cmd then
+        insert(script_arguments, 1, arg)
+        cmd = "-input-name"
+    end
+
+    -- Handle multi-character arguments.
+    if (cmd:len() >= 2) and (not cmd:match("^%-")) then
+        local i = 0
+        for char in cmd:gmatch(".") do
+            i = i + 1
+            insert(script_arguments, i, "-" .. char)
         end
+
+        goto continue
     end
 
-    return path
+    -- Get the function to process the argument and run it.
+    local func = process_arguments[cmd]
+
+    if not func then
+        error("Invalid argument:", arg)
+    end
+
+    func(script_arguments)
+
+    ::continue::
 end
 
--- Map the choice names to file names.
-local choice_mapping = {
-    wrapper = find_script("extractbb-wrapper.lua"),
-    scratch = find_script("extractbb-scratch.lua"),
+-- Validate the arguments.
+if not type(page_number) == "number" then
+    error("Invalid page number:", page_number)
+end
+
+if not input_name then
+    error("No input file specified.")
+end
+
+-- Validate the bounding box type. We need this rather crazy fallback scheme
+-- to match the behaviour of "extractbb".
+local bbox_orders = {}
+bbox_orders.mediabox = {
+    { img = "media", pdfe = "MediaBox" },
 }
+bbox_orders.cropbox = {
+    { img = "crop", pdfe = "CropBox" }, unpack(bbox_orders.mediabox)
+}
+bbox_orders.artbox = {
+    { img = "art", pdfe = "ArtBox" }, unpack(bbox_orders.cropbox)
+}
+bbox_orders.trimbox = {
+    { img = "trim", pdfe = "TrimBox" }, unpack(bbox_orders.artbox)
+}
+bbox_orders.bleedbox = {
+    { img = "bleed", pdfe = "BleedBox" }, unpack(bbox_orders.trimbox)
+}
+bbox_orders.auto = {
+    bbox_orders.cropbox[1], bbox_orders.artbox[1], bbox_orders.trimbox[1],
+    bbox_orders.bleedbox[1], bbox_orders.mediabox[1],
+}
 
--- Choose the implementation to run.
-local choice = choice_mapping[env_choice] or choice_mapping[DEFAULT]
+local bbox_order = bbox_orders[bbox_option]
 
-if not choice then
-    error("No implementation of extractbb found.")
+if not bbox_order then
+    error("Invalid PDF box type:", bbox_option)
 end
 
--- And run it.
-dofile(choice)
+-- Set the default pixel resolution.
+local default_dpi
+if output_format == "xbb" then
+    default_dpi = 72
+elseif output_format == "bb" then
+    default_dpi = 100
+else
+    error("Invalid output format:", output_format)
+end
+
+-- Open the output file.
+if not output_file then
+    local base_name   = input_name:match("(.+)%.([^.]-)$") or input_name
+    local output_name = base_name .. "." .. output_format
+    output_file = open_file(output_name, "write", "text")
+end
+
+
+------------------------
+--- Image Processing ---
+------------------------
+
+local x_min, y_min, x_max, y_max
+local num_pages, image_type
+local pdf_major_version, pdf_minor_version
+
+if open_image then
+    -- Check the number of pages.
+    local image = open_image(input_name)
+    num_pages = image.pages
+
+    if page_number > num_pages then
+        error("Invalid page number:", page_number)
+    end
+
+    -- Open the image to the specified page and bounding box. If the requested
+    -- bounding box is not available, LuaTeX will fall back to the crop box
+    -- or the media box.
+    image = open_image(input_name, page_number, bbox_order[1].img)
+
+    if not image then
+        error("Cannot open image:", input_name)
+    end
+
+    -- Get the image metadata.
+    image_type   = image.imagetype
+    local bounding_box = image.bbox
+
+    if not bounding_box then
+        error("Cannot get bounding box:", page_number)
+    end
+
+    local x_resolution = image.xres
+    local y_resolution = image.yres
+
+    if (x_resolution or 0) == 0 then
+        x_resolution = default_dpi
+    end
+
+    if (y_resolution or 0) == 0 then
+        y_resolution = default_dpi
+    end
+
+    -- Convert the bounding box to PostScript points.
+    for i, dimen in ipairs(bounding_box) do
+        if image_type == "pdf" then
+            dimen = dimen / BP_TO_SP
+        else
+            if i % 2 == 1 then
+                dimen = dimen / x_resolution * IN_TO_BP
+            else
+                dimen = dimen / y_resolution * IN_TO_BP
+            end
+        end
+
+        bounding_box[i] = dimen
+    end
+
+    -- Save the bounding box.
+    x_min, y_min, x_max, y_max = unpack(bounding_box)
+
+    -- We can't get the PDF version with the "img" library, so we'll just
+    -- pretend that it's v1.5 (which supports most features).
+    pdf_major_version = 1
+    pdf_minor_version = 5
+else
+    -- Fallback to PDFs only.
+    image_type = "pdf"
+    local document = pdfe.open(input_name)
+
+    if pdfe.getstatus(document) ~= 0 then
+        error("Cannot open PDF file:", input_name)
+    end
+
+    -- Check the number of pages.
+    num_pages = pdfe.getnofpages(document)
+
+    if type(num_pages) ~= "number" then
+        error("Invalid number of pages:", num_pages)
+    end
+
+    if page_number > num_pages then
+        error("Invalid page number:", page_number)
+    end
+
+    -- Get the page.
+    local page = pdfe.getpage(document, page_number)
+
+    if not page then
+        error("Cannot get page:", page_number)
+    end
+
+    -- Get the bounding box. Here, we check the boxes in the exact same order
+    -- that "extractbb" does.
+    local bounding_box
+    for _, bbox in ipairs(bbox_order) do
+        bounding_box = pdfe.getbox(page, bbox.pdfe)
+
+        if bounding_box then
+            break
+        end
+    end
+
+    if not bounding_box then
+        error("Cannot get bounding box:", page_number)
+    end
+
+    -- Save the bounding box.
+    x_min, y_min, x_max, y_max = unpack(bounding_box)
+
+    -- Get the PDF version.
+    pdf_major_version, pdf_minor_version = pdfe.getversion(document)
+end
+
+-- Validate the bounding box.
+for _, dimen in ipairs { x_min, y_min, x_max, y_max } do
+    if type(dimen) ~= "number" then
+        error("Invalid bounding box:", x_min, y_min, x_max, y_max)
+    end
+end
+
+
+--------------
+--- Output ---
+--------------
+
+-- Get the output fields and values.
+local lines = {}
+
+insert(lines, ("Title: %s"):format(input_name))
+insert(lines, ("Creator: %s"):format(version))
+insert(lines,
+       ("BoundingBox: %d %d %d %d")
+       :format(round(x_min), round(y_min), round(x_max), round(y_max)))
+
+if output_format == "xbb" then
+    insert(lines,
+           ("HiResBoundingBox: %0.6f %0.6f %0.6f %0.6f")
+           :format(x_min, y_min, x_max, y_max))
+
+    if image_type == "pdf" then
+        insert(lines,
+               ("PDFVersion: %d.%d")
+               :format(pdf_major_version, pdf_minor_version))
+
+        insert(lines, ("Pages: %d"):format(num_pages))
+    end
+
+end
+
+insert(lines, ("CreationDate: %s"):format(os.date(DATE_FORMAT, SOURCE_DATE_EPOCH)))
+
+-- Create the output text.
+local begin_line = "%%"
+local end_line   = "\n"
+
+local text = begin_line ..
+             table.concat(lines, end_line .. begin_line) ..
+             end_line .. end_line
+
+-- Write the output text.
+output_file:write(text)
+output_file:close()
+
+-- Everything is done, so now we can exit.
+os.exit(0)

Modified: trunk/Master/tlpkg/libexec/ctan2tds
===================================================================
--- trunk/Master/tlpkg/libexec/ctan2tds	2025-02-12 14:09:34 UTC (rev 73915)
+++ trunk/Master/tlpkg/libexec/ctan2tds	2025-02-12 14:58:30 UTC (rev 73916)
@@ -4887,6 +4887,9 @@
         &SYSTEM ("ln -s $linkname $platdir/clxelatex")
           if $linkname eq "cluttex"; # cluttex->clxelatex
         #
+        &SYSTEM ("ln -s $linkname $platdir/ebb")
+          if $linkname eq "extractbb"; # ebb->extractbb
+        #
         &SYSTEM ("ln -s $linkname $platdir/r$linkname")
           if $linkname =~ /^(pdfcrop|epstopdf)$/; # rpdfcrop ->pdfcrop, ...
         #



More information about the tex-live-commits mailing list.