diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4862066..a6317de 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,7 @@ env: # The exact toolchain used for tests and releases. go.mod declares a # minimum - this is the pin. Raising it can change generated bytes, so the # byte stability guard has to be green before it moves. - GO_VERSION: "1.26.7" + GO_VERSION: "1.27.0" jobs: test: @@ -324,7 +324,7 @@ jobs: # what makes it worth having: a scanner that lists every advisory # touching the module graph produces noise, and noise gets switched off. # Measured before switching it on, 2026-08-02: no vulnerabilities found. - run: go run golang.org/x/vuln/cmd/govulncheck@v1.1.4 ./... + run: go run golang.org/x/vuln/cmd/govulncheck@v1.7.0 ./... staticcheck: name: staticcheck @@ -367,7 +367,7 @@ jobs: # both of them the word "Pillow" at the start of an error string, which # is the name of the library that refused the image rather than a # sentence. Zero findings with the config in place. - run: go run honnef.co/go/tools/cmd/staticcheck@v0.7.0 ./... + run: go run honnef.co/go/tools/cmd/staticcheck@v0.8.1 ./... lint: name: linters diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 58c9433..ff51e24 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -29,7 +29,7 @@ concurrency: env: # The same pin CI carries. A release built on a different toolchain than the # one the byte stability guards ran under is a release nobody measured. - GO_VERSION: "1.26.7" + GO_VERSION: "1.27.0" jobs: check: diff --git a/CHANGELOG.md b/CHANGELOG.md index a4ec802..53e5191 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,40 @@ because it turns other people's test suites red. ### Breaking +- **Six formats have different bytes, because the tool is built with Go 1.27 + now.** Sizes are unchanged. Every size that worked before still works, every + reader that took these files still takes them, and the same sizes are + reachable. What changed is the compressed data inside them. + + Affected: `targz`, `png`, `docx`, `xlsx`, `pptx`, `ico` when it holds a png, + and `zip` when you ask for compression. `zip` left alone is untouched, + because its default stores rather than compresses. The other seventeen + formats are byte for byte what they were. + + Go 1.27 changed `compress/flate`, which is what all of those run through. + Below the default compression level the change is only how a stream is + closed, and above it the compressor itself behaves differently. + + Two minimums moved with it. The smallest `png` is **74 B** rather than 73, + and sizes 75 to 82 and 85 are the ones it cannot produce. The smallest + `targz` is **1049 B** rather than 1052, the next size up is 1051, and 1050 is + the one it cannot produce. `tfg formats` prints the current numbers. + + **A suite pinning hashes for those formats will go red once and then stay + green.** There is no switch back: staying on the old compiler was not a + choice this tool can offer, since the compiler comes from whoever builds it. + +- **`.tar.gz` could not be produced at all under Go 1.27 until this release.** + Every size was refused with an error saying the generator produced three + bytes fewer than planned. The size of a `.tar.gz` is worked out rather than + measured - compressing twice to learn a length would make a preview cost what + the run costs - and that arithmetic carried a number that turned out to + describe one release of Go. + + It measures that number now, at first use, and checks its own answer before + trusting it. A later Go release can move these bytes again, but it can no + longer stop the format from being written. + - **A generated `.tar.gz` has different bytes, because a lot of them could not be opened by a Go program.** Sizes are unchanged, every size that worked before still works, and every reader that took these files still diff --git a/go.mod b/go.mod index fb2cd74..d5e157d 100644 --- a/go.mod +++ b/go.mod @@ -28,7 +28,23 @@ go 1.26.5 // here" and "green there" have to mean the same compiler. The byte stability // guards were run under 1.26.7 before this line moved and none of them // shifted, so D11 holds and no major version is owed. -toolchain go1.26.7 +// +// Raised to 1.27.0 on 2026-09-01, and this one IS owed a major version. Go +// 1.27 changed compress/flate, so every format that puts bytes through deflate +// produces different ones - eleven of the fifty one pinned cases moved, and all +// seven of the pinned standard library paths. Sizes are unchanged and the same +// sizes are reachable. Decision by the owner, and the reason was not that the +// release is better: this machine builds other projects that are already on +// 1.27, a toolchain setting belongs to the account rather than to a project, so +// the two were taking it in turns. Staying meant a check before every command +// forever. Written up in docs/GO-127-MIGRATION.md. +// +// TAR.GZ could not be produced at all under 1.27 until this move, because its +// size arithmetic carried the gzip framing as a constant and the block that +// closes a level zero stream went from five bytes to two. It measures the +// framing now, so the next release moves the bytes again but does not stop the +// format from being written. +toolchain go1.27.0 require github.com/goccy/go-yaml v1.19.2 diff --git a/internal/format/targz/compress.go b/internal/format/targz/compress.go index df6483e..581e68e 100644 --- a/internal/format/targz/compress.go +++ b/internal/format/targz/compress.go @@ -126,8 +126,34 @@ func nextFiller(target, got, filler int64) (next, extra int64, useExtra, done bo case deficit < 0 || (deficit > 0 && deficit < 2): // Overshot, or left a remainder the extra field cannot hold: it costs // two bytes before it holds anything. Give the filler back enough that - // the field has room to work. - return filler - (2 - deficit), 0, false, false + // the field has room to work, and give back A WHOLE BLOCK MORE. + // + // The block is the point, and giving back only the overshoot is what + // used to happen and does not converge. The filler is a tar entry, and + // a tar entry is padded up to a whole 512 byte block - so handing back + // fewer than 512 bytes changes WHICH bytes the archive carries and not + // HOW MANY. What comes out the other side of gzip then wobbles by a + // byte or so either way, and the walk spends its rounds stepping five + // bytes at a time across a staircase, never landing. + // + // Measured 2026-09-01: a 256 KiB archive at compression best sat at + // 262 147 and 262 148 B for eight rounds while the filler came down + // from 258 481 to 258 447. This had been true all along and Go 1.27 + // only moved which sizes land on a step edge, so it looked like the + // compiler broke it. Probe: tools/probes/targzsettle. + // + // Undershooting is safe and overshooting is not: the extra field adds + // exactly what it is given, up to 65 531 B, so a round that lands under + // the target finishes on the next pass. A whole block is well inside + // that. + next := filler - (2 - deficit) - tarBlock + if next < 0 && filler > 0 { + // Try with no filler at all before deciding the size is out of + // reach. Only an archive that overshoots with nothing in it is + // genuinely too small. + next = 0 + } + return next, 0, false, false case deficit == 0: // Landed without needing the field at all. return filler, 0, false, true diff --git a/internal/format/targz/framing.go b/internal/format/targz/framing.go new file mode 100644 index 0000000..80561f1 --- /dev/null +++ b/internal/format/targz/framing.go @@ -0,0 +1,146 @@ +// What gzip costs on top of the bytes it carries, measured rather than +// written down. +// +// This file exists because a number that was written down turned out to be a +// fact about one Go release. The size of a TAR.GZ is arithmetic - it has to +// be, because the bytes pass through deflate and building the archive to +// measure it would make a preview cost what the run costs. The arithmetic +// needs to know what the gzip stream adds, and until 2026-09-01 that was three +// constants in targz.go. +// +// Go 1.27.0 changed one of them. The block that closes a level zero stream +// went from a five byte empty STORED block to a two byte one, so every archive +// came out three bytes short of its plan and the format refused to write +// anything at all - measured on every size tried, from 64 kB to 10 MB. The +// engine was right to refuse. The arithmetic was describing Go 1.26. +// +// Bumping the constant would have worked until the next release. Measuring +// asks the library that is actually linked, so it holds for the one after that +// too. The model is the same under both releases and only the constants move: +// +// overhead(n) = base + perBlock * ceil(n / storeBlock) +// +// Measured 2026-09-01, level zero: perBlock is 5 under both, base is 23 under +// go1.26.7 and 20 under go1.27.0. +// +// One honest limit, because it would otherwise look like this file is proven +// and it is only half proven. Replacing the measurement with today's two +// constants written down would pass every test in this repository, today, on +// this compiler - the mutation runner was pointed at exactly that and it +// cannot go red. What the measurement buys is the NEXT release, and no test +// that runs today can demonstrate that. The mutations here cover the +// arithmetic being wrong. They cannot cover it being right for the wrong +// reason. That is why this comment is long: it is the only thing standing +// between a later reader and a tidy simplification back to the bug. +package targz + +import ( + "bytes" + "compress/gzip" + "fmt" + "io" +) + +// framing is what a level zero gzip stream costs beyond its content. +type framing struct { + // base is the header, the trailer, and the block that closes the stream. + base int64 + // perBlock is what each stored block of content costs. + perBlock int64 +} + +// The framing, measured once before anything can ask for it. +// +// A package level variable rather than a sync.Once, and the guard against +// stray concurrency is what pointed that out. The first version reached for +// sync.OnceValues to keep the measurement lazy, and lazy bought nothing here: +// the registry works out this format's minimum during init, which asks for the +// framing anyway, so every program was paying for it before main started +// whichever way it was written. Go initialises a package variable exactly once +// and before any goroutine exists, so there is nothing here for a lock to +// protect. +var framingValue, framingErr = measureFraming() + +// measuredFraming hands back what was measured, in the shape the callers want. +func measuredFraming() (framing, error) { return framingValue, framingErr } + +// measureFraming works the two constants out from three compressions, and then +// checks the model against a fourth. +// +// Two points settle the line and the third says whether it is a line at all. +// Without that check a change to the BLOCK SIZE - rather than to the cost of a +// block - would be read as a change to the constants, and the arithmetic would +// be quietly wrong instead of loudly refused. That is the failure this whole +// file exists to stop happening a second time, so it is worth one more +// compression of a buffer that is already in memory. +func measureFraming() (framing, error) { + one, err := storedOverhead(storeBlock) + if err != nil { + return framing{}, err + } + two, err := storedOverhead(2 * storeBlock) + if err != nil { + return framing{}, err + } + + f := framing{perBlock: two - one} + f.base = one - f.perBlock + + // Two independent checks the two points above cannot make on their own: a + // third multiple of the block, and the empty stream, which is base alone. + three, err := storedOverhead(3 * storeBlock) + if err != nil { + return framing{}, err + } + empty, err := storedOverhead(0) + if err != nil { + return framing{}, err + } + if want := f.base + 3*f.perBlock; three != want { + return framing{}, fmt.Errorf( + "targz: this build of Go frames a gzip stream in a shape this tool does not understand. "+ + "Three blocks of content cost %d B where the two measured before them predict %d B, "+ + "so the size of an archive cannot be worked out without building it", + three, want) + } + if empty != f.base { + return framing{}, fmt.Errorf( + "targz: this build of Go frames an empty gzip stream at %d B where the measurement says %d B, "+ + "so the size of an archive cannot be worked out without building it", + empty, f.base) + } + return f, nil +} + +// storedOverhead is what gzip adds to n bytes at compression level zero. +// +// The content is zeros, and that is safe precisely because the level is zero: +// stored blocks carry their input unchanged, so the framing does not depend on +// what is in them. At any other level it would. +func storedOverhead(n int64) (int64, error) { + var out bytes.Buffer + w, err := gzip.NewWriterLevel(&out, gzip.NoCompression) + if err != nil { + return 0, err + } + if n > 0 { + if _, err := io.CopyN(w, zeros{}, n); err != nil { + return 0, err + } + } + if err := w.Close(); err != nil { + return 0, err + } + return int64(out.Len()) - n, nil +} + +// zeros is an endless run of zero bytes, so the measurement allocates one +// small buffer rather than the megabyte it reads. +type zeros struct{} + +func (zeros) Read(p []byte) (int, error) { + for i := range p { + p[i] = 0 + } + return len(p), nil +} diff --git a/internal/format/targz/size.go b/internal/format/targz/size.go index 208db8e..9cfd9ba 100644 --- a/internal/format/targz/size.go +++ b/internal/format/targz/size.go @@ -79,12 +79,19 @@ func tarLength(m memo) int64 { } // gzipFixed is the whole file except the comment. +// +// The framing comes from framing.go, which asks the gzip that is linked rather +// than reading a number written here. If that measurement refused, this uses a +// zero framing and the sizes are nonsense - which is safe only because Plan +// returns the refusal before anything is written or announced. Nothing acts on +// a size worked out from a framing this tool did not understand. func gzipFixed(tarLen int64) int64 { + f, _ := measuredFraming() blocks := tarLen / storeBlock if tarLen%storeBlock != 0 { blocks++ } - return gzipFraming + storeBlockCost*(blocks+1) + tarLen + return f.base + f.perBlock*blocks + tarLen } // commentCost is what a comment adds to the file: its bytes plus the zero that diff --git a/internal/format/targz/targz.go b/internal/format/targz/targz.go index 939d675..07e425c 100644 --- a/internal/format/targz/targz.go +++ b/internal/format/targz/targz.go @@ -72,14 +72,16 @@ const ( // storeBlock is the largest run of bytes gzip emits as one stored block at // compression level zero. It decides the framing overhead, so it decides // the arithmetic in size.go. + // + // This one stays written down because it is a fact about DEFLATE - a + // stored block carries a sixteen bit length - rather than about a release + // of Go. What each block COSTS, and what the header, trailer and closing + // block cost, moved to framing.go and are measured, because those did turn + // out to be facts about a release. framing.go checks this number too: if a + // build ever framed at a different block size, the model would stop + // predicting a third block and the format refuses rather than guessing. storeBlock = 65535 - // gzipFraming is the fixed ten byte header plus the eight byte trailer. - gzipFraming = 18 - - // storeBlockCost is what each stored block costs on top of its content. - storeBlockCost = 5 - writeChunk = 32 * 1024 ) @@ -188,6 +190,13 @@ type memo struct { } func (generator) Plan(r format.Request) (format.Plan, error) { + // Before anything else, because every size below is worked out from this + // and a framing this tool does not understand has to be a refusal rather + // than an archive of the wrong length. See framing.go. + if _, err := measuredFraming(); err != nil { + return format.Plan{}, err + } + groups, err := archive.Groups("targz", r) if err != nil { return format.Plan{}, err diff --git a/internal/guard/testdata/generator-golden.json b/internal/guard/testdata/generator-golden.json index 2b9c963..9b6e562 100644 --- a/internal/guard/testdata/generator-golden.json +++ b/internal/guard/testdata/generator-golden.json @@ -34,13 +34,13 @@ }, "docx_32kib": { "bytes": 32768, - "sha256": "ba7994133148c6016a7b2c2bd1361c8b98b9b55abbf9471fa5256ce746dc7c66", - "measured_on": "2026-08-19" + "sha256": "b3012da61624aae102bc8b8b55b031e5881daa77086eaeb0f95efc09a765635e", + "measured_on": "2026-09-01" }, "docx_32kib_many_paragraphs": { "bytes": 32768, - "sha256": "11a40c75a83468e9adddc9fc00ce9f4edd3a658cc03cc81c3700ed2238230cd4", - "measured_on": "2026-08-19" + "sha256": "a72407997a6b22d6955c1813b5df8b67fc9d11a0b1d596fbd34e19324e9d1511", + "measured_on": "2026-09-01" }, "gif_64kib": { "bytes": 65536, @@ -68,8 +68,8 @@ }, "ico_32kib_png_inside": { "bytes": 32768, - "sha256": "72197d05fa9cccac8545d01b5878ea2f5b3e69cb13a3120f546e8a4ece799d01", - "measured_on": "2026-08-19" + "sha256": "e7a3c65676224d2f7c79ce1b54b93124fafd8a7f158afe43f86ea99805f6cba8", + "measured_on": "2026-09-01" }, "jpg_192kib_many_segments": { "bytes": 196608, @@ -140,17 +140,18 @@ }, "png_64kib": { "bytes": 65536, - "sha256": "85e174c37ebd91b39afdd5b4ca906d7b996c8ceb43a287424412b08ab815e521" + "sha256": "65c6153643fe861e3e32149cefb638f5209a9c783cf702e655585d4e405ff657", + "measured_on": "2026-09-01" }, "pptx_32kib": { "bytes": 32768, - "sha256": "5ff36a308816c9938deba34d83d175a33df1f757acc872ba39305c46282ebdb4", - "measured_on": "2026-08-19" + "sha256": "a8383149e40ca00d0b3cc118290cdd163cfcd6480b4c8c68e0913bda1b3462ef", + "measured_on": "2026-09-01" }, "pptx_32kib_five_slides": { "bytes": 32768, - "sha256": "6a9db5085b0b8d2b288290c2169422307d7d61f8eb08c3c1cfe880c7189b4555", - "measured_on": "2026-08-19" + "sha256": "0e2c4b5722b4b123a4eeae4d64570b4a158af82cbc138a3063b98e4c118bb2e3", + "measured_on": "2026-09-01" }, "svg_8kib": { "bytes": 8192, @@ -158,17 +159,17 @@ }, "targz_16kib": { "bytes": 16384, - "sha256": "47304109c42a00e600fb25cfa25bc218140a7bb5b3914b5310cfbb3874366baa", + "sha256": "e44c58b38ede2c3bc6c332fd807a7d182b42b4f087a9899aad39d318aeb515a6", "measured_on": "2026-09-01" }, "targz_past_the_comment_limit": { "bytes": 262144, - "sha256": "8b20c23fb1e2ed4dae11755ef7f356a65377a83b3323c7be20bfafa42f5511d2", + "sha256": "e0ea316d5fd0d3dee866a551660cd7626fc1902d5d36dfdb2e1ca8e240395e13", "measured_on": "2026-09-01" }, "targz_with_three_pdfs": { "bytes": 65536, - "sha256": "01969ba9c8368dbc8af3810291cf37468359b1b6b2ba9bf224c1b72cde19c612", + "sha256": "e13105e912e3ebef8f5a41bb30be6160e6afd8611cec245aa781fc26d21d0d9c", "measured_on": "2026-09-01" }, "tiff_100kib_sized_to_fit": { @@ -215,13 +216,13 @@ }, "xlsx_32kib": { "bytes": 32768, - "sha256": "11c19f918b1a996634117117c63e337513db7cadaa1872880482b59abfc7e1da", - "measured_on": "2026-08-19" + "sha256": "37ca56ea77ebcfa13af54b16fba2c95d10fc4f762f6c9a04506d83a340a3b35a", + "measured_on": "2026-09-01" }, "xlsx_32kib_grid": { "bytes": 32768, - "sha256": "c60de4f39aac004d4cc42369fc59e910dca5224b12567deaade3e63c85a3163e", - "measured_on": "2026-08-19" + "sha256": "ad434e7c8c47519c4b96f2b826bf6eceabd846b6e9e5e2cc8c1ef7a86188f328", + "measured_on": "2026-09-01" }, "xml_8kib": { "bytes": 8192, @@ -282,6 +283,10 @@ "pdf_16kib", "zip_with_three_pdfs" ] + }, + { + "on": "2026-09-01", + "why": "The project moved from Go 1.26.7 to Go 1.27.0, and Go 1.27 changed compress/flate. Every format here that puts bytes through deflate produces different ones: docx, pptx and xlsx compress their parts, png compresses its image data, ico when it holds a png, and targz through gzip. Sizes are unchanged - every one of these files is the length it was, and the same sizes are reachable. At compression level zero the change is framing rather than algorithm: the block that closes the stream went from five bytes to two. Above level zero the algorithm itself moved, so the difference is larger and not constant. zip is absent from this list because its default is store, which puts nothing through deflate - ask for compression and it moves too. The tar.gz arithmetic that broke on this now MEASURES the framing rather than carrying it as a constant, so a later Go release moves the bytes again but does not stop the format from being produced." } ] } diff --git a/internal/guard/testdata/stdlib-golden.json b/internal/guard/testdata/stdlib-golden.json index ff5bf53..749b23e 100644 --- a/internal/guard/testdata/stdlib-golden.json +++ b/internal/guard/testdata/stdlib-golden.json @@ -1,37 +1,37 @@ { "note": "Measured values for the standard library paths that produce our bytes. If a test goes red against this file, that is a breaking change - bump the major version and write the changelog entry. Never edit the numbers here to make a run green.", - "measured_on": "2026-08-01", - "go_version": "go1.26.5 windows/amd64", + "measured_on": "2026-09-01", + "go_version": "go1.27.0 windows/amd64", "paths": { "flate_level_1": { - "bytes": 971, - "sha256": "6712e08ec1ada0f3d35f07100a0b726b6112b24cb94ad69920fe5e65317f3dee" + "bytes": 577, + "sha256": "fd1f4f82788ac84d8647b5189a9a8479775e0605085e73fe8471017123e33d44" }, "flate_default": { - "bytes": 574, - "sha256": "ff87807f62eca68f717f730c0a7a23a4a31f2197472aa9beaa0d9a96912483e3" + "bytes": 577, + "sha256": "fd1f4f82788ac84d8647b5189a9a8479775e0605085e73fe8471017123e33d44" }, "gzip_default": { - "bytes": 592, - "sha256": "51af913d03c1595df26696ede2751448249b578393147c0106566cc49ec56810" + "bytes": 595, + "sha256": "461bb8b70264485c48a6ec4d924e8e42e19d8289055242158293bb3e510ee448" }, "gzip_store": { - "bytes": 65569, - "sha256": "324638839c7230e01c38c9c21925c62dfecbeea820138249c7f70d4e505ee52b", + "bytes": 65566, + "sha256": "da6527c020455e10b4a99d9f0a883bbae3b77a9a1c2c6a8fe12c6af1af1b8035", "measured_on": "2026-08-04", "why": "TAR.GZ writes every archive through this path, and its exact size arithmetic depends on the stored block framing it produces. Confirmed identical on go1.26.5 windows/amd64 and go1.26.5 linux/amd64, and byte for byte across six toolchains - go1.21.13, go1.22.12, go1.23.12, go1.24.0, go1.25.0 and go1.26.5 - by tools/toolchain-sweep.py, which runs the same inputs in a standalone module because go.mod here requires 1.26.5 and an older toolchain will not build it." }, "zip_deflate": { - "bytes": 728, - "sha256": "bf16317f2aef3141a567dd17eb272839d1cd382e4b1125a79d5c1de0e63d7169" + "bytes": 731, + "sha256": "ff29df157b068b2254dc30932ab9b1a21e99c9c99c08ea461201fc4ba4db173e" }, "png_default": { - "bytes": 1319, - "sha256": "98e83eaa0f8b23a39e36eb9f93c6500001ee44f62f9f9060329c9b71c545dec7" + "bytes": 1400, + "sha256": "5c7a9cd72c2471549201fcacea29cf613c5809ba9ffd3ccbf06264bed8a5428b" }, "png_best": { - "bytes": 1313, - "sha256": "eed381ee6b99dfb4b2039333efd83b06c637d602fc7e791f495e5353e953246e" + "bytes": 1310, + "sha256": "35526c2b39059a53bec53151ca03f7391f3e58b6d637e977ecd9b8d6553d51a8" } } } diff --git a/internal/gui/window/generate.go b/internal/gui/window/generate.go index 5fe47a4..b5708f7 100644 --- a/internal/gui/window/generate.go +++ b/internal/gui/window/generate.go @@ -502,16 +502,16 @@ func (g *Generate) settle() ([]engine.Target, engine.Options, error) { } return []engine.Target{{ - ID: g.id.Text, - Format: g.formatPick.Selected, - Sizes: engine.Uniform(files, bytesWanted), - NameTmpl: g.name.Text, - Label: g.label.Checked, - Properties: g.properties(), - }}, engine.Options{ - OutDir: g.outDir.Text, - Seed: seed, - Command: "tfg-gui", - ManifestName: engine.DefaultManifestName, - }, nil + ID: g.id.Text, + Format: g.formatPick.Selected, + Sizes: engine.Uniform(files, bytesWanted), + NameTmpl: g.name.Text, + Label: g.label.Checked, + Properties: g.properties(), + }}, engine.Options{ + OutDir: g.outDir.Text, + Seed: seed, + Command: "tfg-gui", + ManifestName: engine.DefaultManifestName, + }, nil } diff --git a/web/public/formats/index.html b/web/public/formats/index.html index 7fb5993..b3f2c34 100644 --- a/web/public/formats/index.html +++ b/web/public/formats/index.html @@ -117,7 +117,7 @@

24 file formats, every one generated at an exact size

docx .docx - 1227 + 1220 full libreoffice @@ -187,14 +187,14 @@

24 file formats, every one generated at an exact size

png .png - 73 + 74 full pillow pptx .pptx - 4826 + 4809 full libreoffice @@ -208,7 +208,7 @@

24 file formats, every one generated at an exact size

targz .tar.gz - 9793 + 9790 full 7z @@ -243,7 +243,7 @@

24 file formats, every one generated at an exact size

xlsx .xlsx - 1735 + 1726 full libreoffice diff --git a/web/public/pl/formaty/index.html b/web/public/pl/formaty/index.html index d4f1a83..c8aea5e 100644 --- a/web/public/pl/formaty/index.html +++ b/web/public/pl/formaty/index.html @@ -116,7 +116,7 @@

24 formatów plików, każdy generowany o dokładnym rozmiarze

docx .docx - 1227 + 1220 full libreoffice @@ -186,14 +186,14 @@

24 formatów plików, każdy generowany o dokładnym rozmiarze

png .png - 73 + 74 full pillow pptx .pptx - 4826 + 4809 full libreoffice @@ -207,7 +207,7 @@

24 formatów plików, każdy generowany o dokładnym rozmiarze

targz .tar.gz - 9793 + 9790 full 7z @@ -242,7 +242,7 @@

24 formatów plików, każdy generowany o dokładnym rozmiarze

xlsx .xlsx - 1735 + 1726 full libreoffice